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 ac80b8270..8e186cf79 100644 --- a/README.md +++ b/README.md @@ -377,40 +377,41 @@ that handles configuring the app. When the app settings are opened, this function is called with one argument, `back`: a callback to return to the settings menu. -Usually it will save any information in `app.json` where `app` is the name +Usually it will save any information in `myappid.json` where `myappid` is the name of your app - so you should change the example accordingly. Example `settings.js` ```js // make sure to enclose the function in parentheses (function(back) { - let settings = require('Storage').readJSON('app.json',1)||{}; + 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('app.json',settings); + require('Storage').write('myappid.json', settings); } const appMenu = { '': {'title': 'App Settings'}, '< Back': back, 'Monkeys': { - value: settings.monkeys||12, + value: settings.monkeys, onchange: (m) => {save('monkeys', m)} } }; E.showMenu(appMenu) }) ``` -In this example the app needs to add `app.settings.js` to `storage` in `apps.json`. -It should also add `app.json` to `data`, to make sure it is cleaned up when the app is uninstalled. +In this example the app needs to add `myappid.settings.js` to `storage` in `apps.json`. +It should also add `myappid.json` to `data`, to make sure it is cleaned up when the app is uninstalled. ```json - { "id": "app", + { "id": "myappid", ... "storage": [ ... - {"name":"app.settings.js","url":"settings.js"}, + {"name":"myappid.settings.js","url":"settings.js"} ], "data": [ - {"name":"app.json"} + {"name":"myappid.json"} ] }, ``` diff --git a/apps.json b/apps.json index 7fe259899..13bb5892d 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", @@ -29,10 +29,35 @@ ], "sortorder": -10 }, + { + "id": "hebrew_calendar", + "name": "Hebrew Calendar", + "shortName": "HebCal", + "version": "0.03", + "description": "lists the date according to the hebrew calendar", + "icon": "app.png", + "tags": "", + "supports": [ + "BANGLEJS", + "BANGLEJS2" + ], + "readme": "README.md", + "storage": [ + { + "name": "hebrew_calendar.app.js", + "url": "app.js" + }, + { + "name": "hebrew_calendar.img", + "url": "app-icon.js", + "evaluate": true + } + ] + }, { "id": "messages", "name": "Messages", - "version": "0.03", + "version": "0.09", "description": "App to display notifications from iOS and Gadgetbridge", "icon": "app.png", "type": "app", @@ -41,16 +66,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 +86,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.04", "description": "(BETA) App to display notifications from iOS devices", "icon": "app.png", "tags": "tool,system,ios,apple,messages,notifications", @@ -77,13 +106,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.07", - "description": "Logs health data and provides an app to view it (BETA - requires firmware 2v11)", + "version": "0.08", + "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"], @@ -100,7 +129,7 @@ "id": "launch", "name": "Launcher", "shortName": "Launcher", - "version": "0.08", + "version": "0.10", "description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.", "icon": "app.png", "type": "launch", @@ -108,14 +137,16 @@ "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.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]}, + {"name":"launch.settings.js","url":"settings.js","supports":["BANGLEJS2"]} ], + "data": [{"name":"launch.json"}], "sortorder": -10 }, { "id": "setting", "name": "Settings", - "version": "0.33", + "version": "0.35", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", @@ -131,11 +162,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"]}, @@ -165,7 +197,7 @@ { "id": "locale", "name": "Languages", - "version": "0.09", + "version": "0.11", "description": "Translations for different countries", "icon": "locale.png", "type": "locale", @@ -211,7 +243,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"}], @@ -231,15 +263,17 @@ "id": "mywelcome", "name": "Customised Welcome", "shortName": "My Welcome", - "version": "0.12", + "version": "0.13", "description": "Appears at first boot and explains how to use Bangle.js. Like 'Welcome', but can be customised with a greeting", "icon": "app.png", "tags": "start,welcome", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "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"}, + {"name":"mywelcome.app.js","url":"app-bangle1.js","supports": ["BANGLEJS"]}, + {"name":"mywelcome.app.js","url":"app-bangle2.js","supports": ["BANGLEJS2"]}, {"name":"mywelcome.settings.js","url":"settings.js"}, {"name":"mywelcome.img","url":"app-icon.js","evaluate":true} ], @@ -263,6 +297,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", @@ -273,6 +321,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} @@ -287,6 +336,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"}, @@ -417,6 +467,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} @@ -439,6 +490,27 @@ {"name":"matrixclock.img","url":"matrixclock-icon.js","evaluate":true} ] }, + { + "id": "mandelbrotclock", + "name": "Mandelbrot Clock", + "version": "0.01", + "description": "A mandelbrot set themed clock cool", + "icon": "mandelbrotclock.png", + "screenshots": [{ "url": "screenshot_mandelbrotclock.png" }], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + { "name": "mandelbrotclock.app.js", "url": "mandelbrotclock.js" }, + { + "name": "mandelbrotclock.img", + "url": "mandelbrotclock-icon.js", + "evaluate": true + } + ] + }, { "id": "imgclock", "name": "Image background clock", @@ -467,6 +539,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"}, @@ -543,13 +616,14 @@ { "id": "cubescramble", "name": "Cube Scramble", - "version":"0.02", - "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"], + "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} @@ -599,7 +673,7 @@ { "id": "compass", "name": "Compass", - "version": "0.04", + "version": "0.05", "description": "Simple compass that points North", "icon": "compass.png", "screenshots": [{"url":"screenshot_compass.png"}], @@ -653,7 +727,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", @@ -672,7 +746,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", @@ -723,11 +797,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} @@ -740,7 +814,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} @@ -749,12 +823,12 @@ { "id": "weather", "name": "Weather", - "version": "0.10", + "version": "0.11", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], "tags": "widget,outdoors", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "readme": "readme.md", "storage": [ {"name":"weather.app.js","url":"app.js"}, @@ -768,12 +842,13 @@ { "id": "chargeanim", "name": "Charge Animation", - "version": "0.01", + "version": "0.02", "description": "When charging, show a sideways charging animation and keep the screen on. When removed from the charger load the clock again.", "icon": "icon.png", "tags": "battery", - "supports": ["BANGLEJS"], + "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"}, @@ -839,7 +914,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", @@ -892,7 +967,7 @@ "icon": "widget.png", "type": "widget", "tags": "widget", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"widchime.wid.js","url":"widget.js"}, {"name":"widchime.settings.js","url":"settings.js"} @@ -908,7 +983,7 @@ "icon": "widget.png", "type": "widget", "tags": "widget", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"widram.wid.js","url":"widget.js"} ] @@ -979,6 +1054,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} @@ -1180,6 +1256,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} @@ -1195,6 +1272,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} @@ -1210,6 +1288,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} @@ -1241,6 +1320,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} @@ -1256,6 +1336,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} @@ -1323,6 +1404,7 @@ "icon": "show-color.png", "type": "app", "tags": "tool", + "screenshots": [{"url":"bangle1-view-color-screenshot.png"}], "supports": ["BANGLEJS"], "allow_emulator": true, "storage": [ @@ -1338,6 +1420,7 @@ "icon": "clock-mixed.png", "type": "clock", "tags": "clock", + "screenshots": [{"url":"bangle1-mixed-clock-screenshot.png"}], "supports": ["BANGLEJS"], "allow_emulator": true, "storage": [ @@ -1355,6 +1438,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} @@ -1368,6 +1452,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"}, @@ -1427,6 +1512,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} @@ -1443,6 +1529,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} @@ -1502,6 +1589,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} @@ -1516,6 +1604,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"}, @@ -1530,6 +1619,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": [ @@ -1562,6 +1652,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} @@ -1608,6 +1699,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} @@ -1652,6 +1744,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} @@ -1690,13 +1783,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": [ @@ -1714,6 +1807,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} @@ -1823,6 +1917,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} @@ -1851,6 +1946,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} @@ -1860,11 +1956,12 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.09", - "description": "[BETA] Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are", + "version": "0.11", + "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "icon": "app.png", - "tags": "outdoors,gps", + "tags": "outdoors,gps,osm", "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], "custom": "custom.html", "customConnect": true, "storage": [ @@ -1894,11 +1991,12 @@ "id": "chronowid", "name": "Chrono Widget", "shortName": "Chrono Widget", - "version": "0.03", + "version": "0.04", "description": "Chronometer (timer) which runs as widget.", "icon": "app.png", "tags": "tool,widget", "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], "readme": "README.md", "storage": [ {"name":"chronowid.wid.js","url":"widget.js"}, @@ -1928,7 +2026,7 @@ "icon": "custom.png", "type": "bootloader", "tags": "tool,system", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "custom": "custom.html", "storage": [ {"name":"custom"} @@ -1943,6 +2041,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"}, @@ -1976,6 +2075,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} @@ -1992,6 +2092,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}, @@ -2029,6 +2130,19 @@ {"name":"snake.img","url":"snake-icon.js","evaluate":true} ] }, + { "id": "snek", + "name": "The snek game", + "shortName":"Snek", + "version": "0.01", + "description": "A snek game where you control a snek to eat all the apples!", + "icon": "snek-icon.js", + "supports": ["BANGLEJS2"], + "tags": "game,fun", + "storage": [ + {"name":"snek.app.js","url":"snek.js"}, + {"name":"snek.img","url":"snek-icon.js","evaluate":true} + ] + }, { "id": "calculator", "name": "Calculator", @@ -2118,13 +2232,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}, @@ -2140,6 +2255,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"}, @@ -2173,6 +2289,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} @@ -2250,6 +2367,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} @@ -2312,6 +2430,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}, @@ -2363,6 +2482,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}, @@ -2379,6 +2499,7 @@ "icon": "beebclock.png", "type": "clock", "tags": "clock", + "screenshots": [{"url":"bangle1-beeb-clock-screenshot.png"}], "supports": ["BANGLEJS"], "allow_emulator": true, "storage": [ @@ -2412,6 +2533,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"}, @@ -2490,6 +2612,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, @@ -2603,6 +2726,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} @@ -2630,6 +2754,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"}, @@ -2639,14 +2764,16 @@ { "id": "magnav", "name": "Navigation Compass", - "version": "0.04", + "version": "0.05", "description": "Compass with linear display as for GPSNAV. Has Tilt compensation and remembers calibration.", + "screenshots": [{"url":"screenshot-b2.png"},{"url":"screenshot-light-b2.png"}], "icon": "magnav.png", "tags": "tool,outdoors", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ - {"name":"magnav.app.js","url":"magnav.min.js"}, + {"name":"magnav.app.js","url":"magnav_b1.js","supports":["BANGLEJS"]}, + {"name":"magnav.app.js","url":"magnav_b2.js","supports":["BANGLEJS2"]}, {"name":"magnav.img","url":"magnav-icon.js","evaluate":true} ], "data": [{"name":"magnav.json"}] @@ -2675,6 +2802,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"}, @@ -2730,8 +2858,9 @@ { "id": "multiclock", "name": "Multi Clock", - "version": "0.08", - "description": "Clock with multiple faces. Switch between faces with BTN1 & BTN3 or swipe left-right. For best display set theme Background 2 to cyan or some other bright colour in settings.", + "version": "0.09", + "description": "Clock with multiple faces. Switch between faces with BTN1 & BTN3 (Bangle 2 touch top-right, bottom right). For best display set theme Background 2 to cyan or some other bright colour in settings.", + "screenshots": [{"url":"screen-ana.png"},{"url":"screen-big.png"},{"url":"screen-td.png"},{"url":"screen-nifty.png"},{"url":"screen-word.png"},{"url":"screen-sec.png"}], "icon": "multiclock.png", "type": "clock", "tags": "clock", @@ -2809,6 +2938,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}, @@ -2852,6 +2982,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"}, @@ -2895,7 +3026,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", @@ -3127,15 +3258,17 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.04", - "description": "Desktop style App Launcher with six apps per page - fast access if you have lots of apps installed.", + "version": "0.05", + "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", + "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "icon": "icon.png", "type": "launch", "tags": "tool,system,launcher", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ - {"name":"dtlaunch.app.js","url":"app.js"}, + {"name":"dtlaunch.app.js","url":"app-b1.js", "supports": ["BANGLEJS"]}, + {"name":"dtlaunch.app.js","url":"app-b2.js", "supports": ["BANGLEJS2"]}, {"name":"dtlaunch.img","url":"app-icon.js","evaluate":true} ] }, @@ -3229,6 +3362,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"}, @@ -3330,6 +3464,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}, @@ -3644,15 +3779,15 @@ "id": "gbmusic", "name": "Gadgetbridge Music Controls", "shortName": "Music Controls", - "version": "0.05", + "version": "0.07", "description": "Control the music on your Gadgetbridge-connected phone", "icon": "icon.png", - "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_2.png"}], + "screenshots": [{"url":"screenshot_v1.png"},{"url":"screenshot_v2.png"}], "type": "app", "tags": "tools,bluetooth,gadgetbridge,music", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", - "allow_emulator": false, + "allow_emulator": true, "storage": [ {"name":"gbmusic.app.js","url":"app.js"}, {"name":"gbmusic.settings.js","url":"settings.js"}, @@ -3669,6 +3804,7 @@ "icon": "battleship-icon.png", "tags": "game", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-battle-ship-screenshot.png"}], "readme": "README.md", "allow_emulator": true, "storage": [ @@ -3718,12 +3854,13 @@ "id": "qmsched", "name": "Quiet Mode Schedule and Widget", "shortName": "Quiet Mode", - "version": "0.02", - "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"], + "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"qmsched","url":"lib.js"}, @@ -3835,8 +3972,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"], @@ -3872,6 +4009,7 @@ "type": "clock", "tags": "clock", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-mystic-clock-screenshot.png"}], "readme": "README.md", "allow_emulator": true, "storage": [ @@ -3888,6 +4026,7 @@ "icon": "hcclock-icon.png", "type": "clock", "tags": "clock", + "screenshots": [{"url":"bangle1-high-contrast-clock-screenshot.png"}], "supports": ["BANGLEJS"], "allow_emulator": true, "storage": [ @@ -3969,6 +4108,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} @@ -3978,10 +4118,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"}, @@ -3998,6 +4139,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} @@ -4022,15 +4164,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"} @@ -4040,7 +4191,7 @@ { "id": "antonclk", "name": "Anton Clock", - "version": "0.02", + "version": "0.03", "description": "A simple clock using the bold Anton font.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], @@ -4057,7 +4208,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", @@ -4073,7 +4224,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", @@ -4201,7 +4352,7 @@ { "id": "swiperclocklaunch", "name": "Swiper Clock Launch", - "version": "0.01", + "version": "0.02", "description": "Navigate between clock and launcher with Swipe action", "icon": "swiperclocklaunch.png", "type": "bootloader", @@ -4217,7 +4368,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"], @@ -4235,11 +4386,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" }, @@ -4250,8 +4408,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", @@ -4264,30 +4422,32 @@ ] }, { - "id": "a_battery_widget", + "id": "wid_a_battery_widget", "name": "A Battery Widget (with percentage)", "shortName":"A Battery Widget", "icon": "widget.png", - "version":"1.0", + "version":"1.01", "type": "widget", "supports": ["BANGLEJS2"], "readme": "README.md", "description": "Simple and slim battery widget with charge status and percentage", "tags": "widget,battery", "storage": [ - {"name":"a_battery_widget.wid.js","url":"widget.js"} + {"name":"wid_a_battery_widget.wid.js","url":"widget.js"} ] }, - { + { "id": "lcars", "name": "LCARS Clock", "shortName":"LCARS", "icon": "lcars.png", - "version":"0.01", + "version":"0.06", + "readme": "README.md", "supports": ["BANGLEJS2"], "description": "Library Computer Access Retrieval System (LCARS) clock.", "type": "clock", "tags": "clock", + "screenshots": [{"url":"screenshot.png"}], "storage": [ {"name":"lcars.app.js","url":"lcars.app.js"}, {"name":"lcars.img","url":"lcars.icon.js","evaluate":true} @@ -4297,15 +4457,328 @@ "name": "Binary Watch", "shortName":"BinWatch", "icon": "app.png", - "version":"0.02", - "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "version":"0.04", + "supports": ["BANGLEJS2"], + "readme": "README.md", "allow_emulator":true, "description": "Famous binary watch", "tags": "clock", "type": "clock", "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", + "version": "0.01", + "description": "Based on the original Bluetooth Music Controls. Swipe up/down for volume, left/right for previous and next, tap for play/pause and btn1 to lock and unlock the controls. Enable HID in settings, pair with your phone, then use this app to control music from your watch!", + "icon": "hidmsicswipe.png", + "tags": "bluetooth", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"hidmsicswipe.app.js","url":"hidmsicswipe.js"}, + {"name":"hidmsicswipe.img","url":"hidmsicswipe-icon.js","evaluate":true} + ] + }, + { + "id": "authentiwatch", + "name": "2FA Authenticator", + "shortName": "AuthWatch", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "version": "0.04", + "description": "Google Authenticator compatible tool.", + "tags": "tool", + "interface": "interface.html", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"authentiwatch.app.js","url":"app.js"}, + {"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.04", + "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":"Speech Timer", + "icon": "app.png", + "version":"1.01", + "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": "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"} + ] + }, + { + "id": "widbata", + "name": "Battery Level Widget (Themed)", + "shortName":"Battery Theme", + "icon": "widbata.png", + "screenshots": [{"url":"screenshot_widbata_1.png"}], + "version":"0.01", + "type": "widget", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "description": "Shows the current battery level status in the top right using the clocks colour theme", + "tags": "widget,battery", + "storage": [ + {"name":"widbata.wid.js","url":"widbata.wid.js"} + ] + }, + { + "id": "weatherClock", + "name": "Weather Clock", + "version": "0.02", + "description": "A clock which displays current weather conditions (requires Gadgetbridge and Weather apps).", + "icon": "app.png", + "screenshots": [{"url":"screens/screen1.png"}], + "type": "clock", + "tags": "clock, weather", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"weatherClock.app.js","url":"app.js"}, + {"name":"weatherClock.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "menuwheel", + "name": "Wheel Menus", + "version": "0.01", + "description": "Replace Bangle.js 2's menus with a version that contains variable-size text and a back button", + "readme": "README.md", + "icon": "icon.png", + "screenshots": [ + {"url":"screenshot_b1_dark.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_light.png"}, + {"url":"screenshot_b2_dark.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_light.png"} + ], + "type": "boot", + "tags": "system", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"menuwheel.boot.js","url":"boot.js"} + ] + }, + { "id": "widChargingStatus", + "name": "Charging Status", + "shortName":"ChargingStatus", + "icon": "widget.png", + "version":"0.1", + "type": "widget", + "description": "A simple widget that shows a yellow lightning icon to indicate whenever the watch is charging. This way one can see the charging status at a glance, no matter which battery widget is being used.", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widChargingStatus.wid.js","url":"widget.js"} + ] + }, + { + "id": "flow", + "name": "FLOW", + "shortName": "FLOW", + "version": "0.01", + "description": "A game where you have to help a flow avoid white obstacles thing by tapping! This is a demake of an app which I forgot the name of. Press BTN(1) to restart. See if you can get to 2500 score!", + "icon": "app.png", + "tags": "game", + "supports" : ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name": "flow.app.js", "url": "app.js" }, + {"name": "flow.img", "url": "app-icon.js","evaluate": true } + ] } ] diff --git a/apps/.eslintrc.json b/apps/.eslintrc.json index 9d4f8a4aa..d656c2555 100644 --- a/apps/.eslintrc.json +++ b/apps/.eslintrc.json @@ -152,6 +152,8 @@ "no-prototype-builtins": "off", "no-redeclare": "off", "no-unreachable": "warn", + "no-cond-assign": "warn", + "no-useless-catch": "warn", // TODO: "no-undef": "warn", "no-undef": "off", "no-unused-vars": "off", 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..36859c060 --- /dev/null +++ b/apps/93dub/ChangeLog @@ -0,0 +1,4 @@ +0.01: Initial version for upload +0.02: DiscoMinotaur's adjustments (removed battery and adjusted spacing) +0.03: Code style cleanup +0.04: Set 00:00 to 12:00 for 12 hour time diff --git a/apps/93dub/README.md b/apps/93dub/README.md new file mode 100644 index 000000000..3830ee023 --- /dev/null +++ b/apps/93dub/README.md @@ -0,0 +1,12 @@ +# 93 Dub + +![](screenshot.png) + +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) +Ray Holder (minor 12 hour time rendering adjustment) 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..8f662a616 --- /dev/null +++ b/apps/93dub/app.js @@ -0,0 +1,140 @@ +// 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; + } + } else if (h === 0) { + // display 12:00 instead of 00:00 for 12 hr mode + h = "12"; + } + + //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_battery_widget/ChangeLog b/apps/a_battery_widget/ChangeLog deleted file mode 100644 index 0dbd5f758..000000000 --- a/apps/a_battery_widget/ChangeLog +++ /dev/null @@ -1 +0,0 @@ -2021/11/18 | 1.0: Release for Bangle 2 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 + +![](screenshot.png) + +## 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..b3aa9e0dd --- /dev/null +++ b/apps/a_speech_timer/ChangeLog @@ -0,0 +1,2 @@ +1.00: Release (2021/12/01) +1.01: Grey font when timer is frozen (2021/12/04) 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 + +![](screenshot0.png) +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) + +## 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..440cd92c6 --- /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().setFontAlign(0, 0).setColor(istimeron ? "#000" : "#444"); + g.setFont("Michroma36").drawString(timeToString(current_value), 88, 62); + + g.reset().setFontAlign(0, 0); + + 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/antonclk/ChangeLog b/apps/antonclk/ChangeLog index 8c2a33143..f88276a90 100644 --- a/apps/antonclk/ChangeLog +++ b/apps/antonclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Load widgets after setUI so widclk knows when to hide +0.03: Clock now shows day of week under date. diff --git a/apps/antonclk/app.js b/apps/antonclk/app.js index f6fcf1708..7912dfc0f 100644 --- a/apps/antonclk/app.js +++ b/apps/antonclk/app.js @@ -23,6 +23,7 @@ function draw() { var date = new Date(); var timeStr = require("locale").time(date,1); var dateStr = require("locale").date(date).toUpperCase(); + var dowStr = require("locale").dow(date).toUpperCase(); // draw time g.setFontAlign(0,0).setFont("Anton"); g.clearRect(0,y-40,g.getWidth(),y+35); // clear the background @@ -32,6 +33,10 @@ function draw() { g.setFontAlign(0,0).setFont("6x8",2); g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background g.drawString(dateStr,x,y); + //draw day of week + y += 16; + g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background + g.drawString(dowStr,x,y); // queue draw in one minute queueDraw(); } diff --git a/apps/authentiwatch/ChangeLog b/apps/authentiwatch/ChangeLog new file mode 100644 index 000000000..e1b8ed5bc --- /dev/null +++ b/apps/authentiwatch/ChangeLog @@ -0,0 +1,4 @@ +0.04: Fix tapping at very bottom of list, exit on inactivity +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 new file mode 100644 index 000000000..8d0e74a0c --- /dev/null +++ b/apps/authentiwatch/README.md @@ -0,0 +1,29 @@ +# 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 +* Hash calculations: + * Bangle 1: SHA1 only (same as Google Authenticator) + * Bangle 2: All (SHA1, SHA256, SHA512) +* Timed (TOTP) and Counter (HOTP) modes +* Custom periods +* Between 6 and 10 digits +* Phone/PC configuration web page: + * Add/edit/delete/arrange tokens + * Scan QR codes + * Produce scannable QR codes + +## Usage + +* Use the Phone/PC web page interface to manage the tokens stored on the watch. +* Tokens are stored *ONLY* on the watch. +* Swipe right to exit to the app launcher. +* Swipe left on selected counter token to advance the counter to the next value. + +## Creator + +Andrew Gregory (andrew.gregory at gmail) diff --git a/apps/authentiwatch/app-icon.js b/apps/authentiwatch/app-icon.js new file mode 100644 index 000000000..c901fb843 --- /dev/null +++ b/apps/authentiwatch/app-icon.js @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000..c0cb608c0 --- /dev/null +++ b/apps/authentiwatch/app.js @@ -0,0 +1,325 @@ +const tokenentryheight = 46; +// Hash functions +const crypto = require("crypto"); +const algos = { + "SHA512":{sha:crypto.SHA512,retsz:64,blksz:128}, + "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"; + +// sample settings: +// {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}} +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 +// +// Example: +// +// otpauth://totp/${url}:AA_${algorithm}_${digits}dig_${period}s@${url}?algorithm=${algorithm}&digits=${digits}&issuer=${url}&period=${period}&secret=${secret} +// +// ${algorithm} : one of SHA1 / SHA256 / SHA512 +// ${digits} : one of 6 / 8 +// ${period} : one of 30 / 60 +// ${url} : a domain name "example.com" +// ${secret} : the seed code + +function b32decode(seedstr) { + // RFC4648 + var i, buf = 0, bitcount = 0, retstr = ""; + for (i in seedstr) { + var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(seedstr.charAt(i).toUpperCase(), 0); + if (c != -1) { + buf <<= 5; + buf |= c; + bitcount += 5; + if (bitcount >= 8) { + retstr += String.fromCharCode(buf >> (bitcount - 8)); + buf &= (0xFF >> (16 - bitcount)); + bitcount -= 8; + } + } + } + if (bitcount > 0) { + retstr += String.fromCharCode(buf << (8 - bitcount)); + } + var retbuf = new Uint8Array(retstr.length); + for (i in retstr) { + retbuf[i] = retstr.charCodeAt(i); + } + return retbuf; +} +function do_hmac(key, message, algo) { + var a = algos[algo]; + // RFC2104 + if (key.length > a.blksz) { + key = a.sha(key); + } + var istr = new Uint8Array(a.blksz + message.length); + var ostr = new Uint8Array(a.blksz + a.retsz); + for (var i = 0; i < a.blksz; ++i) { + var c = (i < key.length) ? key[i] : 0; + istr[i] = c ^ 0x36; + ostr[i] = c ^ 0x5C; + } + istr.set(message, a.blksz); + ostr.set(a.sha(istr), a.blksz); + var ret = a.sha(ostr); + // RFC4226 dynamic truncation + var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4); + return v.getUint32(0) & 0x7FFFFFFF; +} +function hotp(d, token, dohmac) { + var tick; + if (token.period > 0) { + // RFC6238 - timed + var seconds = Math.floor(d.getTime() / 1000); + tick = Math.floor(seconds / token.period); + } else { + // RFC4226 - counter + tick = -token.period; + } + var msg = new Uint8Array(8); + var v = new DataView(msg.buffer); + v.setUint32(0, tick >> 16 >> 16); + v.setUint32(4, tick & 0xFFFFFFFF); + 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; + } + } + return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)}; +} + +var state = { + listy: 0, + prevcur:0, + curtoken:-1, + nextTime:0, + otp:"", + rem:0, + hide:0 +}; + +function drawToken(id, r) { + var x1 = r.x; + var y1 = r.y; + var x2 = r.x + r.w - 1; + var y2 = r.y + r.h - 1; + 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) { + // current token + g.setColor(g.theme.fgH); + g.setBgColor(g.theme.bgH); + g.setFont("Vector", 16); + // center just below top line + g.setFontAlign(0, -1, 0); + adj = y1; + } else { + g.setColor(g.theme.fg); + g.setBgColor(g.theme.bg); + g.setFont("Vector", 30); + // center in box + g.setFontAlign(0, 0, 0); + adj = (y1 + y2) / 2; + } + g.clearRect(x1, y1, x2, y2); + 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 + let xr = Math.floor(Bangle.appRect.w * state.rem / tokens[id].period); + g.fillRect(x1, y2 - 4, xr, y2 - 1); + adj = 0; + } else { + // counter - draw triangle as swipe hint + let yc = (y1 + y2) / 2; + g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]); + adj = 10; + } + // digits just below label + sz = 30; + do { + g.setFont("Vector", sz--); + } while (g.stringWidth(state.otp) > (r.w - adj)); + g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + 16, false); + } + // shaded lines top and bottom + g.setColor(0.5, 0.5, 0.5); + g.drawLine(x1, y1, x2, y1); + g.drawLine(x1, y2, x2, y2); + g.setClipRect(0, 0, g.getWidth(), g.getHeight()); +} + +function draw() { + var timerfn = exitApp; + var timerdly = 10000; + 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 + if (state.curtoken != -1) { + state.prevcur = state.curtoken; + state.curtoken = -1; + } + state.nextTime = 0; + } else { + // time to generate a new token + var r = hotp(d, t, state.otp != ""); + state.nextTime = r.next; + state.otp = r.hotp; + if (t.period <= 0) { + state.hide = 1; + } + state.hide--; + } + } + state.rem = Math.max(0, Math.floor((state.nextTime - d.getTime()) / 1000)); + } + if (tokens.length > 0) { + var drewcur = false; + var id = Math.floor(state.listy / tokenentryheight); + var y = id * tokenentryheight + Bangle.appRect.y - state.listy; + while (id < tokens.length && y < Bangle.appRect.y2) { + drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenentryheight}); + if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) { + drewcur = true; + } + id += 1; + y += tokenentryheight; + } + if (drewcur) { + // the current token has been drawn - schedule a redraw + if (tokens[state.curtoken].period > 0) { + timerdly = (state.otp == calculating) ? 1 : 1000; // timed + } else { + timerdly = state.nexttime - d.getTime(); // counter + } + timerfn = draw; + if (tokens[state.curtoken].period <= 0) { + state.hide = 0; + } + } else { + // de-select the current token if it is scrolled out of view + if (state.curtoken != -1) { + state.prevcur = state.curtoken; + state.curtoken = -1; + } + state.nexttime = 0; + } + } else { + g.setFont("Vector", 30); + g.setFontAlign(0, 0, 0); + g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); + } + if (state.drawtimer) { + clearTimeout(state.drawtimer); + } + state.drawtimer = setTimeout(timerfn, timerdly); +} + +function onTouch(zone, e) { + if (e) { + var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenentryheight); + if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) { + id = -1; + } + if (state.curtoken != id) { + if (id != -1) { + var y = id * tokenentryheight - state.listy; + if (y < 0) { + state.listy += y; + y = 0; + } + y += tokenentryheight; + if (y > Bangle.appRect.h) { + state.listy += (y - Bangle.appRect.h); + } + state.otp = ""; + } + state.nextTime = 0; + state.curtoken = id; + state.hide = 2; + } + } + draw(); +} + +function onDrag(e) { + if (e.x > g.getWidth() || e.y > g.getHeight()) return; + if (e.dx == 0 && e.dy == 0) return; + var newy = Math.min(state.listy - e.dy, tokens.length * tokenentryheight - Bangle.appRect.h); + state.listy = Math.max(0, newy); + draw(); +} + +function onSwipe(e) { + if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) { + tokens[state.curtoken].period--; + let newsettings={tokens:tokens,misc:settings.misc}; + require("Storage").writeJSON("authentiwatch.json", newsettings); + state.nextTime = 0; + state.otp = ""; + state.hide = 2; + } + draw(); +} + +function bangle1Btn(e) { + if (tokens.length > 0) { + if (state.curtoken == -1) { + state.curtoken = state.prevcur; + } else { + switch (e) { + case -1: state.curtoken--; break; + case 1: state.curtoken++; break; + } + } + state.curtoken = Math.max(state.curtoken, 0); + state.curtoken = Math.min(state.curtoken, tokens.length - 1); + var fakee = {}; + fakee.y = state.curtoken * tokenentryheight - state.listy + Bangle.appRect.y; + state.curtoken = -1; + state.nextTime = 0; + onTouch(0, fakee); + } else { + draw(); // resets idle timer + } +} + +function exitApp() { + Bangle.showLauncher(); +} + +Bangle.on('touch', onTouch); +Bangle.on('drag' , onDrag ); +Bangle.on('swipe', onSwipe); +if (typeof BTN2 == 'number') { + setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising", debounce:50, repeat:true}); + setWatch(function(){exitApp(); }, BTN2, {edge:"rising", debounce:50, repeat:true}); + setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising", debounce:50, repeat:true}); +} +Bangle.loadWidgets(); + +// Clear the screen once, at startup +g.clear(); +draw(); +Bangle.drawWidgets(); diff --git a/apps/authentiwatch/app.png b/apps/authentiwatch/app.png new file mode 100644 index 000000000..8775d3e40 Binary files /dev/null and b/apps/authentiwatch/app.png differ diff --git a/apps/authentiwatch/interface.html b/apps/authentiwatch/interface.html new file mode 100644 index 000000000..26533b17b --- /dev/null +++ b/apps/authentiwatch/interface.html @@ -0,0 +1,375 @@ + + + + + + + + + + + + + + + + + +

Authentiwatch

+
+

No watch comms.

+
+
+ + + +
+
+
+
+
+
+ +
+
+ + + + diff --git a/apps/authentiwatch/qr_packed.js b/apps/authentiwatch/qr_packed.js new file mode 100644 index 000000000..28b1bddd0 --- /dev/null +++ b/apps/authentiwatch/qr_packed.js @@ -0,0 +1,107 @@ +/* Packed with Google Closure +* +* Ported to JavaScript by Lazar Laszlo 2011 +* lazarsoft@gmail.com, www.lazarsoft.info +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +var qrcode=function(){"use strict";function a(h,b){this.count=h;this.dataCodewords=b;this.__defineGetter__("Count",function(){return this.count});this.__defineGetter__("DataCodewords",function(){return this.dataCodewords})}function f(h,b,e){this.ecCodewordsPerBlock=h;this.ecBlocks=e?[b,e]:Array(b);this.__defineGetter__("ECCodewordsPerBlock",function(){return this.ecCodewordsPerBlock});this.__defineGetter__("TotalECCodewords",function(){return this.ecCodewordsPerBlock*this.NumBlocks});this.__defineGetter__("NumBlocks", +function(){for(var d=0,c=0;cMath.abs(d-b);if(h){var a=b;b=e;e=a;a=d;d=c;c=a}for(var m=Math.abs(d-b),f=Math.abs(c-e),q=-m>>1,k=ed?(h=b/(b-d),d=0):d>=g.width&&(h=(g.width-1-b)/(d-b),d=g.width-1);c=Math.floor(e-(c-e)*h);h=1;0>c?(h=e/(e-c),c=0):c>=g.height&&(h=(g.height-1-e)/(c-e),c=g.height-1);d=Math.floor(b+(d-b)*h);a+=this.sizeOfBlackWhiteBlackRun(b,e,d,c);return a-1};this.calculateModuleSizeOneWay=function(b,e){var d=this.sizeOfBlackWhiteBlackRunBothWays(Math.floor(b.X), +Math.floor(b.Y),Math.floor(e.X),Math.floor(e.Y)),c=this.sizeOfBlackWhiteBlackRunBothWays(Math.floor(e.X),Math.floor(e.Y),Math.floor(b.X),Math.floor(b.Y));return isNaN(d)?c/7:isNaN(c)?d/7:(d+c)/14};this.calculateModuleSize=function(b,e,d){return(this.calculateModuleSizeOneWay(b,e)+this.calculateModuleSizeOneWay(b,d))/2};this.distance=function(b,e){var d=b.X-e.X,c=b.Y-e.Y;return Math.sqrt(d*d+c*c)};this.computeDimension=function(b,e,d,c){e=Math.round(this.distance(b,e)/c);b=Math.round(this.distance(b, +d)/c);b=(e+b>>1)+7;switch(b&3){case 0:b++;break;case 2:b--;break;case 3:throw"Error";}return b};this.findAlignmentInRegion=function(b,e,d,c){c=Math.floor(c*b);var h=Math.max(0,e-c);e=Math.min(g.width-1,e+c);if(e-h<3*b)throw"Error";var a=Math.max(0,d-c);return(new R(this.image,h,a,e-h,Math.min(g.height-1,d+c)-a,b,this.resultPointCallback)).find()};this.createTransform=function(b,e,d,c,h){h-=3.5;var a;if(null!=c){var p=c.X;c=c.Y;var f=a=h-3}else p=e.X-b.X+d.X,c=e.Y-b.Y+d.Y,f=a=h;return z.quadrilateralToQuadrilateral(3.5, +3.5,h,3.5,f,a,3.5,h,b.X,b.Y,e.X,e.Y,p,c,d.X,d.Y)};this.sampleGrid=function(b,e,d){return F.sampleGrid3(b,d,e)};this.processFinderPatternInfo=function(b){var e=b.TopLeft,d=b.TopRight;b=b.BottomLeft;var c=this.calculateModuleSize(e,d,b);if(1>c)throw"Error";var h=this.computeDimension(e,d,b,c),a=k.getProvisionalVersionForDimension(h),m=a.DimensionForVersion-7,f=null;if(0>3&3);this.dataMask=h&7;this.__defineGetter__("ErrorCorrectionLevel",function(){return this.errorCorrectionLevel});this.__defineGetter__("DataMask",function(){return this.dataMask});this.GetHashCode=function(){return this.errorCorrectionLevel.ordinal()<< +3|this.dataMask};this.Equals=function(b){return this.errorCorrectionLevel==b.errorCorrectionLevel&&this.dataMask==b.dataMask}}function C(h,b,e){this.ordinal_Renamed_Field=h;this.bits=b;this.name=e;this.__defineGetter__("Bits",function(){return this.bits});this.__defineGetter__("Name",function(){return this.name});this.ordinal=function(){return this.ordinal_Renamed_Field}}function I(h,b){b||(b=h);if(1>h||1>b)throw"Both dimensions must be greater than 0";this.width=h;this.height=b;var e=h>>5;0!=(h& +31)&&e++;this.rowSize=e;this.bits=Array(e*b);for(e=0;e>5)],d&31)&1)};this.set_Renamed=function(d,c){this.bits[c*this.rowSize+ +(d>>5)]|=1<<(d&31)};this.flip=function(d,c){this.bits[c*this.rowSize+(d>>5)]^=1<<(d&31)};this.clear=function(){for(var d=this.bits.length,c=0;cc||0>d)throw"Left and top must be nonnegative";if(1>b||1>e)throw"Height and width must be at least 1";e=d+e;b=c+b;if(b>this.height||e>this.width)throw"The region must fit inside the matrix";for(;c>5)]|=1<<(a&31)}}function G(a,b){this.numDataCodewords= +a;this.codewords=b;this.__defineGetter__("NumDataCodewords",function(){return this.numDataCodewords});this.__defineGetter__("Codewords",function(){return this.codewords})}function T(a){var b=a.Dimension;if(21>b||1!=(b&3))throw"Error BitMatrixParser";this.bitMatrix=a;this.parsedFormatInfo=this.parsedVersion=null;this.copyBit=function(e,d,c){return this.bitMatrix.get_Renamed(e,d)?c<<1|1:c<<1};this.readFormatInformation=function(){if(null!=this.parsedFormatInfo)return this.parsedFormatInfo;for(var e= +0,d=0;6>d;d++)e=this.copyBit(d,8,e);e=this.copyBit(7,8,e);e=this.copyBit(8,8,e);e=this.copyBit(8,7,e);for(d=5;0<=d;d--)e=this.copyBit(8,d,e);this.parsedFormatInfo=r.decodeFormatInformation(e);if(null!=this.parsedFormatInfo)return this.parsedFormatInfo;for(var c=this.bitMatrix.Dimension,e=0,b=c-8,d=c-1;d>=b;d--)e=this.copyBit(d,8,e);for(d=c-7;d>2;if(6>=d)return k.getVersionForNumber(d);for(var d=0,c=e-11,b=5;0<=b;b--)for(var a=e-9;a>=c;a--)d=this.copyBit(a,b,d);this.parsedVersion=k.decodeVersionInformation(d);if(null!=this.parsedVersion&&this.parsedVersion.DimensionForVersion==e)return this.parsedVersion;d=0;for(a=5;0<=a;a--)for(b=e-9;b>=c;b--)d=this.copyBit(a,b,d);this.parsedVersion=k.decodeVersionInformation(d);if(null!= +this.parsedVersion&&this.parsedVersion.DimensionForVersion==e)return this.parsedVersion;throw"Error readVersion";};this.readCodewords=function(){var b=this.readFormatInformation(),d=this.readVersion(),c=H.forReference(b.DataMask),b=this.bitMatrix.Dimension;c.unmaskBitMatrix(this.bitMatrix,b);for(var c=d.buildFunctionPattern(),a=!0,h=Array(d.TotalCodewords),m=0,f=0,g=0,k=b-1;0t;t++)c.get_Renamed(k-t,v)||(g++,f<<=1,this.bitMatrix.get_Renamed(k- +t,v)&&(f|=1),8==g&&(h[m++]=f,f=g=0));a^=1}if(m!=d.TotalCodewords)throw"Error readCodewords";return h}}function w(a,b){if(null==b||0==b.length)throw"System.ArgumentException";this.field=a;var e=b.length;if(1c.length){var b=d,d=c;c=b}for(var b=Array(c.length),e=c.length-d.length,h=0;hc)throw"System.ArgumentException";if(0==d)return this.field.Zero;for(var b=this.coefficients.length,e=Array(b+c),a=0;a=c.Degree&&!b.Zero;)var a=b.Degree-c.Degree, +h=this.field.multiply(b.getCoefficient(b.Degree),e),f=c.multiplyByMonomial(a,h),a=this.field.buildMonomial(a,h),d=d.addOrSubtract(a),b=b.addOrSubtract(f);return[d,b]}}function n(a){this.expTable=Array(256);this.logTable=Array(256);for(var b=1,e=0;256>e;e++)this.expTable[e]=b,b<<=1,256<=b&&(b^=a);for(e=0;255>e;e++)this.logTable[this.expTable[e]]=e;a=Array(1);a[0]=0;this.zero=new w(this,Array(a));a=Array(1);a[0]=1;this.one=new w(this,Array(a));this.__defineGetter__("Zero",function(){return this.zero}); +this.__defineGetter__("One",function(){return this.one});this.buildMonomial=function(d,c){if(0>d)throw"System.ArgumentException";if(0==c)return this.zero;for(var b=Array(d+1),e=0;e>b:(a>>b)+(2<<~b)}function U(a,b,e){this.x=a;this.y=b;this.count=1;this.estimatedModuleSize=e;this.__defineGetter__("EstimatedModuleSize",function(){return this.estimatedModuleSize});this.__defineGetter__("Count",function(){return this.count});this.__defineGetter__("X",function(){return this.x});this.__defineGetter__("Y",function(){return this.y});this.incrementCount=function(){this.count++}; +this.aboutEquals=function(d,c,b){return Math.abs(c-this.y)<=d&&Math.abs(b-this.x)<=d?(d=Math.abs(d-this.estimatedModuleSize),1>=d||1>=d/this.estimatedModuleSize):!1}}function V(a){this.bottomLeft=a[0];this.topLeft=a[1];this.topRight=a[2];this.__defineGetter__("BottomLeft",function(){return this.bottomLeft});this.__defineGetter__("TopLeft",function(){return this.topLeft});this.__defineGetter__("TopRight",function(){return this.topRight})}function S(){this.image=null;this.possibleCenters=[];this.hasSkipped= +!1;this.crossCheckStateCount=[0,0,0,0,0];this.resultPointCallback=null;this.__defineGetter__("CrossCheckStateCount",function(){this.crossCheckStateCount[0]=0;this.crossCheckStateCount[1]=0;this.crossCheckStateCount[2]=0;this.crossCheckStateCount[3]=0;this.crossCheckStateCount[4]=0;return this.crossCheckStateCount});this.foundPatternCross=function(a){for(var b=0,e=0;5>e;e++){var d=a[e];if(0==d)return!1;b+=d}if(7>b)return!1;b=Math.floor((b<m)return NaN;for(;0<=m&&!c[b+m*g.width]&&l[1]<=e;)l[1]++,m--;if(0>m||l[1]>e)return NaN;for(;0<=m&&c[b+m*g.width]&&l[0]<=e;)l[0]++,m--;if(l[0]>e)return NaN;for(m=a+1;m=e)return NaN;for(;m=e||5*Math.abs(l[0]+l[1]+l[2]+l[3]+l[4]-d)>=2*d?NaN:this.foundPatternCross(l)?this.centerFromEnd(l,m):NaN};this.crossCheckHorizontal=function(a,b,e,d){for(var c=this.image,h=g.width,l=this.CrossCheckStateCount,m=a;0<=m&&c[m+b*g.width];)l[2]++,m--;if(0>m)return NaN;for(;0<=m&&!c[m+b*g.width]&&l[1]<=e;)l[1]++,m--;if(0>m||l[1]>e)return NaN;for(;0<=m&&c[m+b*g.width]&&l[0]<= +e;)l[0]++,m--;if(l[0]>e)return NaN;for(m=a+1;m=e)return NaN;for(;m=e||5*Math.abs(l[0]+l[1]+l[2]+l[3]+l[4]-d)>=d?NaN:this.foundPatternCross(l)?this.centerFromEnd(l,m):NaN};this.handlePossibleCenter=function(a,b,e){var d=a[0]+a[1]+a[2]+a[3]+a[4];e=this.centerFromEnd(a,e);b=this.crossCheckVertical(b,Math.floor(e),a[2],d);if(!isNaN(b)&&(e=this.crossCheckHorizontal(Math.floor(e), +Math.floor(b),a[2],d),!isNaN(e))){a=d/7;for(var d=!1,c=this.possibleCenters.length,h=0;ha)throw"Couldn't find enough finder patterns (found "+a+")";if(3a&&this.possibleCenters.splice(d,1)}3d.count?-1:c.count=a)return 0;for(var b=null,e=0;e=K)if(null==b)b=d;else return this.hasSkipped=!0,Math.floor((Math.abs(b.X-d.X)-Math.abs(b.Y-d.Y))/2)}return 0};this.haveMultiplyConfirmedCenters=function(){for(var a,b=0,e=0,d=this.possibleCenters.length,c=0;c=K&&(b++,e+=a.EstimatedModuleSize); +if(3>b)return!1;for(var b=e/d,p=0,c=0;cl[2]&&(m+=b-l[2]-c,f=d-1));else{do f++;while(f=d||1>=d/this.estimatedModuleSize):!1}}function R(a,b,e,d,c,f,l){this.image=a;this.possibleCenters= +[];this.startX=b;this.startY=e;this.width=d;this.height=c;this.moduleSize=f;this.crossCheckStateCount=[0,0,0];this.resultPointCallback=l;this.centerFromEnd=function(c,d){return d-c[2]-c[1]/2};this.foundPatternCross=function(c){for(var d=this.moduleSize,b=d/2,a=0;3>a;a++)if(Math.abs(d-c[a])>=b)return!1;return!0};this.crossCheckVertical=function(c,d,b,a){var e=this.image,h=g.height,f=this.crossCheckStateCount;f[0]=0;f[1]=0;f[2]=0;for(var l=c;0<=l&&e[d+l*g.width]&&f[1]<=b;)f[1]++,l--;if(0>l||f[1]>b)return NaN; +for(;0<=l&&!e[d+l*g.width]&&f[0]<=b;)f[0]++,l--;if(f[0]>b)return NaN;for(l=c+1;lb)return NaN;for(;lb||5*Math.abs(f[0]+f[1]+f[2]-a)>=2*a?NaN:this.foundPatternCross(f)?this.centerFromEnd(f,l):NaN};this.handlePossibleCenter=function(c,d,b){var a=c[0]+c[1]+c[2];b=this.centerFromEnd(c,b);d=this.crossCheckVertical(d,Math.floor(b),2*c[1],a);if(!isNaN(d)){c=(c[0]+c[1]+c[2])/3;for(var a=this.possibleCenters.length, +e=0;e>1),p=[0,0,0],k=0;k>1:-(k+1>>1));p[0]=0;p[1]=0;p[2]=0;for(var A=b;A=b?this.dataLengthMode=0:10<=b&&26>=b?this.dataLengthMode=1:27<= +b&&40>=b&&(this.dataLengthMode=2);this.getNextBits=function(b){var c,d;if(b>this.bitPointer-b+1;this.bitPointer-=b;return d}if(b>8-(b-(this.bitPointer+1));this.bitPointer-=b%8;0>this.bitPointer&&(this.bitPointer= +8+this.bitPointer);return d}if(b>8-(b-(this.bitPointer+1+8));this.bitPointer-=(b-8)%8;0>this.bitPointer&&(this.bitPointer=8+this.bitPointer);return c+e+d}return 0}; +this.NextMode=function(){return this.blockPointer>this.blocks.length-this.numErrorCorrectionCode-2?0:this.getNextBits(4)};this.getDataLength=function(b){for(var c=0;1!=b>>c;)c++;return this.getNextBits(g.sizeOfDataLengthInfo[this.dataLengthMode][c])};this.getRomanAndFigureString=function(b){var c="",d="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".split("");do if(1c&&(d+="0"),10>c&&(d+="0"),b-=3):2==b?(c=this.getNextBits(7),10>c&&(d+="0"),b-=2):1==b&&(c=this.getNextBits(4),--b),d+=c;while(0=d+33088?d+33088:d+49472);b--}while(0< +b);return c};this.parseECIValue=function(){var b=0,c=this.getNextBits(8);0==(c&128)&&(b=c&127);128==(c&192)&&(b=this.getNextBits(8),b|=(c&63)<<8);192==(c&224)&&(b=this.getNextBits(8),b|=(c&31)<<16);return b};this.__defineGetter__("DataByte",function(){var b=[];do{var c=this.NextMode();if(0==c)if(0a)throw"Invalid data length: "+a;switch(c){case 1:c=this.getFigureString(a);for(var a=Array(c.length),e=0;ed||d>c||-1>e||e>h)throw"Error.checkAndNudgePoints ";f=!1;-1==d?(b[m]=0,f=!0):d==c&&(b[m]=c-1,f=!0);-1==e?(b[m+1]=0,f=!0):e==h&&(b[m+1]=h-1,f=!0)}f=!0;for(m=b.length-2;0<=m&&f;m-=2){d=Math.floor(b[m]);e=Math.floor(b[m+1]);if(-1>d||d>c||-1>e||e>h)throw"Error.checkAndNudgePoints ";f=!1;-1==d?(b[m]=0,f=!0):d==c&&(b[m]=c-1,f=!0);-1==e?(b[m+1]=0,f=!0):e==h&&(b[m+1]=h-1,f=!0)}},sampleGrid3:function(a,b,e){for(var d=new I(b),c=Array(b<<1),h=0;h>1)+.5,c[k+1]=m;e.transformPoints1(c);F.checkAndNudgePoints(a,c);try{for(k=0;k>1,h)}catch(q){throw"Error.checkAndNudgePoints";}}return d},sampleGridx:function(a,b,e,d,c,f,l,g,k,q,n,x,v,t,r,A,u,w){e=z.quadrilateralToQuadrilateral(e,d,c,f,l,g,k,q,n,x,v,t,r,A,u,w);return F.sampleGrid3(a,b,e)}};k.VERSION_DECODE_INFO=[31892,34236,39577,42195,48118,51042,55367,58893,63784,68472,70749,76311,79154, +84390,87683,92361,96236,102084,102881,110507,110734,117786,119615,126325,127568,133589,136944,141498,145311,150283,152622,158308,161089,167017];k.VERSIONS=[new k(1,[],new f(7,new a(1,19)),new f(10,new a(1,16)),new f(13,new a(1,13)),new f(17,new a(1,9))),new k(2,[6,18],new f(10,new a(1,34)),new f(16,new a(1,28)),new f(22,new a(1,22)),new f(28,new a(1,16))),new k(3,[6,22],new f(15,new a(1,55)),new f(26,new a(1,44)),new f(18,new a(2,17)),new f(22,new a(2,13))),new k(4,[6,26],new f(20,new a(1,80)),new f(18, +new a(2,32)),new f(26,new a(2,24)),new f(16,new a(4,9))),new k(5,[6,30],new f(26,new a(1,108)),new f(24,new a(2,43)),new f(18,new a(2,15),new a(2,16)),new f(22,new a(2,11),new a(2,12))),new k(6,[6,34],new f(18,new a(2,68)),new f(16,new a(4,27)),new f(24,new a(4,19)),new f(28,new a(4,15))),new k(7,[6,22,38],new f(20,new a(2,78)),new f(18,new a(4,31)),new f(18,new a(2,14),new a(4,15)),new f(26,new a(4,13),new a(1,14))),new k(8,[6,24,42],new f(24,new a(2,97)),new f(22,new a(2,38),new a(2,39)),new f(22, +new a(4,18),new a(2,19)),new f(26,new a(4,14),new a(2,15))),new k(9,[6,26,46],new f(30,new a(2,116)),new f(22,new a(3,36),new a(2,37)),new f(20,new a(4,16),new a(4,17)),new f(24,new a(4,12),new a(4,13))),new k(10,[6,28,50],new f(18,new a(2,68),new a(2,69)),new f(26,new a(4,43),new a(1,44)),new f(24,new a(6,19),new a(2,20)),new f(28,new a(6,15),new a(2,16))),new k(11,[6,30,54],new f(20,new a(4,81)),new f(30,new a(1,50),new a(4,51)),new f(28,new a(4,22),new a(4,23)),new f(24,new a(3,12),new a(8,13))), +new k(12,[6,32,58],new f(24,new a(2,92),new a(2,93)),new f(22,new a(6,36),new a(2,37)),new f(26,new a(4,20),new a(6,21)),new f(28,new a(7,14),new a(4,15))),new k(13,[6,34,62],new f(26,new a(4,107)),new f(22,new a(8,37),new a(1,38)),new f(24,new a(8,20),new a(4,21)),new f(22,new a(12,11),new a(4,12))),new k(14,[6,26,46,66],new f(30,new a(3,115),new a(1,116)),new f(24,new a(4,40),new a(5,41)),new f(20,new a(11,16),new a(5,17)),new f(24,new a(11,12),new a(5,13))),new k(15,[6,26,48,70],new f(22,new a(5, +87),new a(1,88)),new f(24,new a(5,41),new a(5,42)),new f(30,new a(5,24),new a(7,25)),new f(24,new a(11,12),new a(7,13))),new k(16,[6,26,50,74],new f(24,new a(5,98),new a(1,99)),new f(28,new a(7,45),new a(3,46)),new f(24,new a(15,19),new a(2,20)),new f(30,new a(3,15),new a(13,16))),new k(17,[6,30,54,78],new f(28,new a(1,107),new a(5,108)),new f(28,new a(10,46),new a(1,47)),new f(28,new a(1,22),new a(15,23)),new f(28,new a(2,14),new a(17,15))),new k(18,[6,30,56,82],new f(30,new a(5,120),new a(1,121)), +new f(26,new a(9,43),new a(4,44)),new f(28,new a(17,22),new a(1,23)),new f(28,new a(2,14),new a(19,15))),new k(19,[6,30,58,86],new f(28,new a(3,113),new a(4,114)),new f(26,new a(3,44),new a(11,45)),new f(26,new a(17,21),new a(4,22)),new f(26,new a(9,13),new a(16,14))),new k(20,[6,34,62,90],new f(28,new a(3,107),new a(5,108)),new f(26,new a(3,41),new a(13,42)),new f(30,new a(15,24),new a(5,25)),new f(28,new a(15,15),new a(10,16))),new k(21,[6,28,50,72,94],new f(28,new a(4,116),new a(4,117)),new f(26, +new a(17,42)),new f(28,new a(17,22),new a(6,23)),new f(30,new a(19,16),new a(6,17))),new k(22,[6,26,50,74,98],new f(28,new a(2,111),new a(7,112)),new f(28,new a(17,46)),new f(30,new a(7,24),new a(16,25)),new f(24,new a(34,13))),new k(23,[6,30,54,74,102],new f(30,new a(4,121),new a(5,122)),new f(28,new a(4,47),new a(14,48)),new f(30,new a(11,24),new a(14,25)),new f(30,new a(16,15),new a(14,16))),new k(24,[6,28,54,80,106],new f(30,new a(6,117),new a(4,118)),new f(28,new a(6,45),new a(14,46)),new f(30, +new a(11,24),new a(16,25)),new f(30,new a(30,16),new a(2,17))),new k(25,[6,32,58,84,110],new f(26,new a(8,106),new a(4,107)),new f(28,new a(8,47),new a(13,48)),new f(30,new a(7,24),new a(22,25)),new f(30,new a(22,15),new a(13,16))),new k(26,[6,30,58,86,114],new f(28,new a(10,114),new a(2,115)),new f(28,new a(19,46),new a(4,47)),new f(28,new a(28,22),new a(6,23)),new f(30,new a(33,16),new a(4,17))),new k(27,[6,34,62,90,118],new f(30,new a(8,122),new a(4,123)),new f(28,new a(22,45),new a(3,46)),new f(30, +new a(8,23),new a(26,24)),new f(30,new a(12,15),new a(28,16))),new k(28,[6,26,50,74,98,122],new f(30,new a(3,117),new a(10,118)),new f(28,new a(3,45),new a(23,46)),new f(30,new a(4,24),new a(31,25)),new f(30,new a(11,15),new a(31,16))),new k(29,[6,30,54,78,102,126],new f(30,new a(7,116),new a(7,117)),new f(28,new a(21,45),new a(7,46)),new f(30,new a(1,23),new a(37,24)),new f(30,new a(19,15),new a(26,16))),new k(30,[6,26,52,78,104,130],new f(30,new a(5,115),new a(10,116)),new f(28,new a(19,47),new a(10, +48)),new f(30,new a(15,24),new a(25,25)),new f(30,new a(23,15),new a(25,16))),new k(31,[6,30,56,82,108,134],new f(30,new a(13,115),new a(3,116)),new f(28,new a(2,46),new a(29,47)),new f(30,new a(42,24),new a(1,25)),new f(30,new a(23,15),new a(28,16))),new k(32,[6,34,60,86,112,138],new f(30,new a(17,115)),new f(28,new a(10,46),new a(23,47)),new f(30,new a(10,24),new a(35,25)),new f(30,new a(19,15),new a(35,16))),new k(33,[6,30,58,86,114,142],new f(30,new a(17,115),new a(1,116)),new f(28,new a(14,46), +new a(21,47)),new f(30,new a(29,24),new a(19,25)),new f(30,new a(11,15),new a(46,16))),new k(34,[6,34,62,90,118,146],new f(30,new a(13,115),new a(6,116)),new f(28,new a(14,46),new a(23,47)),new f(30,new a(44,24),new a(7,25)),new f(30,new a(59,16),new a(1,17))),new k(35,[6,30,54,78,102,126,150],new f(30,new a(12,121),new a(7,122)),new f(28,new a(12,47),new a(26,48)),new f(30,new a(39,24),new a(14,25)),new f(30,new a(22,15),new a(41,16))),new k(36,[6,24,50,76,102,128,154],new f(30,new a(6,121),new a(14, +122)),new f(28,new a(6,47),new a(34,48)),new f(30,new a(46,24),new a(10,25)),new f(30,new a(2,15),new a(64,16))),new k(37,[6,28,54,80,106,132,158],new f(30,new a(17,122),new a(4,123)),new f(28,new a(29,46),new a(14,47)),new f(30,new a(49,24),new a(10,25)),new f(30,new a(24,15),new a(46,16))),new k(38,[6,32,58,84,110,136,162],new f(30,new a(4,122),new a(18,123)),new f(28,new a(13,46),new a(32,47)),new f(30,new a(48,24),new a(14,25)),new f(30,new a(42,15),new a(32,16))),new k(39,[6,26,54,82,110,138, +166],new f(30,new a(20,117),new a(4,118)),new f(28,new a(40,47),new a(7,48)),new f(30,new a(43,24),new a(22,25)),new f(30,new a(10,15),new a(67,16))),new k(40,[6,30,58,86,114,142,170],new f(30,new a(19,118),new a(6,119)),new f(28,new a(18,47),new a(31,48)),new f(30,new a(34,24),new a(34,25)),new f(30,new a(20,15),new a(61,16)))];k.getVersionForNumber=function(a){if(1>a||40>2)}catch(b){throw"Error getVersionForNumber";}};k.decodeVersionInformation=function(a){for(var b=4294967295,e=0,d=0;d=b?this.getVersionForNumber(e):null};z.quadrilateralToQuadrilateral=function(a,b,e,d,c,f,g,m,k,q,n,x,v,t,r,u){a=this.quadrilateralToSquare(a,b,e,d,c,f,g,m);return this.squareToQuadrilateral(k, +q,n,x,v,t,r,u).times(a)};z.squareToQuadrilateral=function(a,b,e,d,c,f,g,m){var h=m-f,l=b-d+f-m;if(0==h&&0==l)return new z(e-a,c-e,a,d-b,f-d,b,0,0,1);var p=e-c,k=g-c;c=a-e+c-g;f=d-f;var n=p*h-k*f,h=(c*h-k*l)/n,l=(p*l-c*f)/n;return new z(e-a+h*e,g-a+l*g,a,d-b+h*d,m-b+l*m,b,h,l,1)};z.quadrilateralToSquare=function(a,b,e,d,c,f,g,m){return this.squareToQuadrilateral(a,b,e,d,c,f,g,m).buildAdjoint()};var N=[[21522,0],[20773,1],[24188,2],[23371,3],[17913,4],[16590,5],[20375,6],[19104,7],[30660,8],[29427, +9],[32170,10],[30877,11],[26159,12],[25368,13],[27713,14],[26998,15],[5769,16],[5054,17],[7399,18],[6608,19],[1890,20],[597,21],[3340,22],[2107,23],[13663,24],[12392,25],[16177,26],[14854,27],[9396,28],[8579,29],[11994,30],[11245,31]],B=[0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4];r.numBitsDiffering=function(a,b){a^=b;return B[a&15]+B[u(a,4)&15]+B[u(a,8)&15]+B[u(a,12)&15]+B[u(a,16)&15]+B[u(a,20)&15]+B[u(a,24)&15]+B[u(a,28)&15]};r.decodeFormatInformation=function(a){var b=r.doDecodeFormatInformation(a);return null!= +b?b:r.doDecodeFormatInformation(a^21522)};r.doDecodeFormatInformation=function(a){for(var b=4294967295,e=0,d=0;d=b?new r(e):null};C.forBits=function(a){if(0>a||a>=O.length)throw"ArgumentException";return O[a]};var Y=new C(0,1,"L"),Z=new C(1,0,"M"),aa=new C(2,3,"Q"),ba=new C(3,2,"H"),O=[Z,Y,ba,aa];G.getDataBlocks=function(a,b,e){if(a.length!=b.TotalCodewords)throw"ArgumentException"; +var d=b.getECBlocksForLevel(e);e=0;var c=d.getECBlocks();for(b=0;ba||7h)throw"ReedSolomonException Bad error location";a[h]=n.addOrSubtract(a[h],c[f])}};this.runEuclideanAlgorithm=function(a,e,d){if(a.Degree=Math.floor(d/2);){var k=a,q=b,n=h;a=e;b=f;h=g;if(a.Zero)throw"r_{i-1} was zero";e=k;g=this.field.Zero;f=a.getCoefficient(a.Degree); +for(f=this.field.inverse(f);e.Degree>=a.Degree&&!e.Zero;){var k=e.Degree-a.Degree,r=this.field.multiply(e.getCoefficient(e.Degree),f),g=g.addOrSubtract(this.field.buildMonomial(k,r));e=e.addOrSubtract(a.multiplyByMonomial(k,r))}f=g.multiply1(b).addOrSubtract(q);g=g.multiply1(h).addOrSubtract(n)}d=g.getCoefficient(0);if(0==d)throw"ReedSolomonException sigmaTilde(0) was zero";d=this.field.inverse(d);a=g.multiply2(d);d=e.multiply2(d);return[a,d]};this.findErrorLocations=function(a){var b=a.Degree;if(1== +b)return Array(a.getCoefficient(1));for(var d=Array(b),c=0,f=1;256>f&&cg.maxImgSize&&(f=d.width/d.height,e=Math.sqrt(g.maxImgSize/f),f*=e);a.width=f;a.height=e;b.drawImage(d,0,0,a.width,a.height);g.width=a.width;g.height=a.height;try{g.imagedata= +b.getImageData(0,0,a.width,a.height)}catch(y){g.result=Error("Cross domain image reading not supported in your browser! Save it to your computer then drag and drop the file!");null!=g.callback&&g.callback(g.result);return}try{g.result=g.process(b)}catch(y){console.log(y),g.result=Error("error decoding QR Code")}null!=g.callback&&g.callback(g.result)};d.onerror=function(){null!=g.callback&&g.callback(Error("Failed to load the image"))};d.src=a},isUrl:function(a){return/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/.test(a)}, +decode_url:function(a){var b="";try{b=escape(a)}catch(e){console.log(e),b=a}a="";try{a=decodeURIComponent(b)}catch(e){console.log(e),a=b}return a},decode_utf8:function(a){return g.isUrl(a)?g.decode_url(a):a},process:function(a){var b=(new Date).getTime(),e=g.grayScaleToBitmap(g.grayscale());if(g.debug){for(var d=0;dc;c++){d[c]=Array(4);for(var f=0;4>f;f++)d[c][f]=[0,0]}for(c=0;4>c;c++)for(f= +0;4>f;f++){d[f][c][0]=255;for(var h=0;hd[f][c][1]&&(d[f][c][1]=k)}}a=Array(4);for(b=0;4>b;b++)a[b]=Array(4);for(c=0;4>c;c++)for(f=0;4>f;f++)a[f][c]=Math.floor((d[f][c][0]+d[f][c][1])/2);return a},grayScaleToBitmap:function(a){for(var b=g.getMiddleBrightnessPerArea(a),e=b.length,d=Math.floor(g.width/e),c=Math.floor(g.height/e),f=new ArrayBuffer(g.width*g.height),f=new Uint8Array(f),h=0;h=e&&d>=c?(d=a[0],e=a[1],c=a[2]):c>=d&&c>=e?(d=a[1], +e=a[0],c=a[2]):(d=a[2],e=a[0],c=a[1]);if(0>function(a,b,c){var d=b.x;b=b.y;return(c.x-d)*(a.y-b)-(c.y-b)*(a.x-d)}(e,d,c))var f=e,e=c,c=f;a[0]=e;a[1]=d;a[2]=c};return g}(); diff --git a/apps/authentiwatch/screenshot.png b/apps/authentiwatch/screenshot.png new file mode 100644 index 000000000..2a7bcbd9a Binary files /dev/null and b/apps/authentiwatch/screenshot.png differ 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 f916cd6cf..1e54f489c 100644 --- a/apps/binwatch/ChangeLog +++ b/apps/binwatch/ChangeLog @@ -1,2 +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/README.md b/apps/binwatch/README.md index d9da4968b..52e868e21 100644 --- a/apps/binwatch/README.md +++ b/apps/binwatch/README.md @@ -1,10 +1,47 @@ # TheBinWatch Binary watch to train Your brain -Inspired by the 80's LCD wrist watch from RALtec +Inspired by the LCD wrist watch from TecRAL from 1989 + +![](screenshot.png) +![](screenshot2.png) + ## Usage - swipe to left or right to change displayed text (date, time, ...) - currently only available for BangeJs2 - Widgets will not be shown -- If bluetooth connection is not established an icon will show up \ No newline at end of file +- If bluetooth connection is not established an icon will show up + +## How it works +Binary means that every digit can represent 2 states: 0 or 1, displayed by a black bar. + +The principle is the same like in out well known and daily used decimal system with values from 0 to 9: + +We start from the most right position with the least significant bit (binary digit) which can have the value 0 or 1 +The 2nd bit from the right can have the value 0 or 2 (sum of all bits to the right set to 1 plus 1). +This principle is valid for all the remaining bits. + +Mathematically spoken: the value of a digit is the base number of the system (10 for decimal or 2 for binary) +to the power of the position (from the right, starting with 0). +That means in numbers: 2^5 = 32, 2^4 = 16, 2^3 = 8, 2^2 = 4, 2^1 = 2, 2^0 = 1 + +The upper row represents the hours with 4 bit (2^4 = 16 possible values in total, 12 are used: 1 to 12), + the 2nd row represents the minutes with 6 bit (2^6 = 64 possible values in total, 60 are used: 0 to 59). +Same holds for the thrid row: 0-59 seconds + +To read the values of a row we summ up the vaules of set bits (black bars). +E.g. the picture above, 3rd row (seconds): +101001 +is 1 * 32 + 0 * 16 + 1 * 8 + 0 * 4 + 0 * 2 + 1 * 1 +is (only the '1' bit): 32 + 8 + 1 = 41 + +for the minutes we do the same: 32 + 1 = 33 +and the hours: 8 + 2 = 10 + +So the time is 10:33:41 (that's all) + +## TRAIN YOUR BRAIN + +Remark: more infos about the original watch including manual can be found here: +https://timeartpiece.com/watches/tech-ral-binary diff --git a/apps/binwatch/app-icon.js b/apps/binwatch/app-icon.js index 31d0a4cca..10d7e84e8 100644 --- a/apps/binwatch/app-icon.js +++ b/apps/binwatch/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("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")) +require("heatshrink").decompress(atob("mEwwcCgEBkmSpICKCwQRRhMn/4AK+VACIU4A4PAz+27dt20ECI1IgEDCIOT+wRB2EkCIX+BwMCpE/8f+gmSvwRB2Mkz///v/5IRBpwRHwIRC5PzCIMSCIXwMQNP7dshMkyf/p+G/MgiV+CIPxCJFM8gRByf+CIIvBRIP7sCMCv/h8//C4P+g6ABCIdiCIVP/M///kFIPAj6iLCIYAOCPH4ibUC2zABdgW/8ARFUgILB2/8fwf/kB3BPobUD3/kz4pCTwMDCIrCBCIWTCINv/IREfAVJDoYpCv/JkmAv4RCYQYRM+ARCn4vCHYX+bQOQh4RBfAYRJyUBCI3/F4IFB/4RGdP4RHwDmC7/gmzaC//tbQWBR4UbfAWQgzIDfwVsR4QRCfAIRM/0DCIWSgDaDz4RBsDXDCIIdByVAfAb+CCIf/4AREjYRFgZ9D/D4DpEDfAT+Cj4REhoRJ7ARE/8PfAVJgbmDp/YWZHgv6zIkkSBYWB44sB/4CB/AREkESp4EBx4RBx0/CIPACAf5kECCIQAHPQIAB5MAgVJEYs4AwIjECIMACI0ACIv+pARCn5rDvwFDGoQRDhILDABHyoARBgKeCARQQBCKIA==")) \ No newline at end of file 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/binwatch/app.png b/apps/binwatch/app.png index ebab4670e..e1a0c88ff 100644 Binary files a/apps/binwatch/app.png and b/apps/binwatch/app.png differ diff --git a/apps/binwatch/screenshot.png b/apps/binwatch/screenshot.png new file mode 100644 index 000000000..ebab4670e Binary files /dev/null and b/apps/binwatch/screenshot.png differ diff --git a/apps/binwatch/screenshot2.png b/apps/binwatch/screenshot2.png new file mode 100644 index 000000000..fa171d253 Binary files /dev/null and b/apps/binwatch/screenshot2.png differ 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/ChangeLog b/apps/chargeanim/ChangeLog index 5560f00bc..a7262b0c9 100644 --- a/apps/chargeanim/ChangeLog +++ b/apps/chargeanim/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Bangle.js 2 compatibility diff --git a/apps/chargeanim/app.js b/apps/chargeanim/app.js index c2702337a..68d0cdff5 100644 --- a/apps/chargeanim/app.js +++ b/apps/chargeanim/app.js @@ -1,8 +1,12 @@ +g.setBgColor(0, 0, 0); g.clear().flip(); -var imgbat = require("heatshrink").decompress(atob("nlWhH+AH4A/AH4AHwoAQHXQ8pHf47rF6YAXHXQ8OHVo8NHf47/Hf47/Hf47/Hf47/Hf47/Hf47r1I766Y756Z351I766ayTHco6BHfCxBHfI6CdyY7jHQQ73WIayUHcQ6DHew6EHeqxEdyo7gOwo70HQqyVHbyxFHeo6GHeY6Hdyo7cWI47zHQ6yWHbY6IHeKxIABa9MHbI6TQJo7YHUI7YWMKzbQKQYOHdYYPHcK9IWJw7sDKA7hHTA7pWKA7qDKQ7gdwwaTHcyxSHcR2ZHcwZUHcqxUHcLuEHSo7kHSw7gWLI7kHS47iHTA7fdwKxYHcQ6ZHb46bO8A76ADg7/Hf47/Hf47/Hf47/Hf47/Hf47/HbY8uHRg8tHRwA/AH4AsA==")); -var imgbubble = require("heatshrink").decompress(atob("ikQhH+AAc0AAgKEAAwRFCpgMDnVerwULCIuCCYoUGCQQQBnQ9MA4Q3GChI5DEpATIJYISKCY46LCYwANCa4UObJ7INeCoSOCpAOI")); +var imgbat = require("heatshrink").decompress(atob("nFYhBC/AH4A/AGUeACA22HEo3/G8YrTAC422HBQ2tHBI3/G/43/G/43/G/43/G/43/G/43/G+fTG+vSN+w326Q31GwI3/G9g2WG742CG/43rGwY3yGwg33RKo3bNzQ3bGwo3/G9A2GG942dG/43QGw43uGxA34IKw3VGyY3iG0I3pb8pBRG+wYPG8wYQG/42uG8oZSG/43bDKY3iDKg3cNzI3iRKo3gGyo3/G7A2WG7g2aG/43WGzA3dGzI3/G6fTGzRvcG/43/G/43/G/43/G/43/G/43/G/437HFw2IHFo2KAH4A/AH4Aa")); +var imgbubble = require("heatshrink").decompress(atob("i0UhAebgoAFCaYXNBocjAAIWNCYoVHCw4UFIZwqELJQWFKZQVOChYVzABwVaCx7wKCqIWNCg4WMChIXJCZgAnA==")); - var W=240,H=240; +var W=g.getWidth(),H=g.getHeight(); +var b2v = (W != 240)?-1:1; +var b2rot = (W != 240)?Math.PI:0; +var b2scale = W/240.0; var bubbles = []; for (var i=0;i<10;i++) { bubbles.push({y:Math.random()*H,ly:0,x:(0.5+(i<5?i:i+8))*W/18,v:0.6+Math.random(),s:0.5+Math.random()}); @@ -12,12 +16,16 @@ function anim() { /* we don't use any kind of buffering here. Just draw one image at a time (image contains a background) too, and there is minimal flicker. */ - var mx = 120, my = 120; + var mx = W/2.0, my = H/2.0; bubbles.forEach(f=>{ - f.y-=f.v;if (f.y<-24) f.y=H+8; - g.drawImage(imgbubble,f.y,f.x,{scale:f.s}); + f.y-=f.v * b2v; + if (f.y<-24) + f.y=H+8; + else if (f.y > (H+8)) + f.y=0; + g.drawImage(imgbubble,f.y,f.x,{scale:f.s * b2scale, rotate:b2rot}); }); - g.drawImage(imgbat, mx,my,{rotate:Math.sin(getTime()*2)*0.5-Math.PI/2}); + g.drawImage(imgbat, mx,my,{scale:b2scale, rotate:Math.sin(getTime()*2)*0.5-Math.PI/2 + b2rot}); g.flip(); } 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/chronowid/ChangeLog b/apps/chronowid/ChangeLog index e173467a1..ded543397 100644 --- a/apps/chronowid/ChangeLog +++ b/apps/chronowid/ChangeLog @@ -1,3 +1,5 @@ 0.01: New widget and app! 0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme) -0.03: Display only minutes:seconds when less than 1 hour left \ No newline at end of file +0.03: Display only minutes:seconds when less than 1 hour left +0.04: Change to 7 segment font, move to top widget bar + Better auto-update behaviour, less RAM used diff --git a/apps/chronowid/README.md b/apps/chronowid/README.md index ec1d5dd46..6e0aba681 100644 --- a/apps/chronowid/README.md +++ b/apps/chronowid/README.md @@ -5,14 +5,13 @@ The advantage is, that you can still see your normal watchface and other widgets The widget is always active, but only shown when the timer is on. Hours, minutes, seconds and timer status can be set with an app. -When there is less than one seconds left on the timer it buzzes. +When there is less than one second left on the timer it buzzes. The widget has been tested on Bangle 1 and Bangle 2 ## Screenshots -![](chrono_with_wave.jpg) -![](chrono_with_pastel.jpg) +![](screenshot.png) ## Features @@ -28,15 +27,15 @@ There are no settings section in the settings app, timer can be set using an app * Hours: Set the hours for the timer * Minutes: Set the minutes for the timer * Seconds: Set the seconds for the timer -* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app to load the widget which starts the timer. The widget is always there, but only visible when timer is on. +* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app to load the widget which starts the timer. The widget is always there, but only visible when timer is on. ## Releases -* Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/chronowid (https://banglejs.com/apps/) +* Official app loader: https://github.com/espruino/BangleApps/tree/master/apps/chronowid (https://banglejs.com/apps/) * Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/chronowid (https://purple-tentacle.github.io/BangleApps/index.html#) * Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid ## Requests -If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/ \ No newline at end of file +If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/ diff --git a/apps/chronowid/app.js b/apps/chronowid/app.js index 0cacdee23..f38105e34 100644 --- a/apps/chronowid/app.js +++ b/apps/chronowid/app.js @@ -3,7 +3,6 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); const storage = require('Storage'); -const boolFormat = v => v ? "On" : "Off"; let settingsChronowid; function updateSettings() { @@ -12,6 +11,7 @@ function updateSettings() { now.getHours() + settingsChronowid.hours, now.getMinutes() + settingsChronowid.minutes, now.getSeconds() + settingsChronowid.seconds); settingsChronowid.goal = goal.getTime(); storage.writeJSON('chronowid.json', settingsChronowid); + if (WIDGETS["chronowid"]) WIDGETS["chronowid"].reload(); } function resetSettings() { @@ -44,6 +44,7 @@ function showMenu() { timerMenu.started.value = settingsChronowid.started; } }, + '< Back' : ()=>{load();}, 'Reset values': function() { settingsChronowid.hours = 0; settingsChronowid.minutes = 0; @@ -84,15 +85,15 @@ function showMenu() { }, 'Timer on': { value: settingsChronowid.started, - format: boolFormat, + format: v => v ? "On" : "Off", onchange: v => { settingsChronowid.started = v; updateSettings(); } }, }; - timerMenu['-Exit-'] = ()=>{load();}; + return E.showMenu(timerMenu); } -showMenu(); \ No newline at end of file +showMenu(); diff --git a/apps/chronowid/chrono_with_pastel.jpg b/apps/chronowid/chrono_with_pastel.jpg deleted file mode 100644 index 2f5993e79..000000000 Binary files a/apps/chronowid/chrono_with_pastel.jpg and /dev/null differ diff --git a/apps/chronowid/chrono_with_wave.jpg b/apps/chronowid/chrono_with_wave.jpg deleted file mode 100644 index 5f35bd28b..000000000 Binary files a/apps/chronowid/chrono_with_wave.jpg and /dev/null differ diff --git a/apps/chronowid/screenshot.png b/apps/chronowid/screenshot.png new file mode 100644 index 000000000..f94eece94 Binary files /dev/null and b/apps/chronowid/screenshot.png differ diff --git a/apps/chronowid/widget.js b/apps/chronowid/widget.js index f0e785efd..2d1c78941 100644 --- a/apps/chronowid/widget.js +++ b/apps/chronowid/widget.js @@ -1,93 +1,79 @@ (() => { - const storage = require('Storage'); - settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file - var height = 23; - var width = 58; + var settingsChronowid; var interval = 0; //used for the 1 second interval timer - var now = new Date(); + var diff; - var time = 0; - var diff = settingsChronowid.goal - now; - //Convert ms to time function getTime(t) { var milliseconds = parseInt((t % 1000) / 100), seconds = Math.floor((t / 1000) % 60), minutes = Math.floor((t / (1000 * 60)) % 60), hours = Math.floor((t / (1000 * 60 * 60)) % 24); - - hours = (hours < 10) ? "0" + hours : hours; - minutes = (minutes < 10) ? "0" + minutes : minutes; - seconds = (seconds < 10) ? "0" + seconds : seconds; - - return hours + ":" + minutes + ":" + seconds; + return hours.toString().padStart(2,0) + ":" + minutes.toString().padStart(2,0) + ":" + seconds.toString().padStart(2,0); } - function printDebug() { - print ("Nowtime: " + getTime(now)); - print ("Now: " + now); + /*function printDebug() { print ("Goaltime: " + getTime(settingsChronowid.goal)); print ("Goal: " + settingsChronowid.goal); print("Difftime: " + getTime(diff)); print("Diff: " + diff); print ("Started: " + settingsChronowid.started); print ("----"); - } + }*/ //counts down, calculates and displays function countDown() { - now = new Date(); + var now = new Date(); diff = settingsChronowid.goal - now; //calculate difference - WIDGETS["chronowid"].draw(); - //time is up + // time is up if (settingsChronowid.started && diff < 1000) { Bangle.buzz(1500); //write timer off to file settingsChronowid.started = false; - storage.writeJSON('chronowid.json', settingsChronowid); + require('Storage').writeJSON('chronowid.json', settingsChronowid); clearInterval(interval); //stop interval + interval = undefined; } - //printDebug(); + // calculates width and redraws accordingly + WIDGETS["chronowid"].redraw(); } - // draw your widget - function draw() { - if (!settingsChronowid.started) { - width = 0; - return; //do not draw anything if timer is not started - } - g.reset(); - if (diff >= 0) { - if (diff < 3600000) { //less than 1 hour left - width = 58; - g.clearRect(this.x,this.y,this.x+width,this.y+height); - g.setFont("6x8", 2); - g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00 - } - if (diff >= 3600000) { //one hour or more left - width = 48; - g.clearRect(this.x,this.y,this.x+width,this.y+height); - g.setFont("6x8", 1); - g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00 - } - } - // not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed. - // else { - // width = 58; - // g.clearRect(this.x,this.y,this.x+width,this.y+height); - // g.setFont("6x8", 2); - // g.drawString("END", this.x+15, this.y+5); - // } - } - - if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second - // add the widget - WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() { - reload(); - Bangle.drawWidgets(); // relayout all widgets + WIDGETS["chronowid"]={area:"tl",width:0,draw:function() { + if (!this.width) return; + g.reset().setFontAlign(0,0).clearRect(this.x,this.y,this.x+this.width,this.y+23); + //g.drawRect(this.x,this.y,this.x+this.width-1, this.y+23); + var scale; + var timeStr; + if (diff < 3600000) { //less than 1 hour left + width = 58; + scale = 2; + timeStr = getTime(diff).substring(3); // remove hour part 00:00:00 -> 00:00 + } else { //one hour or more left + width = 48; + scale = 1; + timeStr = getTime(diff); //display hour 00:00:00 but small + } + // Font5x9Numeric7Seg - just build this in as it's tiny + g.setFontCustom(atob("AAAAAAAAAAIAAAQCAQAAAd0BgMBdwAAAAAAAdwAB0RiMRcAAAERiMRdwAcAQCAQdwAcERiMRBwAd0RiMRBwAAEAgEAdwAd0RiMRdwAcERiMRdwAFAAd0QiEQdwAdwRCIRBwAd0BgMBAAABwRCIRdwAd0RiMRAAAd0QiEQAAAAAAAAAA="), 32, atob("BgAAAAAAAAAAAAAAAAYCAAYGBgYGBgYGBgYCAAAAAAAABgYGBgYG"), 9 + (scale<<8)); + g.drawString(timeStr, this.x+this.width/2, this.y+12); + }, redraw:function() { + var last = this.width; + if (!settingsChronowid.started) this.width = 0; + else this.width = (diff < 3600000) ? 58 : 48; + if (last != this.width) Bangle.drawWidgets(); + else this.draw(); + }, reload:function() { + settingsChronowid = require('Storage').readJSON("chronowid.json",1)||{}; + if (interval) clearInterval(interval); + interval = undefined; + // start countdown each second + if (settingsChronowid.started) interval = setInterval(countDown, 1000); + // reset everything + countDown(); }}; //printDebug(); - countDown(); -})(); \ No newline at end of file + // set width correctly, start countdown each second + WIDGETS["chronowid"].reload(); +})(); 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/compass/ChangeLog b/apps/compass/ChangeLog index d2bfbd4fa..4bb7838ac 100644 --- a/apps/compass/ChangeLog +++ b/apps/compass/ChangeLog @@ -2,3 +2,4 @@ 0.02: Show text if uncalibrated 0.03: Eliminate flickering 0.04: Fix for Bangle.js 2 and themes +0.05: Fix bearing not clearing correctly (visible in single or double digit bearings) diff --git a/apps/compass/compass.js b/apps/compass/compass.js index d26081dd5..65ad83c4f 100644 --- a/apps/compass/compass.js +++ b/apps/compass/compass.js @@ -48,7 +48,7 @@ Bangle.on('mag', function(m) { } g.setFontAlign(0,0).setFont("6x8",3); var y = 36; - g.clearRect(M-40,y,M+40,y+24); + g.clearRect(M-40,24,M+40,48); g.drawString(Math.round(m.heading),M,y,true); } 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 078be395e..46852864a 100644 --- a/apps/cubescramble/ChangeLog +++ b/apps/cubescramble/ChangeLog @@ -1,2 +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-icon.js b/apps/cubescramble/cube-scramble-icon.js index 686d47068..d22d6d33b 100644 --- a/apps/cubescramble/cube-scramble-icon.js +++ b/apps/cubescramble/cube-scramble-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("AH4A6iIAQCwkBC6MQC4kYxAACwMT/4ACmMe9wAC6IXLj4XD+IXE8IX/C9/zR4oXOmYABC6UTCwQXZjrXKf5IAHC713AAURindAAVBiatDmIXFi4XDuMdC4fRYooX/5nBC6Xc5gABC6UcCwQXF+aPMC471DC6MTCwQXHa4V2szXBC4bXBC5YAQC7se9wAC6MYxAACwJTCAAIXL8IXFQYoX/C/4XtjrXNu1ma4z/JAA4XEgAXRCwgA/AGo")) +require("heatshrink").decompress(atob("mEwwhC/AHcRACAWEgIXRiAXEjGIAAWBif/AAUxj3uAAXRC5cfC4fxC4nhC/4Xv+aPFC50zAAIXSiYWCC7Mda5T/JAA4Xeu4ACiMU7oACoMTVocxC4sXC4dxjoXD6LFFC//M4IXS7nMAAIXSjgWCC4vzR5gXHeoYXRiYWCC47XCu1ma4IXDa4IXLACAXdj3uAAXRjGIAAWBKYQABC5fhC4qDFC/4X/C9sda5t2szXGf5IAHC4kAC6IWEAH4A1")) 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/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 985321e91..c3102b4b9 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -2,3 +2,4 @@ 0.02: Multiple pages 0.03: cycle thru pages 0.04: reset to clock after 2 mins of inactivity +0.05: add Bangle 2 version diff --git a/apps/dtlaunch/README.md b/apps/dtlaunch/README.md index 70f7ff931..ba2301d91 100644 --- a/apps/dtlaunch/README.md +++ b/apps/dtlaunch/README.md @@ -3,7 +3,7 @@ ![](screenshot.jpg) In the picture above, the Settings app is selected. -## Controls +## Controls- Bangle **BTN1** - move backward through app icons on a page @@ -13,4 +13,12 @@ In the picture above, the Settings app is selected. **Swipe Left** - move to next page of app icons +**Swipe Right** - move to previous page of app icons + +## Controls- Bangle 2 + +**Touch** - icon to select, scond touch launches app + +**Swipe Left** - move to next page of app icons + **Swipe Right** - move to previous page of app icons \ No newline at end of file diff --git a/apps/dtlaunch/app.js b/apps/dtlaunch/app-b1.js similarity index 100% rename from apps/dtlaunch/app.js rename to apps/dtlaunch/app-b1.js diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js new file mode 100644 index 000000000..674fe3677 --- /dev/null +++ b/apps/dtlaunch/app-b2.js @@ -0,0 +1,105 @@ +/* Desktop launcher +* +*/ + +var s = require("Storage"); +var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); +apps.sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; +}); +apps.forEach(app=>{ + if (app.icon) + app.icon = s.read(app.icon); // should just be a link to a memory area + }); + +var Napps = apps.length; +var Npages = Math.ceil(Napps/4); +var maxPage = Npages-1; +var selected = -1; +var oldselected = -1; +var page = 0; +const XOFF = 24; +const YOFF = 30; + +function draw_icon(p,n,selected) { + var x = (n%2)*72+XOFF; + var y = n>1?72+YOFF:YOFF; + (selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+10,y+2,x+60,y+52); + g.clearRect(x+12,y+4,x+59,y+51); + g.setColor(g.theme.fg); + try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){} + g.setFontAlign(0,-1,0).setFont("6x8",1); + var txt = apps[p*4+n].name.split(" "); + for (var i = 0; i < txt.length; i++) { + txt[i] = txt[i].trim(); + g.drawString(txt[i],x+36,y+54+i*8); + } +} + +function drawPage(p){ + g.reset(); + g.clearRect(0,24,175,175); + var O = 88+YOFF/2-12*(Npages/2); + for (var j=0;j{ + selected = 0; + oldselected=-1; + if (dir<0){ + ++page; if (page>maxPage) page=0; + drawPage(page); + } else { + --page; if (page<0) page=maxPage; + drawPage(page); + } +}); + +function isTouched(p,n){ + if (n<0 || n>3) return false; + var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF; + var x2 = x1+71; var y2 = y1+81; + return (p.x>x1 && p.y>y1 && p.x{ + var i; + for (i=0;i<4;i++){ + if((page*4+i)=0) { + if (selected!=i){ + draw_icon(page,selected,false); + } else { + load(apps[page*4+i].src); + } + } + selected=i; + break; + } + } + } + if ((i==4 || (page*4+i)>Napps) && selected>=0) { + draw_icon(page,selected,false); + selected=-1; + } +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawPage(0); diff --git a/apps/dtlaunch/shot1.png b/apps/dtlaunch/shot1.png new file mode 100644 index 000000000..e6a9bcd3a Binary files /dev/null and b/apps/dtlaunch/shot1.png differ diff --git a/apps/dtlaunch/shot2.png b/apps/dtlaunch/shot2.png new file mode 100644 index 000000000..4c0c33c91 Binary files /dev/null and b/apps/dtlaunch/shot2.png differ diff --git a/apps/dtlaunch/shot3.png b/apps/dtlaunch/shot3.png new file mode 100644 index 000000000..1ffdf8090 Binary files /dev/null and b/apps/dtlaunch/shot3.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/flow/README.md b/apps/flow/README.md new file mode 100644 index 000000000..caeaf92d9 --- /dev/null +++ b/apps/flow/README.md @@ -0,0 +1,12 @@ +# FLOW + +This is a game where you have to help a flow avoid white obstacles thing by tapping! +This is a demake of an app which I forgot the name of. +Press BTN(1) to restart. +See if you can get to 2500 score! + +## Screenshots + +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) diff --git a/apps/flow/app-icon.js b/apps/flow/app-icon.js new file mode 100644 index 000000000..969a608f4 --- /dev/null +++ b/apps/flow/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AwX48EHgEC1WgCQkVqoDBBfuqBQcBqoLagEqGAguBqALaGAOoAoQuEBbEAKgIMBBQNUBbgMCyoKHBbBVBBYIKGBbEBtNVrQLfOgNaT4gLagp0CPQOABbcBFwNAgEKBgILbitVqAFClWq0ALZFwTDFGAQLZFwYwDBfg")) diff --git a/apps/flow/app.js b/apps/flow/app.js new file mode 100644 index 000000000..5f4da8f35 --- /dev/null +++ b/apps/flow/app.js @@ -0,0 +1,220 @@ +const isB2 = process.env.HWVERSION === 2; + +// Bangle.js 1 runs just too fast in direct mode??? (also no getPixel) +if (!isB2) Bangle.setLCDMode("120x120"); + +const options = Bangle.getOptions(); + +options.lockTimeout = 0; +options.lcdPowerTimeout = 0; + +Bangle.setOptions(options); + +g.reset(); +g.setBgColor(0, 0, 0); +g.setColor(255, 255, 255); +g.clear(); +const h = g.getHeight(); + +function trigToCoord(ret) { + return ((ret + 1) * h) / 2; +} + +function trigToLen(ret) { + return (ret * h) / 2; +} + +let i = 0.2; +let speedCoef = 0.014; + +let flowFile = require("Storage").readJSON("flow.json"); + +let highestI = (flowFile && flowFile.hiscore) || 0.1; + +let colorA = [255, 255, 0]; +let colorB = [0, 255, 255]; + +let x = 0; +let xt = 0; +let safeMode = false; +let lost = false; + +function offsetRect(g, x, y, w) { + g.fillRect(x, y, x + w, y + w); +} + +function getColor(num) { + return [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [1, 1, 0], + [0, 1, 1], + [1, 0, 1], + [0.5, 0.5, 1], + [1, 0.5, 0], + [0, 1, 0.5], + [0.5, 0.5, 0.5], + ][num]; +} + +function calculateColor(num) { + colorA = getColor(Math.floor((num % 1) * 10)); + colorB = getColor(Math.floor((num % 10) - (num % 1))); +} + +calculateColor(highestI); + +Bangle.on("touch", () => (safeMode = !safeMode)); + +function resetGame() { + x = xt = 0; + safeMode = lost = false; + i = 0.2; + speedCoef = 0.014; + obstaclePeriod = 150; + obstacleMode = 1; + g.clear(); + shownScore = false; + intervalId = setInterval(draw); +} + +function checkCollision() { + lost = g.getPixel(trigToCoord(+x), (h * 2) / 3 - 4) !== 0; + if (lost) { + scoringI = i; + speedCoef = Math.min(speedCoef, 0.02); + g.setFont(isB2 ? "6x15" : "4x6", 3); + g.setColor(colorA[0], colorA[1], colorA[2]) + .drawString( + "Game over", + trigToCoord(0) - g.stringWidth("Game over") / 2, + trigToCoord(0) + ) + .setColor(1, 1, 1); + } +} + +function drawPlayer() { + if (!safeMode) xt = Math.cos(i * Math.PI * 4) / 7.5; + else xt = -Math.cos(i * Math.PI * 2) / 20 + 0.35; + x = x * 0.8 + xt * 0.2; + if (highestI > 250) calculateColor(i); + g.setColor(colorA[0], colorA[1], colorA[2]); + offsetRect(g, trigToCoord(+x), (h * 2) / 3, 3); + g.setColor(colorB[0], colorB[1], colorB[2]); + offsetRect(g, trigToCoord(-x), (h * 2) / 3, 3); +} + +let obstaclePeriod = 150; +let obstacleMode = 1; + +function drawObstracle() { + g.setColor(1, 1, 1); + switch (obstacleMode) { + case 0: + offsetRect(g, trigToCoord(-0.15), 0, trigToLen(0.3)); + break; + case 1: + offsetRect(g, trigToCoord(0.2), 0, trigToLen(0.2)); + offsetRect(g, trigToCoord(-0.4), 0, trigToLen(0.2)); + break; + case 2: + break; + } + obstaclePeriod--; + if (obstaclePeriod <= 0) { + // If we are off cooldown mode, pick a random actual mode + if (obstacleMode === 2) { + obstaclePeriod = Math.random() * 50 + 50; + obstacleMode = Math.round(Math.random()); + } else if (Math.random() > 0.5) { + // Give it a chance to repeat with no cooldown + obstaclePeriod = 25 + 2.5 * speedCoef; + obstacleMode = 2; + } + } +} + +let shownScore = false; +let scoringI = 0; + +function draw() { + if (!lost) { + drawPlayer(); + checkCollision(); + speedCoef *= 1.0005; + drawObstracle(); + } else { + speedCoef /= 1.05; + if (speedCoef <= 0.005) { + clearInterval(intervalId); + i -= speedCoef; + g.setFont(isB2 ? "6x15" : "4x6", 1); + const str = "Hiscore: " + Math.round(highestI * 10); + g.setColor( + scoringI > highestI ? 0 : 255, + 0, + scoringI > highestI ? 255 : 0 + ) + .drawString( + str, + trigToCoord(0) - g.stringWidth(str) / 2, + trigToCoord(0) + ) + .setColor(255, 255, 255); + if (scoringI > highestI) { + highestI = scoringI; + require("Storage").writeJSON("flow.json", { + hiscore: highestI, + }); + calculateColor(highestI); + } + setTimeout(resetGame, 3000); + } else if (speedCoef <= 0.01 && !shownScore) { + shownScore = true; + g.setFont(isB2 ? "6x15" : "4x6", 2); + const str = "Score: " + Math.round(scoringI * 10); + g.setColor(colorB[0], colorB[1], colorB[2]) + .drawString( + str, + trigToCoord(0) - g.stringWidth(str) / 2, + trigToCoord(0) + ) + .setColor(1, 1, 1); + } + } + i += speedCoef; + g.scroll(0, speedCoef * h); + g.flip(); +} + +let intervalId; + +if (BTN.read()) { + for (let i = 0; i < 10; i++) { + color = getColor(i); + g.setColor(color[0], color[1], color[2]); + g.fillRect((i / 10) * h, 0, ((i + 1) / 10) * h, h); + } + g.setColor(0); + g.setFont("Vector", 9); + let str = "Welcome to the debug screen!"; + g.drawString( + str, + trigToCoord(0) - g.stringWidth(str) / 2, + trigToCoord(0) - 9 + ); + str = "Don't hold BTN while opening to play!"; + g.drawString(str, trigToCoord(0) - g.stringWidth(str) / 2, trigToCoord(0)); + g.flip(); + setInterval(() => { + g.scroll(0, 0.014 * h); + i += 0.014; + calculateColor(i); + g.setColor(colorA[0], colorA[1], colorA[2]); + g.fillRect(0, 0, trigToCoord(0), 0.014 * h); + g.setColor(colorB[0], colorB[1], colorB[2]); + g.fillRect(trigToCoord(0), 0, trigToCoord(1), 0.014 * h); + }, 1000 / 30); +} else intervalId = setInterval(draw, 1000 / 30); diff --git a/apps/flow/app.png b/apps/flow/app.png new file mode 100644 index 000000000..b35c3ca77 Binary files /dev/null and b/apps/flow/app.png differ diff --git a/apps/flow/screenshot1.png b/apps/flow/screenshot1.png new file mode 100644 index 000000000..fd5dee427 Binary files /dev/null and b/apps/flow/screenshot1.png differ diff --git a/apps/flow/screenshot2.png b/apps/flow/screenshot2.png new file mode 100644 index 000000000..e29691b69 Binary files /dev/null and b/apps/flow/screenshot2.png differ diff --git a/apps/flow/screenshot3.png b/apps/flow/screenshot3.png new file mode 100644 index 000000000..3e1c80ba7 Binary files /dev/null and b/apps/flow/screenshot3.png differ 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 ecbca5fb6..9cebf0a31 100644 --- a/apps/gbmusic/ChangeLog +++ b/apps/gbmusic/ChangeLog @@ -2,4 +2,6 @@ 0.02: Increase text brightness, improve controls, (try to) reduce memory usage 0.03: Only auto-start if active app is a clock, auto close after 1 hour of inactivity 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 \ No newline at end of file +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/README.md b/apps/gbmusic/README.md index 4bad9b8c8..5d06164c2 100644 --- a/apps/gbmusic/README.md +++ b/apps/gbmusic/README.md @@ -3,7 +3,9 @@ If you have an Android phone with Gadgetbridge, this app allows you to view and control music playback. -![Screenshot: playing](screenshot.png) ![Screenshot: paused](screenshot_2.png) +| Bangle.js 1 | Bangle.js 2 | +|:-------------------------------------------|:-------------------------------------------| +| ![Screenshot: Bangle 1](screenshot_v1.png) | ![Screenshot: Bangle 2](screenshot_v2.png) | Download the [latest Gadgetbridge for Android here](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/). @@ -23,25 +25,27 @@ Automatically load the app when you play music and close when the music stops. (If the app opened automatically, it closes after music has been paused for 5 minutes.) **Simple button**: -Disable double/triple pressing Button 2: always simply toggle play/pause. +Disable double/triple pressing Middle Button: always simply toggle play/pause. (For music players which handle multiple button presses themselves.) ## Controls ### Buttons -* Button 1: Volume up -* Button 2: - - Single press: toggle play/pause - - Double press: next song - - Triple press: previous song +* Button 1 (*Bangle.js 1*): Volume up +* Middle Button: + - Single press: Toggle play/pause + - Double press: Next song + - Triple press: Previous song - Long-press: open application launcher -* Button 3: Volume down +* Button 3 (*Bangle.js 1*): Volume down ### Touch -* Left: pause/previous song -* Right: next song/resume -* Center: toggle play/pause -* Swipe: next/previous song +* Left: Pause/previous song +* Right: Next song/resume +* Center: Toggle play/pause +* Swipe left/right: Next/previous song +* Swipe up/down (*Bangle.js 2*): Volume up/down + ## Creator diff --git a/apps/gbmusic/app.js b/apps/gbmusic/app.js index 5f95868bb..f514dfccd 100644 --- a/apps/gbmusic/app.js +++ b/apps/gbmusic/app.js @@ -4,77 +4,9 @@ **/ let auto = false; // auto close if opened automatically let stat = ""; -let info = { - artist: "", - album: "", - track: "", - n: 0, - c: 0, -}; const POUT = 300000; // auto close timeout when paused: 5 minutes (in ms) const IOUT = 3600000; // auto close timeout for inactivity: 1 hour (in ms) - -/////////////////////// -// Self-repeating timeouts -/////////////////////// - -// Clock -let tock = -1; -function tick() { - if (!Bangle.isLCDOn()) { - return; - } - const now = new Date; - if (now.getHours()*60+now.getMinutes()!==tock) { - drawDateTime(); - tock = now.getHours()*60+now.getMinutes(); - } - setTimeout(tick, 1000); // we only show minute precision anyway -} - -// Fade out while paused and auto closing -let fade = null; -function fadeOut() { - if (!Bangle.isLCDOn() || !fade) { - return; - } - drawMusic(false); // don't clear: draw over existing text to prevent flicker - setTimeout(fadeOut, 500); -} -function brightness() { - if (!fade) { - return 1; - } - return Math.max(0, 1-((Date.now()-fade)/POUT)); -} - -// Scroll long track names -// use an interval to get smooth movement -let offset = null, // scroll Offset: null = no scrolling - iScroll; -function scroll() { - offset += 10; - drawScroller(); -} -function scrollStart() { - if (offset!==null) { - return; // already started - } - offset = 0; - if (Bangle.isLCDOn()) { - if (!iScroll) { - iScroll = setInterval(scroll, 200); - } - drawScroller(); - } -} -function scrollStop() { - if (iScroll) { - clearInterval(iScroll); - iScroll = null; - } - offset = null; -} +const BANGLE2 = process.env.HWVERSION===2; /** * @param {string} text @@ -85,21 +17,22 @@ function fitText(text) { return Infinity; } // make a guess, then shrink/grow until it fits - const test = (s) => g.setFont("Vector", s).stringWidth(text); - let best = Math.floor(24000/test(100)); - if (test(best)===240) { // good guess! + const w = Bangle.appRect.w, + test = (s) => g.setFont("Vector", s).stringWidth(text); + let best = Math.floor(100*w/test(100)); + if (test(best)===w) { // good guess! return best; } - if (test(best)<240) { + if (test(best) 240 + // width > w do { best--; - } while(test(best)>240); + } while(test(best)>w); return best; } @@ -115,14 +48,6 @@ function textCode(text) { } return code%360; } -// dark magic -function hsv2rgb(h, s, v) { - const f = (n) => { - const k = (n+h/60)%6; - return v-v*s*Math.max(Math.min(k, 4-k, 1), 0); - }; - return {r: f(5), g: f(3), b: f(1)}; -} function f2hex(f) { return ("00"+(Math.round(f*255)).toString(16)).substr(-2); } @@ -131,38 +56,218 @@ function f2hex(f) { * @return {string} Semi-random color to use for given info */ function infoColor(name) { - let h, s, v; - if (name==="num") { - // always white - h = 0; - s = 0; - } else { - // make color depend deterministically on info - let code = textCode(info[name]); - switch(name) { - case "track": // also use album - code += textCode(info.album); - // fallthrough - case "album": // also use artist - code += textCode(info.artist); - } - h = code%360; - s = 0.7; + // make color depend deterministically on info + let code = textCode(layout[name].label); + switch(name) { + case "title": // also use album and artist + code += textCode(layout.album.label); + // fallthrough + case "album": // also use artist + code += textCode(layout.artist.label); } - v = brightness(); - const rgb = hsv2rgb(h, s, v); - return "#"+f2hex(rgb.r)+f2hex(rgb.g)+f2hex(rgb.b); + let rgb; + if (g.getBPP()===3) { + // only pick 3-bit colors, always at full brightness + rgb = [code&1, (code&2)/2, (code&4)/4]; + if (g.setColor(rgb[0], rgb[1], rgb[2]).getColor()===g.theme.bg) { + // avoid picking the bg color + rgb = rgb.map(c => 1-c); + } + return "#"+f2hex(rgb[0])+f2hex(rgb[1])+f2hex(rgb[2]); + } else { + // pick any hue, adjust for brightness + const h = code%360, s = 0.7, b = brightness(); + return E.HSBtoRGB(h/360, s, b); + } +} + +/** + * Render scrolling title + * @param l + */ +function rScroller(l) { + g.setFont("Vector", Math.round(g.getHeight()*l.fsz.slice(0, -1)/100)); + const w = g.stringWidth(l.label)+40, + y = l.y+l.h/2; + l.offset = l.offset%w; + g.setClipRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1) + .setColor(l.col) + .setFontAlign(-1, 0) // left center + .clearRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1) + .drawString(l.label, l.x-l.offset+40, y) + .drawString(l.label, l.x-l.offset+40+w, y); } /** - * Remember track color until info changes - * Because we need this every time we move the scroller - * @return {string} + * Render title + * @param l */ -function trackColor() { - if (!("track_color" in info) || fade) { - info.track_color = infoColor("track"); +function rTitle(l) { + if (l.offset!==null) { + rScroller(l); // already scrolling + return; } - return info.track_color; + let size = fitText(l.label); + if (sizel.h) { + size = l.h; + } + g.setFont("Vector", size) + .setFontAlign(0, -1) // center top + .drawString(l.label, l.x+l.w/2, l.y); +} +/** + * Render icon + * @param l + */ +function rIcon(l) { + const x2 = l.x+l.w-1, + y2 = l.y+l.h-1; + switch(l.icon) { + case "pause": + const w13 = l.w/3; + g.drawRect(l.x, l.y, l.x+w13, y2); + g.drawRect(l.x+l.w-w13, l.y, x2, y2); + break; + case "play": + g.drawPoly([ + l.x, l.y, + x2, l.y+l.h/2, + l.x, y2, + ], true); + break; + case "previous": + const w15 = l.w*1/5; + g.drawPoly([ + x2, l.y, + l.x+w15, l.y+l.h/2, + x2, y2, + ], true); + g.drawRect(l.x, l.y, l.x+w15, y2); + break; + case "next": + const w45 = l.w*4/5; + g.drawPoly([ + l.x, l.y, + l.x+w45, l.y+l.h/2, + l.x, y2, + ], true); + g.drawRect(l.x+w45, l.y, x2, y2); + break; + default: // red X + console.log(`Unknown icon: ${l.icon}`); + g.setColor("#f00") + .drawRect(l.x, l.y, x2, y2) + .drawLine(l.x, l.y, x2, y2) + .drawLine(l.x, y2, x2, l.y); + } +} +let layout; +function makeUI() { + global.gbmusic_active = true; // we don't need our widget (needed for <2.09 devices) + Bangle.loadWidgets(); + Bangle.drawWidgets(); + delete (global.gbmusic_active); + const Layout = require("Layout"); + layout = new Layout({ + type: "v", c: [ + { + type: "h", fillx: 1, c: [ + {id: "time", type: "txt", label: "88:88", valign: -1, halign: -1, font: "8%", bgCol: g.theme.bg}, + {fillx: 1}, + {id: "num", type: "txt", label: "88:88", valign: -1, halign: 1, font: "12%", bgCol: g.theme.bg}, + BANGLE2 ? {} : {id: "up", type: "txt", label: " +", font: "6x8:2"}, + ], + }, + {id: "title", type: "custom", label: "", fillx: 1, filly: 2, offset: null, font: "Vector:20%", render: rTitle, bgCol: g.theme.bg}, + {id: "artist", type: "custom", label: "", fillx: 1, filly: 1, size: 30, render: rInfo, bgCol: g.theme.bg}, + {id: "album", type: "custom", label: "", fillx: 1, filly: 1, size: 20, render: rInfo, bgCol: g.theme.bg}, + {height: 10}, + { + type: "h", c: [ + {width: 3}, + {id: "prev", type: "custom", height: 15, width: 15, icon: "previous", render: rIcon, bgCol: g.theme.bg}, + {id: "date", type: "txt", halign: 0, valign: 1, label: "", font: "8%", fillx: 1, bgCol: g.theme.bg}, + {id: "next", type: "custom", height: 15, width: 15, icon: "next", render: rIcon, bgCol: g.theme.bg}, + BANGLE2 ? {width: 3} : {id: "down", type: "txt", label: " -", font: "6x8:2"}, + ], + }, + {height: 10}, + ], + }, {lazy: true}); + layout.render(); +} + +/////////////////////// +// Self-repeating timeouts +/////////////////////// + +// Clock +let tock = -1; +function tick() { + if (!BANGLE2 && !Bangle.isLCDOn()) { + return; + } + const now = new Date(); + if (now.getHours()*60+now.getMinutes()!==tock) { + drawDateTime(); + tock = now.getHours()*60+now.getMinutes(); + } + setTimeout(tick, 1000); // we only show minute precision anyway +} + +// Fade out while paused and auto closing +let fade = null; +function fadeOut() { + if (BANGLE2 || !Bangle.isLCDOn() || !fade) { + return; + } + layout.render(); + setTimeout(fadeOut, 500); +} +function brightness() { + if (!fade) { + return 1; + } + return Math.max(0, 1-((Date.now()-fade)/POUT)); +} + +// Scroll long track names +// use an interval to get smooth movement +let iScroll; +function scroll() { + layout.title.offset += 10; + rScroller(layout.title); +} +function scrollStart() { + if (layout.title.offset!==null) { + return; // already started + } + layout.title.offset = 0; + if (BANGLE2 || Bangle.isLCDOn()) { + if (!iScroll) { + iScroll = setInterval(scroll, 200); + } + rScroller(layout.title); + } +} +function scrollStop() { + if (iScroll) { + clearInterval(iScroll); + iScroll = null; + } + layout.title.offset = null; } //////////////////// @@ -172,10 +277,9 @@ function trackColor() { * Draw date and time */ function drawDateTime() { - const now = new Date; + const now = new Date(); const l = require("locale"); const is12 = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; - let time; if (is12) { const d12 = new Date(now.getTime()); const hour = d12.getHours(); @@ -184,29 +288,35 @@ function drawDateTime() { } else if (hour>12) { d12.setHours(hour-12); } - time = l.time(d12, true)+l.meridian(now); + layout.time.label = l.time(d12, true)+l.meridian(now); } else { - time = l.time(now, true); + layout.time.label = l.time(now, true); } - g.reset(); - g.setFont("Vector", 24) - .setFontAlign(-1, -1) // top left - .clearRect(10, 30, 119, 54) - .drawString(time, 10, 30); - - const date = require("locale").date(now, true); - g.setFont("Vector", 16) - .setFontAlign(0, 1) // bottom center - .setClipRect(35, 198, 199, 214) - .clearRect(31, 198, 199, 214) - .drawString(date, 119, 240-26); + layout.date.label = require("locale").date(now, true); + layout.render(); } +function drawControls() { + let l = layout; + const cc = a => (a ? "#f00" : "#0f0"); // control color: red for active, green for inactive + if (!BANGLE2) { + l.up.col = cc("volumeup" in tCommand); + l.down.col = cc("volumedown" in tCommand); + } + 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); + layout.render(); +} + +//////////////////////// +// GB event handlers +/////////////////////// /** - * Draw track number and total count - * @param {boolean} clr - Clear area before redrawing? + * Mangle track number and total count for display */ -function drawNum(clr) { +function formatNum(info) { let num = ""; if ("n" in info && info.n>0) { num = "#"+info.n; @@ -214,198 +324,26 @@ function drawNum(clr) { num += "/"+info.c; } } - g.reset(); - g.setFont("Vector", 30) - .setFontAlign(1, -1); // top right - if (clr) { - g.clearRect(225, 30, 120, 60); - } - g.drawString(num, 225, 30); -} -/** - * Clear rectangle used by track title - */ -function clearTrack() { - g.clearRect(0, 60, 239, 119); -} -/** - * Draw track title - * @param {boolean} clr - Clear area before redrawing? - */ -function drawTrack(clr) { - let size = fitText(info.track); - if (size<25) { - // the title is too long: start the scroller - scrollStart(); - return; - } else { - scrollStop(); - } - // stationary track - if (size>40) { - size = 40; - } - g.reset(); - g.setFont("Vector", size) - .setFontAlign(0, 1) // center bottom - .setColor(trackColor()); - if (clr) { - clearTrack(); - } - g.drawString(info.track, 119, 109); -} -/** - * Draw scrolling track title - */ -function drawScroller() { - g.reset(); - g.setFont("Vector", 40); - const w = g.stringWidth(info.track)+40; - offset = offset%w; - g.setFontAlign(-1, 1) // left bottom - .setColor(trackColor()); - clearTrack(); - g.drawString(info.track, -offset+40, 109) - .drawString(info.track, -offset+40+w, 109); + return num; } -/** - * Draw track artist and album - * @param {boolean} clr - Clear area before redrawing? - */ -function drawArtistAlbum(clr) { - // we just use small enough fonts to make these always fit - // calculate stuff before clear+redraw - const aCol = infoColor("artist"); - const bCol = infoColor("album"); - let aSiz = fitText(info.artist); - if (aSiz>30) { - aSiz = 30; - } - let bSiz = fitText(info.album); - if (bSiz>20) { - bSiz = 20; - } - g.reset(); - if (clr) { - g.clearRect(0, 120, 240, 189); - } - let top = 124; - if (info.artist) { - g.setFont("Vector", aSiz) - .setFontAlign(0, -1) // center top - .setColor(aCol) - .drawString(info.artist, 119, top); - top += aSiz+4; // fit album neatly under artist - } - if (info.album) { - g.setFont("Vector", bSiz) - .setFontAlign(0, -1) // center top - .setColor(bCol) - .drawString(info.album, 119, top); - } -} - -/** - * - * @param {string} icon Icon name - * @param {number} x - * @param {number} y - * @param {number} s Icon size - */ -function drawIcon(icon, x, y, s) { - ({ - pause: function(x, y, s) { - const w1 = s/3; - g.drawRect(x, y, x+w1, y+s); - g.drawRect(x+s-w1, y, x+s, y+s); - }, - play: function(x, y, s) { - g.drawPoly([ - x, y, - x+s, y+s/2, - x, y+s, - ], true); - }, - previous: function(x, y, s) { - const w2 = s*1/5; - g.drawPoly([ - x+s, y, - x+w2, y+s/2, - x+s, y+s, - ], true); - g.drawRect(x, y, x+w2, y+s); - }, - next: function(x, y, s) { - const w2 = s*4/5; - g.drawPoly([ - x, y, - x+w2, y+s/2, - x, y+s, - ], true); - g.drawRect(x+w2, y, x+s, y+s); - }, - })[icon](x, y, s); -} -function controlColor(ctrl) { - return (ctrl in tCommand) ? "#ff0000" : "#008800"; -} -function drawControl(ctrl, x, y) { - g.setColor(controlColor(ctrl)); - const s = 20; - if (stat!==controlState) { - g.clearRect(x, y, x+s, y+s); - } - drawIcon(ctrl, x, y, s); -} -let controlState; -function drawControls() { - g.reset(); - if (stat==="play") { - // left touch - drawControl("pause", 10, 190); - // right touch - drawControl("next", 200, 190); - } else { - drawControl("previous", 10, 190); - drawControl("play", 200, 190); - } - g.setFont("6x8", 2); - // BTN1 - g.setFontAlign(1, -1); - g.setColor(controlColor("volumeup")); - g.drawString("+", 240, 30); - // BTN2 - g.setFontAlign(1, 1); - g.setColor(controlColor("volumedown")); - g.drawString("-", 240, 210); - controlState = stat; -} - -/** - * @param {boolean} [clr=true] Clear area before redrawing? - */ -function drawMusic(clr) { - clr = !(clr===false); // undefined means yes - drawNum(clr); - drawTrack(clr); - drawArtistAlbum(clr); -} - -//////////////////////// -// GB event handlers -/////////////////////// /** * Update music info - * @param {Object} e - Gadgetbridge musicinfo event + * @param {Object} info - Gadgetbridge musicinfo event */ -function musicInfo(e) { - info = e; - delete (info.t); - offset = null; - if (Bangle.isLCDOn()) { - drawMusic(); - } +function musicInfo(info) { + scrollStop(); + layout.title.label = info.track || ""; + layout.album.label = info.album || ""; + layout.artist.label = info.artist || ""; + // color depends on all labels + layout.title.col = infoColor("title"); + layout.album.col = infoColor("album"); + layout.artist.col = infoColor("artist"); + layout.num.label = formatNum(info); + layout.render(); + rTitle(layout.title); // force redraw of title, or scroller might break + // reset auto exit interval if (tIxt) { clearTimeout(tIxt); tIxt = null; @@ -435,7 +373,6 @@ function musicState(e) { tIxt = null; } fade = null; - delete info.track_color; if (auto) { // auto opened -> auto close switch(stat) { case "stop": // never actually happens with my phone :-( @@ -444,7 +381,7 @@ function musicState(e) { case "play": // if inactive for double song duration (or an hour if unknown), load the clock // i.e. phone finished playing without bothering to notify the watch - tIxt = setTimeout(load, (info.dur*2000) || IOUT); + tIxt = setTimeout(load, (e.dur*2000) || IOUT); break; case "pause": default: @@ -456,8 +393,7 @@ function musicState(e) { break; } } - if (Bangle.isLCDOn()) { - drawMusic(false); // redraw in case we were fading out but resumed play + if (BANGLE2 || Bangle.isLCDOn()) { drawControls(); } } @@ -473,30 +409,34 @@ function musicState(e) { */ let tPress, nPress = 0; function startButtonWatches() { - // BTN1/3: volume control - // Wait for falling edge to avoid messing with volume while long-pressing BTN3 - // to reload the watch (and same for BTN2 for consistency) - setWatch(() => { sendCommand("volumeup"); }, BTN1, {repeat: true, edge: "falling"}); - setWatch(() => { sendCommand("volumedown"); }, BTN3, {repeat: true, edge: "falling"}); + let btn = BTN1; + if (!BANGLE2) { + // BTN1/3: volume control + // Wait for falling edge to avoid messing with volume while long-pressing BTN3 + // to reload the watch (and same for BTN2 for consistency) + setWatch(() => { sendCommand("volumeup"); }, BTN1, {repeat: true, edge: "falling"}); + setWatch(() => { sendCommand("volumedown"); }, BTN3, {repeat: true, edge: "falling"}); + btn = BTN2; + } - // BTN2: long-press for launcher, otherwise depends on number of presses + // middle button: long-press for launcher, otherwise depends on number of presses setWatch(() => { if (nPress===0) { tPress = setTimeout(() => {Bangle.showLauncher();}, 3000); } - }, BTN2, {repeat: true, edge: "rising"}); + }, btn, {repeat: true, edge: "rising"}); const s = require("Storage").readJSON("gbmusic.json", 1) || {}; if (s.simpleButton) { setWatch(() => { clearTimeout(tPress); togglePlay(); - }, BTN2, {repeat: true, edge: "falling"}); + }, btn, {repeat: true, edge: "falling"}); } else { setWatch(() => { nPress++; clearTimeout(tPress); tPress = setTimeout(handleButton2Press, 500); - }, BTN2, {repeat: true, edge: "falling"}); + }, btn, {repeat: true, edge: "falling"}); } } function handleButton2Press() { @@ -524,7 +464,7 @@ let tCommand = {}; */ function sendCommand(command) { Bluetooth.println(JSON.stringify({t: "music", n: command})); - // for controlColor + // for control color if (command in tCommand) { clearTimeout(tCommand[command]); } @@ -539,18 +479,29 @@ function sendCommand(command) { function togglePlay() { sendCommand(stat==="play" ? "pause" : "play"); } -function startTouchWatches() { +function pausePrev() { + sendCommand(stat==="play" ? "pause" : "previous"); +} +function nextPlay() { + sendCommand(stat==="play" ? "next" : "play"); +} + +/** + * Setup touch+swipe for Bangle.js 1 + */ +function touch1() { Bangle.on("touch", side => { if (!Bangle.isLCDOn()) {return;} // for <2v10 firmware switch(side) { case 1: - sendCommand(stat==="play" ? "pause" : "previous"); + pausePrev(); break; case 2: - sendCommand(stat==="play" ? "next" : "play"); + nextPlay(); break; - case 3: + default: togglePlay(); + break; } }); Bangle.on("swipe", dir => { @@ -558,16 +509,56 @@ function startTouchWatches() { sendCommand(dir===1 ? "previous" : "next"); }); } +/** + * Setup touch+swipe for Bangle.js 2 + */ +function touch2() { + Bangle.on("touch", (side, xy) => { + const ar = Bangle.appRect; + if (xy.xar.x+ar.w*2/3) { + nextPlay(); + } else { + togglePlay(); + } + }); + // swiping + let drag; + Bangle.on("drag", e => { + if (!drag) { // start dragging + drag = {x: e.x, y: e.y}; + } else if (!e.b) { // released + const dx = e.x-drag.x, dy = e.y-drag.y; + drag = null; + if (Math.abs(dx)>Math.abs(dy)+10) { + // horizontal + sendCommand(dx>0 ? "previous" : "next"); + } else if (Math.abs(dy)>Math.abs(dx)+10) { + // vertical + sendCommand(dy>0 ? "volumedown" : "volumeup"); + } + } + }); +} +function startTouchWatches() { + if (BANGLE2) { + touch2(); + } else { + touch1(); + } +} function startLCDWatch() { + if (BANGLE2) { + return; // always keep drawing + } Bangle.on("lcdPower", (on) => { if (on) { // redraw and resume scrolling tick(); - drawMusic(); - drawControls(); + layout.render(); fadeOut(); - if (offset!==null) { - drawScroller(); + if (offset.offset!==null) { if (!iScroll) { iScroll = setInterval(scroll, 200); } @@ -585,15 +576,10 @@ function startLCDWatch() { ///////////////////// // Startup ///////////////////// -// check for saved music stat (by widget) to load g.clear(); -global.gbmusic_active = true; // we don't need our widget (needed for <2.09 devices) -Bangle.loadWidgets(); -Bangle.drawWidgets(); -delete (global.gbmusic_active); function startEmulator() { - if (typeof Bluetooth==="undefined") { // emulator! + if (typeof Bluetooth==="undefined" || typeof Bluetooth.println==="undefined") { // emulator! Bluetooth = { println: (line) => {console.log("Bluetooth:", line);}, }; @@ -609,6 +595,7 @@ function startWatches() { } function start() { + makeUI(); // start listening for music updates const _GB = global.GB; global.GB = (event) => { @@ -628,43 +615,39 @@ function start() { return; } }; - drawMusic(); - drawControls(); startWatches(); tick(); startEmulator(); } function init() { + // check for saved music status (by widget) to load let saved = require("Storage").readJSON("gbmusic.load.json", true); require("Storage").erase("gbmusic.load.json"); if (saved) { // autoloaded: load state was saved by widget - info = saved.info; - stat = saved.state; - delete saved; auto = true; start(); - } else { - delete saved; - let s = require("Storage").readJSON("gbmusic.json", 1) || {}; - if (!("autoStart" in s)) { - // user opened the app, but has not picked a setting yet - // ask them about autoloading now - E.showPrompt( - "Automatically load\n"+ - "when playing music?\n", - ).then(choice => { - s.autoStart = choice; - require("Storage").writeJSON("gbmusic.json", s); - delete s; - setTimeout(start, 0); - }); - } else { - delete s; - start(); - } + musicInfo(saved.info); + musicState(saved.state); + return; } -} -init(); + let s = require("Storage").readJSON("gbmusic.json", 1) || {}; + if ("autoStart" in s) { + start(); + return; + } + + // user opened the app, but has not picked a autoStart setting yet + // ask them about autoloading now + E.showPrompt( + "Automatically load\n"+ + "when playing music?\n" + ).then(choice => { + s.autoStart = choice; + require("Storage").writeJSON("gbmusic.json", s); + setTimeout(start, 0); + }); +} +init(); \ No newline at end of file diff --git a/apps/gbmusic/screenshot.png b/apps/gbmusic/screenshot.png deleted file mode 100644 index 569a6a2c5..000000000 Binary files a/apps/gbmusic/screenshot.png and /dev/null differ diff --git a/apps/gbmusic/screenshot_2.png b/apps/gbmusic/screenshot_2.png deleted file mode 100644 index f19f8f428..000000000 Binary files a/apps/gbmusic/screenshot_2.png and /dev/null differ diff --git a/apps/gbmusic/screenshot_v1.png b/apps/gbmusic/screenshot_v1.png new file mode 100644 index 000000000..3b290e459 Binary files /dev/null and b/apps/gbmusic/screenshot_v1.png differ diff --git a/apps/gbmusic/screenshot_v2.png b/apps/gbmusic/screenshot_v2.png new file mode 100644 index 000000000..b89b5022e Binary files /dev/null and b/apps/gbmusic/screenshot_v2.png differ diff --git a/apps/gbridge/sample_messages.js b/apps/gbridge/sample_messages.js index be33a25b8..046ffa9e4 100644 --- a/apps/gbridge/sample_messages.js +++ b/apps/gbridge/sample_messages.js @@ -15,7 +15,7 @@ GB({"t":"notify","id":1592721714,"src":"ALARMCLOCKRECEIVER"}) GB({"t":"notify-","id":1592721714}) // Weather update (doesn't show a notification, not handled by gbridge app: see weather app) -GB({"t":"weather","temp":288,"hum":94,"txt":"Light rain","wind":0,"loc":"Test City"}) +GB({"t":"weather","temp":288,"hum":94,"txt":"Light rain","wind":0,"wdir":120,"loc":"Test City"}) // Nextcloud updated a file GB({"t":"notify","id":1594184421,"src":"Nextcloud","title":"Downloaded","body":"test.file downloaded"}) 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/health/ChangeLog b/apps/health/ChangeLog index 5eb96a0ea..bde4f8ab8 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -6,3 +6,4 @@ 0.05: Fix daily summary calculation 0.06: Fix daily health summary for movement (a line got deleted!) 0.07: Added coloured bar charts +0.08: Suppress bleed through of E.showMenu's when displaying bar charts diff --git a/apps/health/app.js b/apps/health/app.js index eae45c190..08d6ead17 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -236,6 +236,9 @@ Bangle.on('swipe', dir => { // use setWatch() as Bangle.setUI("updown",..) interacts with swipes function setButton(fn) { + // cancel callback, otherwise a slight up down movement will show the E.showMenu() + Bangle.setUI("updown", undefined); + if (process.env.HWVERSION == 1) btn = setWatch(fn, BTN2); else diff --git a/apps/hebrew_calendar/ChangeLog b/apps/hebrew_calendar/ChangeLog new file mode 100644 index 000000000..d7dbc19e3 --- /dev/null +++ b/apps/hebrew_calendar/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: using TS and rollup to bundle +0.03: bug fixes and support bangle 1 \ No newline at end of file diff --git a/apps/hebrew_calendar/LICENSE b/apps/hebrew_calendar/LICENSE new file mode 100644 index 000000000..cd6624ad4 --- /dev/null +++ b/apps/hebrew_calendar/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-20 Ionică Bizău (https://ionicabizau.net) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/apps/hebrew_calendar/README.md b/apps/hebrew_calendar/README.md new file mode 100644 index 000000000..7a96a97db --- /dev/null +++ b/apps/hebrew_calendar/README.md @@ -0,0 +1,26 @@ +# Hebrew Calendar + +Displays the current hebrew calendar date +Add screen shots (if possible) to the app folder and link then into this file with ![](.png) + +## Usage + +Open the app, and it shows a menu with the date components + +## Features + +Shows the hebrew date, month, and year; alongside the gregorian date + +## Controls + +Name the buttons and what they are used for + +## Requests + +Michael Salaverry (github.com/barakplasma) + +## Creator + +Michael Salaverry +with help from https://github.com/IonicaBizau/hebrew-date (MIT license) + \ No newline at end of file diff --git a/apps/hebrew_calendar/app-icon.js b/apps/hebrew_calendar/app-icon.js new file mode 100644 index 000000000..b6b0a53ae --- /dev/null +++ b/apps/hebrew_calendar/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("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/4ABIn4C/AXWQX/4C/X/6/DghH/AX4C5wC/DgBH/AX4C5Xwi//AX6//gESI/4C/AW8gX4sCI/4C/AW6+FAAMJJX4C/AWtAX48BJX4C/AWq+HAAMEJX4C/AWeAX5MAgRN/AX4CxXpQADn//AH4A/AFn8Xxy//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X7n7tu/CaH27YnRE3d/EyO3X/4AG/3btu2CZ/t23bt4mS74mRtom1/4mSX+32JQXbCZwRCtu/E34mvX+xcCAQN/CRn7OIe3E34muX+39JSQRDTH4mO2wmSt6//X5JKNX4hxNE34mhX/6Y/E36/29qY/E3NtX/6/W/yY/EyffX/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X7hKMX4xxME34mgX+RZGARBiDLIwCIE34mkX+xcOAQISB/YROtu3EyV/E34mOX+39JSFv/4RPAQIm/E0S/29pKR/xxSE0vfTCNtX6QmUX/4CJTCYm/E0C/3LiACB+xxSE0vbTCW+E0u/X/4C/AXy//AX4C/X/4C/AX6//AX4C/X/4C/AX6//AX4C/X/4C/AX6//AX4C/X/4C/AX6//AX4C/X/4C/AX6//AX4C/X/4C/AX6//AX4C/X/4C/AX6//ARX2L6PfTHO+CiXtX/6/wOKX+E3C//ABaY/E34CHTSS//TH6//X+1vOKQmm7/9TCQmmX+xKRt5xSE34miX/6Y/E36/2/5KQv///ZxP24m/E0S//TH4m/X+/2JR4TCOJ+/E34mjX/4CG75xSE0yYC/yYS9omlX+xKPTAZxPE34mkX+3/JRwSD/pxNt4m/E0i/3/ZKM24TEOJt/E34mlX+3/JRm/CQn2OJgm/E0y/39pKLCQv+OJffE3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")) \ No newline at end of file diff --git a/apps/hebrew_calendar/app.js b/apps/hebrew_calendar/app.js new file mode 100644 index 000000000..9c21fa89b --- /dev/null +++ b/apps/hebrew_calendar/app.js @@ -0,0 +1,17 @@ +!function(){"use strict"; +/*! + * This script was taked from this page and ported to Node.js by Ionic Bizu + * http://www.shamash.org/help/javadate.shtml + * + * This script was adapted from C sources written by + * Scott E. Lee, which contain the following copyright notice: + * + * Copyright 1993-1995, Scott E. Lee, all rights reserved. + * Permission granted to use, copy, modify, distribute and sell so long as + * the above copyright and this permission statement are retained in all + * copies. THERE IS NO WARRANTY - USE AT YOUR OWN RISK. + * + * Bill Hastings + * RBI Software Systems + * bhastings@rbi.com + */var t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};var i=new function(t,i,e,o,r,n,h,a,s,f,u,l,v,c){this[0]=t,this[1]=i,this[2]=e,this[3]=o,this[4]=r,this[5]=n,this[6]=h,this[7]=a,this[8]=s,this[9]=f,this[10]=u,this[11]=l,this[12]=v,this[13]=c}("Tishri","Heshvan","Kislev","Tevet","Shevat","AdarI","AdarII","Nisan","Iyyar","Sivan","Tammuz","Av","Elul"),e=new function(t,i,e,o,r,n,h,a,s,f,u,l,v,c,y,d,m,M,b){this[0]=t,this[1]=i,this[2]=e,this[3]=o,this[4]=r,this[5]=n,this[6]=h,this[7]=a,this[8]=s,this[9]=f,this[10]=u,this[11]=l,this[12]=v,this[13]=c,this[14]=y,this[15]=d,this[16]=m,this[17]=M,this[18]=b}(12,12,13,12,12,13,12,13,12,12,13,12,12,13,12,12,13,12,13);g.clear();let o=new Date,r=function(o){var r,n,h=0,a=0,s=0,f=0,u=0,l=0,v=0;function c(t){var i,o,r,n;for(f=Math.floor((t+310)/6940),r=void 0,n=void 0,r=31524,n=(r+=45971*f)>>16,n+=2744*f,o=Math.floor(n/25920),r=(n-=25920*o)<<16|65535&r,i=Math.floor(r/25920),l=o<<16|i,v=r-=25920*i;lt-74);u++)v+=765433*e[u],l+=Math.floor(v/25920),v%=25920}function y(t,i,e){var o=i,r=o%7;return(e>=19440||!(2==t||5==t||7==t||10==t||13==t||16==t||18==t)&&2==r&&e>=9924||(3==t||6==t||8==t||11==t||14==t||17==t||0==t)&&1==r&&e>=16789)&&(o++,7==++r&&(r=0)),3!=r&&5!=r&&0!=r||o++,o}var d=o;return"object"===(void 0===d?"undefined":t(d))&&(r=o.getMonth()+1,n=o.getDate(),d=o.getFullYear()),function(t){var i,o=0,r=0,n=t-347997;if(c(n),n>=(o=y(u,l,v))){if(s=19*f+u+1,n=o-177)return void(n>o-30?(h=13,a=n-o+30):n>o-60?(h=12,a=n-o+60):n>o-89?(h=11,a=n-o+89):n>o-119?(h=10,a=n-o+119):n>o-148?(h=9,a=n-o+148):(h=8,a=n-o+178));if(13==e[(s-1)%19]){if(h=7,(a=n-o+207)>0)return;if(h--,(a+=30)>0)return;h--,a+=30}else{if(h=6,(a=n-o+207)>0)return;h--,a+=30}if(a>0)return;if(h--,(a+=29)>0)return;r=o,c(l-365),o=y(u,l,v)}if(l=n-o-29,355==(i=r-o)||385==i){if(l<=30)return h=2,void(a=l);l-=30}else{if(l<=29)return h=2,void(a=l);l-=29}h=3,a=l}(function(t,i,e){var o=0,r=0,n=void 0;return o=t<0?t+4801:t+4800,i>2?r=i-3:(r=i+9,o--),n=Math.floor(146097*Math.floor(o/100)/4),n+=Math.floor(o%100*1461/4),n+=Math.floor((153*r+2)/5),n+=e-32045}(d,r,n)),{year:s,month:h,date:a,month_name:i[h-1]}}(o);var n={"":{title:"Hebrew Date"},cal:{value:require("locale").date(o,1),onchange:()=>{}},date:{value:r.date,onchange:()=>{}},month:{value:r.month_name,onchange:()=>{}},year:{value:r.year,onchange:()=>{}}};E.showMenu(n)}(); diff --git a/apps/hebrew_calendar/app.png b/apps/hebrew_calendar/app.png new file mode 100644 index 000000000..0dae731cd Binary files /dev/null and b/apps/hebrew_calendar/app.png differ diff --git a/apps/hebrew_calendar/package.json b/apps/hebrew_calendar/package.json new file mode 100644 index 000000000..85e9ebbf0 --- /dev/null +++ b/apps/hebrew_calendar/package.json @@ -0,0 +1,23 @@ +{ + "name": "hebrew_calendar", + "version": "0.0.3", + "description": "Bangle.js app for seeing hebrew calendar", + "main": "app.js", + "types": "app.d.ts", + "scripts": { + "build": "rollup -c" + }, + "author": { + "name": "Michael Salaverry", + "url": "https://github.com/barakplasma" + }, + "license": "MIT", + "devDependencies": { + "@rollup/plugin-typescript": "^4.1.1", + "rollup": "^2.10.2", + "rollup-plugin-terser": "^5.3.0", + "terser": "^4.7.0", + "tslib": "^2.0.0", + "typescript": "^3.9.2" + } +} diff --git a/apps/hebrew_calendar/rollup.config.mjs b/apps/hebrew_calendar/rollup.config.mjs new file mode 100644 index 000000000..5f7f0746f --- /dev/null +++ b/apps/hebrew_calendar/rollup.config.mjs @@ -0,0 +1,15 @@ +import typescript from '@rollup/plugin-typescript'; +import { terser } from 'rollup-plugin-terser'; + +export default { + input: './src/app.ts', + output: { + dir: '.', + format: 'iife', + name: 'hebrew_calendar' + }, + plugins: [ + typescript(), + terser(), + ] +}; diff --git a/apps/hebrew_calendar/src/app.ts b/apps/hebrew_calendar/src/app.ts new file mode 100644 index 000000000..51314e337 --- /dev/null +++ b/apps/hebrew_calendar/src/app.ts @@ -0,0 +1,34 @@ +declare var Bangle: any; +declare var g: any; +declare var E: any; +declare var require: any; + +g.clear(); + +let now = new Date(); +import { hebrewDate } from "./hebrewDate"; + +let today = hebrewDate(now); + +var mainmenu = { + "" : { + "title" : "Hebrew Date" + }, + cal: { + value: require('locale').date(now,1), + onchange : () => {} + }, + date: { + value : today.date, + onchange : () => {} + }, + month: { + value : today.month_name, + onchange : () => {} + }, + year: { + value : today.year, + onchange : () => {} + } +}; +E.showMenu(mainmenu); diff --git a/apps/hebrew_calendar/src/hebrewDate.ts b/apps/hebrew_calendar/src/hebrewDate.ts new file mode 100644 index 000000000..cc3a5ed78 --- /dev/null +++ b/apps/hebrew_calendar/src/hebrewDate.ts @@ -0,0 +1,358 @@ +/*! + * This script was taked from this page and ported to Node.js by Ionic Bizu + * http://www.shamash.org/help/javadate.shtml + * + * This script was adapted from C sources written by + * Scott E. Lee, which contain the following copyright notice: + * + * Copyright 1993-1995, Scott E. Lee, all rights reserved. + * Permission granted to use, copy, modify, distribute and sell so long as + * the above copyright and this permission statement are retained in all + * copies. THERE IS NO WARRANTY - USE AT YOUR OWN RISK. + * + * Bill Hastings + * RBI Software Systems + * bhastings@rbi.com + */ + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var GREG_SDN_OFFSET = 32045, + DAYS_PER_5_MONTHS = 153, + DAYS_PER_4_YEARS = 1461, + DAYS_PER_400_YEARS = 146097; + +var HALAKIM_PER_HOUR = 1080, + HALAKIM_PER_DAY = 25920, + HALAKIM_PER_LUNAR_CYCLE = 29 * HALAKIM_PER_DAY + 13753, + HALAKIM_PER_METONIC_CYCLE = HALAKIM_PER_LUNAR_CYCLE * (12 * 19 + 7); + +var HEB_SDN_OFFSET = 347997, + NEW_MOON_OF_CREATION = 31524, + NOON = 18 * HALAKIM_PER_HOUR, + AM3_11_20 = 9 * HALAKIM_PER_HOUR + 204, + AM9_32_43 = 15 * HALAKIM_PER_HOUR + 589; + +var SUN = 0, + MON = 1, + TUES = 2, + WED = 3, + THUR = 4, + FRI = 5, + SAT = 6; + +function weekdayarr(d0, d1, d2, d3, d4, d5, d6) { + this[0] = d0; + this[1] = d1; + this[2] = d2; + this[3] = d3; + this[4] = d4; + this[5] = d5; + this[6] = d6; +} + +function gregmontharr(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11) { + this[0] = m0; + this[1] = m1; + this[2] = m2; + this[3] = m3; + this[4] = m4; + this[5] = m5; + this[6] = m6; + this[7] = m7; + this[8] = m8; + this[9] = m9; + this[10] = m10; + this[11] = m11; +} + +function hebrewmontharr(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13?: any) { + this[0] = m0; + this[1] = m1; + this[2] = m2; + this[3] = m3; + this[4] = m4; + this[5] = m5; + this[6] = m6; + this[7] = m7; + this[8] = m8; + this[9] = m9; + this[10] = m10; + this[11] = m11; + this[12] = m12; + this[13] = m13; +} + +function monthsperyeararr(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18) { + this[0] = m0; + this[1] = m1; + this[2] = m2; + this[3] = m3; + this[4] = m4; + this[5] = m5; + this[6] = m6; + this[7] = m7; + this[8] = m8; + this[9] = m9; + this[10] = m10; + this[11] = m11; + this[12] = m12; + this[13] = m13; + this[14] = m14; + this[15] = m15; + this[16] = m16; + this[17] = m17; + this[18] = m18; +} + +var gWeekday = new weekdayarr("Sun", "Mon", "Tues", "Wednes", "Thurs", "Fri", "Satur"), + gMonth = new gregmontharr("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"), + hMonth = new hebrewmontharr("Tishri", "Heshvan", "Kislev", "Tevet", "Shevat", "AdarI", "AdarII", "Nisan", "Iyyar", "Sivan", "Tammuz", "Av", "Elul"), + mpy = new monthsperyeararr(12, 12, 13, 12, 12, 13, 12, 13, 12, 12, 13, 12, 12, 13, 12, 12, 13, 12, 13); + +/** + * hebrewDate + * Convert the Gregorian dates into Hebrew calendar dates. + * + * @name hebrewDate + * @function + * @param {Date|Number} inputDate The date object (representing the Gregorian date) or the year. + * @return {Object} An object containing: + * + * - `year`: The Hebrew year. + * - `month`: The Hebrew month. + * - `month_name`: The Hebrew month name. + * - `date`: The Hebrew date. + */ +export const hebrewDate = function (inputDateOrYear: Date) { + var inputMonth, inputDate; + + var hebrewMonth = 0, + hebrewDate = 0, + hebrewYear = 0, + metonicCycle = 0, + metonicYear = 0, + moladDay = 0, + moladHalakim = 0; + + function GregorianToSdn(inputYear, inputMonth, inputDay) { + + var year = 0, + month = 0, + sdn = void 0; + + // Make year a positive number + if (inputYear < 0) { + year = inputYear + 4801; + } else { + year = inputYear + 4800; + } + + // Adjust the start of the year + if (inputMonth > 2) { + month = inputMonth - 3; + } else { + month = inputMonth + 9; + year--; + } + + sdn = Math.floor(Math.floor(year / 100) * DAYS_PER_400_YEARS / 4); + sdn += Math.floor(year % 100 * DAYS_PER_4_YEARS / 4); + sdn += Math.floor((month * DAYS_PER_5_MONTHS + 2) / 5); + sdn += inputDay - GREG_SDN_OFFSET; + + return sdn; + } + + function SdnToHebrew(sdn) { + var tishri1 = 0, + tishri1After = 0, + yearLength = 0, + inputDay = sdn - HEB_SDN_OFFSET; + + FindTishriMolad(inputDay); + tishri1 = Tishri1(metonicYear, moladDay, moladHalakim); + + if (inputDay >= tishri1) { + // It found Tishri 1 at the start of the year. + hebrewYear = metonicCycle * 19 + metonicYear + 1; + if (inputDay < tishri1 + 59) { + if (inputDay < tishri1 + 30) { + hebrewMonth = 1; + hebrewDate = inputDay - tishri1 + 1; + } else { + hebrewMonth = 2; + hebrewDate = inputDay - tishri1 - 29; + } + return; + } + // We need the length of the year to figure this out,so find Tishri 1 of the next year. + moladHalakim += HALAKIM_PER_LUNAR_CYCLE * mpy[metonicYear]; + moladDay += Math.floor(moladHalakim / HALAKIM_PER_DAY); + moladHalakim = moladHalakim % HALAKIM_PER_DAY; + tishri1After = Tishri1((metonicYear + 1) % 19, moladDay, moladHalakim); + } else { + // It found Tishri 1 at the end of the year. + hebrewYear = metonicCycle * 19 + metonicYear; + if (inputDay >= tishri1 - 177) { + // It is one of the last 6 months of the year. + if (inputDay > tishri1 - 30) { + hebrewMonth = 13; + hebrewDate = inputDay - tishri1 + 30; + } else if (inputDay > tishri1 - 60) { + hebrewMonth = 12; + hebrewDate = inputDay - tishri1 + 60; + } else if (inputDay > tishri1 - 89) { + hebrewMonth = 11; + hebrewDate = inputDay - tishri1 + 89; + } else if (inputDay > tishri1 - 119) { + hebrewMonth = 10; + hebrewDate = inputDay - tishri1 + 119; + } else if (inputDay > tishri1 - 148) { + hebrewMonth = 9; + hebrewDate = inputDay - tishri1 + 148; + } else { + hebrewMonth = 8; + hebrewDate = inputDay - tishri1 + 178; + } + return; + } else { + if (mpy[(hebrewYear - 1) % 19] == 13) { + hebrewMonth = 7; + hebrewDate = inputDay - tishri1 + 207; + if (hebrewDate > 0) return; + hebrewMonth--; + hebrewDate += 30; + if (hebrewDate > 0) return; + hebrewMonth--; + hebrewDate += 30; + } else { + hebrewMonth = 6; + hebrewDate = inputDay - tishri1 + 207; + if (hebrewDate > 0) return; + hebrewMonth--; + hebrewDate += 30; + } + if (hebrewDate > 0) return; + hebrewMonth--; + hebrewDate += 29; + if (hebrewDate > 0) return; + // We need the length of the year to figure this out,so find Tishri 1 of this year. + tishri1After = tishri1; + FindTishriMolad(moladDay - 365); + tishri1 = Tishri1(metonicYear, moladDay, moladHalakim); + } + } + yearLength = tishri1After - tishri1; + moladDay = inputDay - tishri1 - 29; + if (yearLength == 355 || yearLength == 385) { + // Heshvan has 30 days + if (moladDay <= 30) { + hebrewMonth = 2; + hebrewDate = moladDay; + return; + } + moladDay -= 30; + } else { + // Heshvan has 29 days + if (moladDay <= 29) { + hebrewMonth = 2; + hebrewDate = moladDay; + return; + } + moladDay -= 29; + } + // It has to be Kislev. + hebrewMonth = 3; + hebrewDate = moladDay; + } + + function FindTishriMolad(inputDay) { + // Estimate the metonic cycle number. Note that this may be an under + // estimate because there are 6939.6896 days in a metonic cycle not + // 6940,but it will never be an over estimate. The loop below will + // correct for any error in this estimate. + metonicCycle = Math.floor((inputDay + 310) / 6940); + // Calculate the time of the starting molad for this metonic cycle. + MoladOfMetonicCycle(); + // If the above was an under estimate,increment the cycle number until + // the correct one is found. For modern dates this loop is about 98.6% + // likely to not execute,even once,because the above estimate is + // really quite close. + while (moladDay < inputDay - 6940 + 310) { + metonicCycle++; + moladHalakim += HALAKIM_PER_METONIC_CYCLE; + moladDay += Math.floor(moladHalakim / HALAKIM_PER_DAY); + moladHalakim = moladHalakim % HALAKIM_PER_DAY; + } + // Find the molad of Tishri closest to this date. + for (metonicYear = 0; metonicYear < 18; metonicYear++) { + if (moladDay > inputDay - 74) break; + moladHalakim += HALAKIM_PER_LUNAR_CYCLE * mpy[metonicYear]; + moladDay += Math.floor(moladHalakim / HALAKIM_PER_DAY); + moladHalakim = moladHalakim % HALAKIM_PER_DAY; + } + } + + function MoladOfMetonicCycle() { + var r1 = void 0, + r2 = void 0, + d1 = void 0, + d2 = void 0; + // Start with the time of the first molad after creation. + r1 = NEW_MOON_OF_CREATION; + // Calculate gMetonicCycle * HALAKIM_PER_METONIC_CYCLE. The upper 32 + // bits of the result will be in r2 and the lower 16 bits will be in r1. + r1 += metonicCycle * (HALAKIM_PER_METONIC_CYCLE & 0xFFFF); + r2 = r1 >> 16; + r2 += metonicCycle * (HALAKIM_PER_METONIC_CYCLE >> 16 & 0xFFFF); + // Calculate r2r1 / HALAKIM_PER_DAY. The remainder will be in r1,the + // upper 16 bits of the quotient will be in d2 and the lower 16 bits + // will be in d1. + d2 = Math.floor(r2 / HALAKIM_PER_DAY); + r2 -= d2 * HALAKIM_PER_DAY; + r1 = r2 << 16 | r1 & 0xFFFF; + d1 = Math.floor(r1 / HALAKIM_PER_DAY); + r1 -= d1 * HALAKIM_PER_DAY; + moladDay = d2 << 16 | d1; + moladHalakim = r1; + } + + function Tishri1(metonicYear, moladDay, moladHalakim) { + var tishri1 = moladDay, + dow = tishri1 % 7, + leapYear = metonicYear == 2 || metonicYear == 5 || metonicYear == 7 || metonicYear == 10 || metonicYear == 13 || metonicYear == 16 || metonicYear == 18, + lastWasLeapYear = metonicYear == 3 || metonicYear == 6 || metonicYear == 8 || metonicYear == 11 || metonicYear == 14 || metonicYear == 17 || metonicYear == 0; + + // Apply rules 2,3 and 4 + if (moladHalakim >= NOON || !leapYear && dow == TUES && moladHalakim >= AM3_11_20 || lastWasLeapYear && dow == MON && moladHalakim >= AM9_32_43) { + tishri1++; + dow++; + if (dow == 7) dow = 0; + } + + // Apply rule 1 after the others because it can cause an additional delay of one day. + if (dow == WED || dow == FRI || dow == SUN) { + tishri1++; + } + + return tishri1; + } + + var inputYear: Date | number = inputDateOrYear; + + if ((typeof inputYear === "undefined" ? "undefined" : _typeof(inputYear)) === "object") { + inputMonth = inputDateOrYear.getMonth() + 1; + inputDate = inputDateOrYear.getDate(); + inputYear = inputDateOrYear.getFullYear(); + } + + SdnToHebrew(GregorianToSdn(inputYear, inputMonth, inputDate)); + + return { + year: hebrewYear, + month: hebrewMonth, + date: hebrewDate, + month_name: hMonth[hebrewMonth - 1] + }; +}; \ No newline at end of file diff --git a/apps/hebrew_calendar/tsconfig.json b/apps/hebrew_calendar/tsconfig.json new file mode 100644 index 000000000..30a9e35f4 --- /dev/null +++ b/apps/hebrew_calendar/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "es2015", + "noImplicitAny": false, + "target": "es2015" + }, + "include": [ + "src" + ] +} diff --git a/apps/hidmsicswipe/changelog b/apps/hidmsicswipe/changelog new file mode 100644 index 000000000..df3737358 --- /dev/null +++ b/apps/hidmsicswipe/changelog @@ -0,0 +1 @@ +0.01: Core functionnality based entirely on hidmsic diff --git a/apps/hidmsicswipe/hidmsicswipe-icon.js b/apps/hidmsicswipe/hidmsicswipe-icon.js new file mode 100644 index 000000000..6a0c64b9c --- /dev/null +++ b/apps/hidmsicswipe/hidmsicswipe-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A5xGICquZzAVUAAIXQCogXQCoxHPCox0BxIXNxIVFBAQXPUAwXPBw4XowAvuC/4X/C9sIC6kIxGZzIXSFgIWBC6QWEC6RECAAOJwAXQFwoXLxAqBC4MICweZCxhWEC4mICxxuDA4I3BCxQ/FQxpyEK6AucC4idMI5OICyQwBQpgA/AH4Au")) diff --git a/apps/hidmsicswipe/hidmsicswipe.js b/apps/hidmsicswipe/hidmsicswipe.js new file mode 100644 index 000000000..e0fc760a4 --- /dev/null +++ b/apps/hidmsicswipe/hidmsicswipe.js @@ -0,0 +1,93 @@ +var storage = require('Storage'); + +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); + }); +} + +function drawApp() { + g.clear(); + if(Bangle.isLocked()==false) E.showMessage('Swipe', 'Music'); + else E.showMessage('Locked', 'Music'); +} + +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){ + E.showMessage('down'); + setTimeout(drawApp, 1000); + down(() => {}); + } + else if(lasty < -40){ + E.showMessage('up'); + setTimeout(drawApp, 1000); + up(() => {}); + } else if(lastx < -40){ + E.showMessage('prev'); + setTimeout(drawApp, 1000); + prev(() => {}); + } else if(lastx > 40){ + E.showMessage('next'); + setTimeout(drawApp, 1000); + next(() => {}); + } else if(lastx==0 && lasty==0){ + E.showMessage('play/pause'); + setTimeout(drawApp, 1000); + toggle(() => {}); + } + lastx = 0; + lasty = 0; + } + else{ + lastx = lastx + e.dx; + lasty = lasty + e.dy; + } + }); + + Bangle.on("lock", function(on) { + if(!on){ + E.showMessage('unlock'); + setTimeout(drawApp, 1000); + } + }); + + drawApp(); +} diff --git a/apps/hidmsicswipe/hidmsicswipe.png b/apps/hidmsicswipe/hidmsicswipe.png new file mode 100644 index 000000000..923b5aa0e Binary files /dev/null and b/apps/hidmsicswipe/hidmsicswipe.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). + +![Set Active Time](images/set-active.png) + +Next set the rest time (i.e. number of seconds to rest between exercises). + +![Set Rest Time](images/set-rest.png) + +Finally choose the number of sets to perform. + +![Set Number Sets](images/set-sets.png) + +Active time will be shown in red, rest time in green. The watch will buzz whenever active or rest time gets to 0. + +![Timer (active)](images/timer1.png) +![Timer (rest)](images/timer2.png) + +You can press the physical button during timer countdown to pause the timer. + +![Paused](images/pause.png) + +View after all sets are completed. Press menu to change settings or restart to start timer again with the same settings. + +![Completed view](images/done.png) + +## 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..dd8a3549b 100644 --- a/apps/ios/ChangeLog +++ b/apps/ios/ChangeLog @@ -1 +1,4 @@ 0.01: New App! +0.02: Remove messages on disconnect +0.03: Handling of message actions (ok/clear) +0.04: Added common bundleId's diff --git a/apps/ios/boot.js b/apps/ios/boot.js index c3ccb9275..875f00067 100644 --- a/apps/ios/boot.js +++ b/apps/ios/boot.js @@ -54,11 +54,26 @@ E.on('notify',msg=>{ "name" : string, */ var appNames = { - "com.netflix.Netflix" : "Netflix", - "com.google.ios.youtube" : "YouTube", + "nl.ah.Appie": "Albert Heijn", + "com.apple.mobilecal": "Calendar", + "com.apple.mobilemail": "Mail", + "com.apple.reminders": "Reminders", + "com.apple.shortcuts": "Shortcuts", + "com.atebits.Tweetie2": "Twitter", + "com.burbn.instagram" : "Instagram", + "com.facebook.Facebook": "Facebook", + "com.facebook.Messenger": "FB Messenger", + "com.google.Gmail" : "GMail", "com.google.hangouts" : "Hangouts", + "com.google.ios.youtube" : "YouTube", + "com.jumbo.app" : "Jumbo", + "com.netflix.Netflix" : "Netflix", + "com.skype.skype": "Skype", "com.skype.SkypeForiPad": "Skype", - "com.atebits.Tweetie2": "Twitter" + "com.spotify.client": "Spotify", + "net.whatsapp.WhatsApp": "WhatsApp", + "com.wordfeud.free": "WordFeud", + // could also use NRF.ancsGetAppInfo(msg.appId) here }; var unicodeRemap = { @@ -95,7 +110,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 bd8a9bd03..0b2f134ad 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -6,3 +6,6 @@ 0.06: Use Bangle.setUI for buttons 0.07: Theme colours fix 0.08: Merge Bangle.js 1 and 2 launchers +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 +0.10: added in selectable font in settings including scalable vector font 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 8b66247c5..156eecdf4 100644 --- a/apps/launch/app-bangle2.js +++ b/apps/launch/app-bangle2.js @@ -1,4 +1,22 @@ var s = require("Storage"); +let fonts = g.getFonts(); +var scaleval = 1; +var vectorval = 20; +var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; +let settings = require('Storage').readJSON("launch.json", true) || {}; +if ("vectorsize" in settings) { + vectorval = parseInt(settings.vectorsize); +} +if ("font" in settings){ + if(settings.font == "Vector"){ + scaleval = vectorval/20; + font = "Vector"+(vectorval).toString(); + } + else{ + font = settings.font; + scaleval = (font.split('x')[1])/20; + } +} var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); apps.sort((a,b)=>{ var n=(0|a.sortorder)-(0|b.sortorder); @@ -11,8 +29,6 @@ apps.forEach(app=>{ if (app.icon) app.icon = s.read(app.icon); // should just be a link to a memory area }); -// FIXME: not needed after 2v11 -var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; // FIXME: check not needed after 2v11 if (g.wrapString) { g.setFont(font); @@ -22,9 +38,9 @@ if (g.wrapString) { function drawApp(i, r) { var app = apps[i]; if (!app) return; - g.clearRect(r.x,r.y,r.x+r.w-1, r.y+r.h-1); - g.setFont(font).setFontAlign(-1,0).drawString(app.name,64,r.y+32); - if (app.icon) try {g.drawImage(app.icon,8,r.y+8);} catch(e){} + g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1)); + g.setFont(font).setFontAlign(-1,0).drawString(app.name,64*scaleval,r.y+(32*scaleval)); + if (app.icon) try {g.drawImage(app.icon,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){} } g.clear(); @@ -32,7 +48,7 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); E.showScroller({ - h : 64, c : apps.length, + h : 64*scaleval, c : apps.length, draw : drawApp, select : i => { var app = apps[i]; @@ -46,3 +62,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/launch/settings.js b/apps/launch/settings.js new file mode 100644 index 000000000..8be1adb36 --- /dev/null +++ b/apps/launch/settings.js @@ -0,0 +1,25 @@ +// make sure to enclose the function in parentheses +(function(back) { + let settings = require('Storage').readJSON('launch.json',1)||{}; + let fonts = g.getFonts(); + function save(key, value) { + settings[key] = value; + require('Storage').write('launch.json',settings); + } + const appMenu = { + '': {'title': 'Launcher Settings'}, + '< Back': back, + 'Font': { + value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"), + min:0, max:fonts.length-1, step:1,wrap:true, + onchange: (m) => {save('font', fonts[m])}, + format: v => fonts[v] + }, + 'Vector font size': { + value: settings.vectorsize || 10, + min:10, max: 20,step:1,wrap:true, + onchange: (m) => {save('vectorsize', m)} + } + }; + E.showMenu(appMenu); +}); 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 c7ec09d30..85bcbad36 100644 --- a/apps/lcars/ChangeLog +++ b/apps/lcars/ChangeLog @@ -1 +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 fdce30c1b..3acaacb4d 100644 --- a/apps/lcars/README.md +++ b/apps/lcars/README.md @@ -1,8 +1,19 @@ # LCARS clock -A simple LCARS inspired clock that shows: - * Current time - * Current date - * Battery level - * Steps +A simple LCARS inspired clock. +Note: To display the steps, its necessary to install +the [Pedometer widget](https://banglejs.com/apps/#pedometer%20widget). +## Features + * Shows the time + * Shows the date + * Shows the current battery level in % + * Shows the number of daily steps + * Swipe left/right to activate an alarm + +## Icons +
Icons made by Smashicons, Freepik from www.flaticon.com
+ + +## 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 cf884a6b7..9b7244ece 100644 --- a/apps/lcars/lcars.app.js +++ b/apps/lcars/lcars.app.js @@ -1,28 +1,89 @@ +const filename = "lcars.setting.json"; +const Storage = require("Storage"); +let settings = Storage.readJSON(filename,1) || { + alarm: -1, +}; + +/* + * Requirements and globals + */ const locale = require('locale'); - -/* - * Assets: Images, fonts etc. - */ -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.setFontMinaSmall = 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("AAAAAAAAAAAAAAAAAAAAA/8w/8wAAQAAAAAA4AA8AAAAA8AAwAAAAAAEABEABEQB/w/8AxEABEwB/w/8AxEABEABEAAAAH4MP8MMEM8GPcGOMGMMGMMH4ABwAAA/gAwgAggAggQwhw/nAAOAA4ADgAOfw8YwwQwAQQAYwAfwAPgAGAAfg85w/wwzgww4wwdgwHggHgAPwAIQAAA8AAwAAAAAAfwH/+fAP4ABgAAgAA4ABfAPH/+A/wAAAAAAEAAHgAfAAfAAHgAMAAAAAAAABgABgABgAf4AP4ABgABgABAAAAAADAADwADAAAAAgAAwAAwAAwAAwAAwAAAAADAADAADAAAAAAEAA8AH4A+AHwA+AAwAAAAAf/g//wwAwwAwwAwwAwwAwwAwf/gH+AAAAAAAYAAwAAwAA//wAAAAAAAAAwAwwBwwDwwDwwGwwcww4wfwwPAwAAAAAAwAwwQwwQwwQwwQwwYww4wf/gHHAAAAAEAAeAA+ADmAPGAcGAwGAh/wD/wAEAAEAAAAf4w/4wwwwwwwwwwwwwwwww/wgfgAAAAAAP/Af/gwwwwwwwwwwwwwwwwwww/gAAAgAAwAAwAAwAwwHww/Az4A/AA8AAAAAAAAfPg//wxwwwwwwwwwwwwww//wffgAAAAAAfwQ/wwwQwwYwwQwwQwwQw//wP/AAAAAAAMDAMDAMDAAAAAAAMDAMDwMDAAAAAAADgADgAHwAGwAMYAMYAIIAAAAEQAGYAGYAGYAGYAGYAGYAGYAAAAMYAMYAGwAGwAHgADgADAAAAAwAAwAAwAAwcwwcwwQAwQA/wAfgAAAAAAAB/8D/+TAGbHjbPzbMTbMzbMzb/zZ/zYAGf/+H/8AAAAAAABwAPwA+AH+A+GA8GAfmAD+AAfgADwAAQAAA//w//wwwwwwwwwwwwwxww//wffgAAAAAAP/Af/gwBwwAwwAwwAwwAwwAwwAwAAA//w//wwAwwAwwAwwAwwAw4Bwf/gH+AAAAAAAf/g//wwQwgQQgQQgQQgQQgQQgAQAAAf/w//wwQAgQAgQAgQAgQAgQAgAAAAAP/Af/gwAwwAwwAwwYwwYwwfwwfwAAAAAA//w//wAYAAYAAYAAYAAYAAYA//w//wAAA//w//wAAAAAAAAwAAwAAw//w//AAAA//w//wAYAA4AD8AHHAeDg4AwgAQAAAAAA//g//wAAwAAwAAwAAwAAwAAwAAQAAAP/w//w+AAPwAB+AAHwADwA/gH4A/AA/4A//wAAwAAA//w//wcAAPAADgAA4AAeAAHAADw//wAAAAAAH/Af/g4AwwAwwAwwAwwAwwAwcDwP/gB4AAAA//w//wwQAwQAwQAwYAwwA/wAPgAAAAH/Af/gwAwwAwwAwwA8wA8wA2cDkP/gB4AAAA//w//wwYAwYAwYAwcAwfA/zwPgwAAAAAAfgA/wwwQwwYwwYwwYwwYwwfwAPgAAAAAAwAAwAAwAA//w//wwAAwAAwAAwAAAAA/+A//gABwAAwAAwAAwAAwAAwAPg//AAAAAAA4AA/AAH4AA/AAHwADwAfgD8AfgA8AAgAAAAA4AA/AAH4AA/AAHwAHwA/AP4A/4Aw/AAHwAHwA/AP4A+AAwAAAAAwAw8DwOHAD8AB4AD8AOHA8DwwAwAAAAAAwAA8AAPAADwAA/wB/wHgAeAA4AAgAAgAQwBwwHwwOww8wxww3gw+Aw4AwwAQAAAH//f//YAAYAAQAAwAA+AAPwAB+AAPwAB8AAMQAAYAAYAAf//AAAAAA"), 32, atob("BgUHDAoRCwMGBggJBQYFBwwHCwsLCwsKCwsFBQkICQoPDAsKDAoKCwsEBgsKDgwMCgwLCwoMDBELCwoGBwY="), 18+(scale<<8)+(1<<16)); + 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)); } -Graphics.prototype.setFontMinaLarge = function(scale) { - // Actual height 35 (34 - 0) - g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAB4AAAAAPgAAAAA+AAAAAD4AAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAH4AAAAD/gAAAB/8AAAA/+AAAAf/AAAAP/gAAAH/wAAAD/4AAAD/8AAAB/+AAAAP/AAAAA/AAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///4AA////wAH////gA+AAAfADwAAA8AOAAABwA4AAAHADgAAAcAOAAABwA4AAAHADgAAAcAOAAABwA4AAAHADgAAAcAOAAABwA8AAAPAD4AAB8AH////gAP///8AAf///gAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAcAAAAADgAAAAAOAAAAAB4AAAAAHAAAAAA8AAAAAD////8AP////wA/////AD////8AAAAAAAAAAAAAAAAAAAAAGAAAAAA4AAAHADgAAA8AOAAAHwA4AAA/ADgAAD8AOAAAfwA4AAD/ADgAAfcAOAAD5wA4AAfHADgAD4cAOAAfBwA4AD8HADwAfgcAPAD8BwAeA/AHAB+f4AcAD//ABwAH/wAHAAH8AAcAAAAAAAAAAAAAAAAAAAAAGAAABgAYAAAGADgAAAcAOAHABwA4AcAHADgBwAcAOAHABwA4AcAHADgBwAcAOAHABwA4AcAHADgBwAcAOAHABwA4AcAHADwB4A8APAPgDwAfD//+AB////4AD/8//AAD/B/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAA8AAAAAPwAAAAD/AAAAAf8AAAAH9wAAAB/HAAAAPwcAAAD+BwAAAfgHAAAH8AcAAB/ABwAAPwAHAAA+AAcAADgABwAAIAAHgAAAH///AAB///8AAH///wAAAAcAAAAABwAAAAAHAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAH/8AYAP//wBgA///AHAD//wAcAOAPABwA4A4AHADgDgAcAOAOABwA4A4AHADgDgAcAOAOABwA4A4AHADgDgA8AOAOADwA4A8APADgD8H4AOAH//gA4AP/8AAAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAD//+AAB///+AAP///8AB/BgH4APgOAHwA8A4APADgDgAcAOAOABwA4A4AHADgDgAcAOAOABwA4A4AHADgDgAcAOAOABwA4A4AHADgDwA8AOAP//wA4Af/+ABgA//wAAAA/8AAAAAAAAAAAAAAAAAAAAAA4AAAAADgAAAAAOAAAAAA4AAAAADgAAAAAOAAAAQA4AAAHADgAAD8AOAAA/wA4AAf+ADgAP/gAOAD/wAA4B/8AADg/+AAAOP/AAAA//wAAAD/4AAAAP8AAAAA/AAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/h/4AA//P/wAH////gA+B/AfADwD4A8AOAHABwA4AcAHADgBwAcAOAHABwA4AcAHADgBwAcAOAHABwA4AcAHADgBwAcAPAPgDwA8A+APAB////4AH////gAP/j/8AAD4B8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAA//gAYAH//ABwA//+AHADwB4AcAOADgBwA4AOAHADgA4AcAOADgBwA4AOAHADgA4AcAOADgBwA4AOAHADgA4AcAPADgDwA+AOAfAB+A4f4AD////AAH///4AAD//8AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAeAAB8AD4AAHwAPgAAfAA+AAB4AB4AAAAAAAAAAAAAAAAAAAAAA="), 46, atob("CxAaDhgYGBgZFhkZCw=="), 40+(scale<<8)+(1<<16)); +Graphics.prototype.setFontAntonioLarge = function(scale) { + // Actual height 34 (34 - 1) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAADwAAAAAeAAAAADwAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAD+AAAAH/wAAAP/+AAAf/+AAA//8AAB//4AAD//wAAD//gAAAf/AAAAD+AAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAB////gA/////AP////8D/////wfAAAA+DwAAADweAAAAeDwAAADwf////+D/////wP////8Af///+AAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAOAAAAADwAAAAAeAAAAAHgAAAAB/////wf////+D/////wf////+D/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAPwH/4AP+B//AH/wf/4D/+D4AB/9weAAf4ODwAP8BweAP/AOD///gBwP//wAOA//4ABwB/4AAOAAAAAAAAAAAAAAAAAAAAB8AA/gA/gAH/AP8AA/8D/gAH/wfAHAA+DwA4ADweAHgAeDwB8ADwf7/+H+D/////gP/9//8A//H/+AA/AH/AAAAAAAAAAAAAAAAAABwAAAAD+AAAAD/wAAAH/+AAAH/5wAAH/wOAAP/gBwAP/gAOAD/////wf////+D/////wf////+AAAABwAAAAAOAAAAABwAAAAAAAAAAAAAAAAAAeAD//4D/Af//Af8D//4D/wf//Af+DwPAADweB4AAeDwPAADweB///+DwP///weA///8DwD//+AAAA/8AAAAAAAAAAAAAAAAAAAAAA////AA/////AP////8D/////wfgPAB+DwB4ADweAOAAeDwBwADwf+PAA+D/x///wP+H//8A/wf//AAAA//gAAAAAAAAAAAAADgAAAAAeAAAAADwAAAAAeAAAD+DwAAP/weAA//+DwA///weB///8Dx//8AAf//wAAD//gAAAf/AAAAD/AAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAD/wf/wB//v//AP////8D/////weAPwAeDwA8ADwcAHAAeDwB8ADwf////+D/////wP/9//8A//H//AA/AD/AAAAAAAAAAAAAAAAAAAAAD//gfAA///D/AP//8f8D///j/weAA8A+DwADgDweAAcAeDwAHgDwf////+B/////gP////8Af///+AAP//4AAAAAAAAAAAAAAAAAAAAAAD4AfAAAfAD4AAD4AfAAAfAD4AAD4AfAAAAAAAAAAAAAA=="), 46, atob("Cg4QEBAQEBAQEBAQCQ=="), 39+(scale<<8)+(1<<16)); } - /* - * Queue drawing every minute + * Draw watch face */ var drawTimeout; function queueDraw() { @@ -34,54 +95,168 @@ function queueDraw() { } -/* - * Draw watch face - */ 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.setFontMinaLarge(); - g.drawString(timeStr, 115, 53); + g.setFontAntonioLarge(); + g.drawString(timeStr, 60, 55); // Write date - g.setFontAlign(-1,-1,0); - g.setFontMinaSmall(); + g.setFontAlign(-1,-1, 0); + g.setFontAntonioSmall(); var dayName = locale.dow(currentDate, true).toUpperCase(); var day = currentDate.getDate(); - g.drawString("DATE:", 40, 107); - g.drawString(dayName + " " + day, 100, 105); + g.drawString(day, 100, 35); + g.drawString(dayName, 100, 55); // Draw battery - var bat = E.getBattery(); - g.drawString("BAT:", 40, 127); - g.drawString(bat+"%", 100, 127); + g.drawString("BAT:", 25, 98); + g.drawString(bat+ "%", 62, 98); // Draw steps - var steps = Bangle.getStepCount(); - g.drawString("STEP:", 40, 147); - g.drawString(steps, 100, 147); + var steps = getSteps(); + g.drawString("STEP:", 25, 121); + g.drawString(steps, 62, 121); + + // 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 queueDraw(); } -// Clear the screen once, at startup -g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); +/* + * Step counter via widget + */ +function getSteps() { + if (stepsWidget() !== undefined) + return stepsWidget().getSteps(); + return "???"; +} -// draw immediately at first, queue update -draw(); +function stepsWidget() { + if (WIDGETS.activepedom !== undefined) { + return WIDGETS.activepedom; + } else if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom; + } + return undefined; +} -// Stop updates when LCD is off, restart when on +/* + * Handle alarm + */ +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; + } + + if(getAlarmMinutes() > 0){ + return; + } + + // 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)); + + // Update alarm state to disabled + settings.alarm = -1; + Storage.writeJSON(filename, settings); +} + + +/* + * Swipe to set an alarm + */ +Bangle.on('swipe',function(dir) { + // Increase alarm + if(dir == -1){ + if(isAlarmEnabled()){ + settings.alarm += 5; + } else { + settings.alarm = getCurrentTimeInMinutes() + 5; + } + } + + // Decrease alarm + if(dir == +1){ + if(isAlarmEnabled() && (settings.alarm-5 > getCurrentTimeInMinutes())){ + settings.alarm -= 5; + } else { + settings.alarm = -1; + } + } + + // Update UI + draw(); + + // Update alarm state + Storage.writeJSON(filename, settings); +}); + + +/* + * Stop updates when LCD is off, restart when on + */ Bangle.on('lcdPower',on=>{ if (on) { draw(); // draw immediately, queue redraw @@ -94,6 +269,12 @@ Bangle.on('lcdPower',on=>{ // Show launcher when middle button pressed Bangle.setUI("clock"); -// Load widgets +// Load widgets - needed by draw Bangle.loadWidgets(); + +// Clear the screen once, at startup and draw clock +g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); +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 new file mode 100644 index 000000000..70db639eb Binary files /dev/null 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..ec74955e9 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -9,3 +9,5 @@ 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 +0.11: Added translations for nl_NL and changes one formatting 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/locale/locales.js b/apps/locale/locales.js index 9e2624b77..e076b70bd 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -184,12 +184,12 @@ var locales = { temperature: "°C", ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, - datePattern: { 0: "%A %B %d %Y", 1: "%d.%m.%y" }, // zondag 1 maart 2020 // 01.01.20 + datePattern: { 0: "%B %d %Y", 1: "%d.%m.%y" }, // zondag 1 maart 2020 // 01.01.20 abday: "zo,ma,di,wo,do,vr,za", day: "zondag,maandag,dinsdag,woensdag,donderdag,vrijdag,zaterdag", abmonth: "jan,feb,mrt,apr,mei,jun,jul,aug,sep,okt,nov,dec", month: "januari,februari,maart,april,mei,juni,juli,augustus,september,oktober,november,december", - // No translation for english... + trans: { yes: "ja", Yes: "Ja", no: "nee", No: "Nee", ok: "ok", on: "aan", off: "uit", "< Back": "< Terug" } }, "en_CA": { lang: "en_CA", diff --git a/apps/magnav/ChangeLog b/apps/magnav/ChangeLog index 35e8798c6..2b2782c7b 100644 --- a/apps/magnav/ChangeLog +++ b/apps/magnav/ChangeLog @@ -2,5 +2,6 @@ 0.02: Course marker 0.03: Tilt compensation and calibration 0.04: Fix Font size +0.05: Inital portable version diff --git a/apps/magnav/README.md b/apps/magnav/README.md index a036644fb..7ef506b2e 100644 --- a/apps/magnav/README.md +++ b/apps/magnav/README.md @@ -6,19 +6,20 @@ This is a tilt and roll compensated compass with a linear display. The compass w ## Calibration -Correct operation of this app depends critically on calibration. When first run on a Bangle, the app will request calibration. This lasts for 30 seconds during which you should move the watch slowly through figures of 8. It is important that during calibration the watch is fully rotated around each of it axes. If the app does give the correct direction heading or is not stable with respect to tilt and roll - redo the calibration by pressing *BTN3*. Calibration data is recorded in a storage file named `magnav.json`. +Correct operation of this app depends critically on calibration. When first run on a Bangle, the app will request calibration. This lasts for 20 seconds during which you should move the watch slowly through figures of 8. It is important that during calibration the watch is fully rotated around each of it axes. If the app does give the correct direction heading or is not stable with respect to tilt and roll - redo the calibration by pressing *BTN2*. Calibration data is recorded in a storage file named `magnav.json`. + +Note: Charging your Bangle due to the magnetic connector clamp seems to require recalibration afterwards for accurate readings. ## Controls -*BTN1* - switches to your selected clock app. +*BTN1* - marks the current heading with a blue circle - see screen shot. This can be used to take a bearing and then follow it.. +(Swipe UP on Bangle 2) -*BTN2* - switches to the app launcher. +*BTN2* - invokes calibration ( can be cancelled if pressed accidentally). +(*BTN1* on Bangle 2) -*BTN3* - invokes calibration ( can be cancelled if pressed accidentally) - -*Touch Left* - marks the current heading with a blue circle - see screen shot. This can be used to take a bearing and then follow it. - -*Touch Right* - cancels the marker (blue circle not displayed). +*BTN3* - cancels the marker (blue circle not displayed) +(swipe DOWN on Bangle 2) ## Support diff --git a/apps/magnav/magnav.min.js b/apps/magnav/magnav.min.js deleted file mode 100644 index 1d5439164..000000000 --- a/apps/magnav/magnav.min.js +++ /dev/null @@ -1,10 +0,0 @@ -var Yoff=80,pal2color=new Uint16Array([0,65535,2047,50712],0,2),buf=Graphics.createArrayBuffer(240,60,2,{msb:!0});Bangle.setLCDTimeout(30);function flip(b,c){g.drawImage({width:240,height:60,bpp:2,buffer:b.buffer,palette:pal2color},0,c);b.clear()}var labels="N NE E SE S SW W NW".split(" "),brg=null; -function drawCompass(b){buf.setColor(1);buf.setFont("Vector",24);var c=b-90;0>c&&(c+=360);buf.fillRect(28,45,212,49);var a=30,d=15-c%15;15>d?a+=d:d=0;for(var e=d;e<=180-d;e+=15){var f=c+e;0==f%90?(buf.drawString(labels[Math.floor(f/45)%8],a-8,0),buf.fillRect(a-2,25,a+2,45)):0==f%45?(buf.drawString(labels[Math.floor(f/45)%8],a-12,0),buf.fillRect(a-2,30,a+2,45)):0==f%15&&buf.fillRect(a,35,a+1,45);a+=15}brg&&(b=brg-b,180b&&(b+=360),b+=120,30>b&&(b=14),210c?1:-1;180<=a&&(a=360-a,d=-d);if(2>a)return c;a=c+d*(1+Math.round(a/5));0>a&&(a+=360);360a&&(a+=360);return a} -function reading(){var b=tiltfixread(CALIBDATA.offset,CALIBDATA.scale);heading=newHeading(b,heading);drawCompass(heading);buf.setColor(1);buf.setFont("6x8",2);buf.setFontAlign(-1,-1);buf.drawString("o",170,0);buf.setFont("Vector",54);b=Math.round(heading);var c=b.toString();buf.drawString(10>b?"00"+c:100>b?"0"+c:c,70,10);flip(buf,Yoff+80)} -function calibrate(){var b=-32E3,c=-32E3,a=-32E3,d=32E3,e=32E3,f=32E3,k=setInterval(function(){var h=Bangle.getCompass();b=h.x>b?h.x:b;c=h.y>c?h.y:c;a=h.z>a?h.z:a;d=h.x{ + CALIBDATA=r; require("Storage").write("magnav.json",r); - CALIBDATA = r; - startdraw(); - setButtons(); + restart() }); } else { - startdraw(); - setTimeout(setButtons,1000); - } + restart() + } } if (first===undefined) first=false; stopdraw(); - clearWatch(); if (first) E.showAlert(msg,title).then(action.bind(null,true)); else E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action); } -Bangle.on('touch', function(b) { - if(!candraw) return; - if(b==1) brg=heading; - if(b==2) brg=null; - }); - var intervalRef; function startdraw(){ @@ -176,29 +174,17 @@ function stopdraw() { } function setButtons(){ - setWatch(()=>{load();}, BTN1, {repeat:false,edge:"falling"}); - setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); - setWatch(docalibrate, BTN3, {repeat:false,edge:"falling"}); + function actions(v){ + if (!v) docalibrate(false); + else if (v==1) brg=null; + else brg=heading; + } + Bangle.setUI("updown",actions); } -var SCREENACCESS = { - withApp:true, - request:function(){ - this.withApp=false; - stopdraw(); - clearWatch(); - }, - release:function(){ - this.withApp=true; - startdraw(); - setButtons(); - } -}; - Bangle.on('lcdPower',function(on) { - if (!SCREENACCESS.withApp) return; if (on) { - startdraw(); + if (!calibrating) startdraw(); } else { stopdraw(); } @@ -209,7 +195,7 @@ Bangle.on('kill',()=>{Bangle.setCompassPower(0);}); Bangle.loadWidgets(); Bangle.setCompassPower(1); if (!CALIBDATA) - docalibrate({},true); + docalibrate(true); else { startdraw(); setButtons(); @@ -217,4 +203,3 @@ else { - diff --git a/apps/magnav/magnav_b2.js b/apps/magnav/magnav_b2.js new file mode 100644 index 000000000..e54280796 --- /dev/null +++ b/apps/magnav/magnav_b2.js @@ -0,0 +1,192 @@ + +const Ypos = 40; + +const labels = ["N","NE","E","SE","S","SW","W","NW"]; +var brg=null; + +function drawCompass(course) { + "ram" + g.setColor(g.theme.fg); + g.setFont("Vector",18); + var start = course-90; + if (start<0) start+=360; + g.fillRect(16,Ypos+45,160,Ypos+49); + var xpos = 16; + var frag = 15 - start%15; + if (frag<15) xpos+=Math.floor((frag*4)/5); else frag = 0; + for (var i=frag;i<=180-frag;i+=15){ + var res = start + i; + if (res%90==0) { + g.drawString(labels[Math.floor(res/45)%8],xpos-6,Ypos+6); + g.fillRect(xpos-2,Ypos+25,xpos+2,Ypos+45); + } else if (res%45==0) { + g.drawString(labels[Math.floor(res/45)%8],xpos-9,Ypos+6); + g.fillRect(xpos-2,Ypos+30,xpos+2,Ypos+45); + } else if (res%15==0) { + g.fillRect(xpos,Ypos+35,xpos+1,Ypos+45); + } + xpos+=12; + } + if (brg) { + var bpos = brg - course; + if (bpos>180) bpos -=360; + if (bpos<-180) bpos +=360; + bpos= Math.floor((bpos*4)/5)+88; + if (bpos<16) bpos = 8; + if (bpos>160) bpos = 170; + g.setColor(g.theme.fg2); + g.fillCircle(bpos,Ypos+45,6); + } +} + +var heading = 0; +function newHeading(m,h){ + var s = Math.abs(m - h); + var delta = (m>h)?1:-1; + if (s>=180){s=360-s; delta = -delta;} + if (s<2) return h; + var hd = h + delta*(1 + Math.round(s/5)); + if (hd<0) hd+=360; + if (hd>360)hd-= 360; + return hd; +} + +var candraw = false; +var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; + +function tiltfixread(O,S){ + "ram" + var m = Bangle.getCompass(); + var g = Bangle.getAccel(); + m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z; + var d = Math.atan2(-m.dx,m.dy)*180/Math.PI; + if (d<0) d+=360; + var phi = Math.atan(-g.x/-g.z); + var cosphi = Math.cos(phi), sinphi = Math.sin(phi); + var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi)); + var costheta = Math.cos(theta), sintheta = Math.sin(theta); + var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta; + var yh = m.dz*sinphi - m.dx*cosphi; + var psi = Math.atan2(yh,xh)*180/Math.PI; + if (psi<0) psi+=360; + return psi; +} + +// Note actual mag is 360-m, error in firmware +function reading() { + "ram" + g.clearRect(0,24,175,175); + var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); + heading = newHeading(d,heading); + drawCompass(heading); + g.setColor(g.theme.fg); + g.setFont("6x8",2); + g.setFontAlign(-1,-1); + g.drawString("o",120,Ypos+80); + g.setFont("Vector",40); + var course = Math.round(heading); + var cs = course.toString(); + cs = course<10?"00"+cs : course<100 ?"0"+cs : cs; + g.drawString(cs,50,Ypos+90); + g.setColor(g.theme.fg2); + g.fillPoly([88,Ypos+60,78,Ypos+80,98,Ypos+80]); + g.setColor(g.theme.fg); + g.flip(); +} + +function calibrate(){ + var max={x:-32000, y:-32000, z:-32000}, + min={x:32000, y:32000, z:32000}; + var ref = setInterval(()=>{ + var m = Bangle.getCompass(); + max.x = m.x>max.x?m.x:max.x; + max.y = m.y>max.y?m.y:max.y; + max.z = m.z>max.z?m.z:max.z; + min.x = m.x { + setTimeout(()=>{ + if(ref) clearInterval(ref); + var offset = {x:(max.x+min.x)/2,y:(max.y+min.y)/2,z:(max.z+min.z)/2}; + var delta = {x:(max.x-min.x)/2,y:(max.y-min.y)/2,z:(max.z-min.z)/2}; + var avg = (delta.x+delta.y+delta.z)/3; + var scale = {x:avg/delta.x, y:avg/delta.y, z:avg/delta.z}; + resolve({offset:offset,scale:scale}); + },20000); + }); +} + +var calibrating=false; +function docalibrate(first){ + calibrating=true; + const title = "Calibrate"; + const msg = "takes 20 seconds"; + function restart() { + calibrating=false; + setButtons(); + startdraw(); + } + function action(b){ + if (b) { + g.clearRect(0,24,175,175); + g.setColor(g.theme.fg); + g.setFont("Vector",18); + g.setFontAlign(0,-1); + g.drawString("Fig 8s to",88,Ypos); + g.drawString("Calibrate",88,Ypos+18); + g.flip(); + calibrate().then((r)=>{ + CALIBDATA=r; + require("Storage").write("magnav.json",r); + restart(); + }); + } else { + restart(); + } + } + if (first===undefined) first=false; + stopdraw(); + if (first) + E.showAlert(msg,title).then(action.bind(null,true)); + else + E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action); +} + +var intervalRef; + +function startdraw(){ + g.clear(1); + Bangle.drawWidgets(); + candraw = true; + intervalRef = setInterval(reading,200); +} + +function stopdraw() { + candraw=false; + if(intervalRef) {clearInterval(intervalRef);} +} + +function setButtons(){ + function actions(v){ + if (!v) docalibrate(false); + else if (v==1) brg=null; + else brg=heading; + } + Bangle.setUI("updown",actions); +} + +Bangle.on('kill',()=>{Bangle.setCompassPower(0);}); + +Bangle.loadWidgets(); +Bangle.setCompassPower(1); +if (!CALIBDATA) + docalibrate(true); +else { + startdraw(); + setButtons(); +} + + + diff --git a/apps/magnav/screenshot-b2.png b/apps/magnav/screenshot-b2.png new file mode 100644 index 000000000..63f830bfc Binary files /dev/null and b/apps/magnav/screenshot-b2.png differ diff --git a/apps/magnav/screenshot-light-b2.png b/apps/magnav/screenshot-light-b2.png new file mode 100644 index 000000000..943dc392c Binary files /dev/null and b/apps/magnav/screenshot-light-b2.png differ diff --git a/apps/mandelbrotclock/ChangeLog b/apps/mandelbrotclock/ChangeLog new file mode 100644 index 000000000..d7bda0d78 --- /dev/null +++ b/apps/mandelbrotclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial Release + \ No newline at end of file diff --git a/apps/mandelbrotclock/README.md b/apps/mandelbrotclock/README.md new file mode 100644 index 000000000..387343a9e --- /dev/null +++ b/apps/mandelbrotclock/README.md @@ -0,0 +1,9 @@ +# Mandelbrot Clock + +A simple clock themed on the mandelbrot set. + +Written by [James Milner](https://www.github.com/jameslmilner) + +![](app.png) + + \ No newline at end of file diff --git a/apps/mandelbrotclock/app.png b/apps/mandelbrotclock/app.png new file mode 100644 index 000000000..95ab99a91 Binary files /dev/null and b/apps/mandelbrotclock/app.png differ diff --git a/apps/mandelbrotclock/mandelbrotclock-icon.js b/apps/mandelbrotclock/mandelbrotclock-icon.js new file mode 100644 index 000000000..a2898e734 --- /dev/null +++ b/apps/mandelbrotclock/mandelbrotclock-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/mandelbrotclock/mandelbrotclock.js b/apps/mandelbrotclock/mandelbrotclock.js new file mode 100644 index 000000000..94636056e --- /dev/null +++ b/apps/mandelbrotclock/mandelbrotclock.js @@ -0,0 +1,34 @@ +// MIT License - James Milner 2021 + +const mandelbrotBmp = { + width: 176, + height: 176, + bpp: 8, + transparent: 254, + buffer: require("heatshrink").decompress( + atob( + "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" + ) + ), +}; + +function draw() { + g.drawImage(mandelbrotBmp); + // 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/mandelbrotclock/mandelbrotclock.png b/apps/mandelbrotclock/mandelbrotclock.png new file mode 100644 index 000000000..19601fe2e Binary files /dev/null and b/apps/mandelbrotclock/mandelbrotclock.png differ diff --git a/apps/mandelbrotclock/screenshot_mandelbrotclock.png b/apps/mandelbrotclock/screenshot_mandelbrotclock.png new file mode 100644 index 000000000..542cff324 Binary files /dev/null and b/apps/mandelbrotclock/screenshot_mandelbrotclock.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/menuwheel/ChangeLog b/apps/menuwheel/ChangeLog new file mode 100644 index 000000000..defdb5049 --- /dev/null +++ b/apps/menuwheel/ChangeLog @@ -0,0 +1 @@ +0.01: New menu! diff --git a/apps/menuwheel/README.md b/apps/menuwheel/README.md new file mode 100644 index 000000000..22cb49466 --- /dev/null +++ b/apps/menuwheel/README.md @@ -0,0 +1,25 @@ +# Wheel Menu + +Replace Bangle.js 2's menus with a version that contains variable-size text and a back button. + +Bangle.js 1: +![Dark Mode Screenshot](screenshot_b1_dark.png) +![Light Mode Screenshot](screenshot_b1_light.png) + +Bangle.js 2: +![Dark Mode Screenshot](screenshot_b2_dark.png) +![Editing Screenshot](screenshot_b2_edit.png) +![Light Mode Screenshot](screenshot_b2_light.png) + + +## Features + +If the menu contains "Back" or "Exit", it is shown as a button instead. +The menu wraps around, with a divider between the last and first items. + +## Controls + +Bangle.js 1: Use BTN1/BTN3 to scroll through items, BTN2 to open/edit the selected item. +Bangle.js 2: Swipe up/down to scroll through items, tap/BTN to open/edit the selected item. + +Press the back button (if present) to go back. \ No newline at end of file diff --git a/apps/menuwheel/boot.js b/apps/menuwheel/boot.js new file mode 100644 index 000000000..3e708e9a8 --- /dev/null +++ b/apps/menuwheel/boot.js @@ -0,0 +1,213 @@ +E.showMenu = function(items) { + g.clearRect(Bangle.appRect); // clear screen if no menu supplied + // clean up back button listener + if (Bangle.backHandler) Bangle.removeListener('touch', Bangle.backHandler) + delete Bangle.backHandler; + if (!items) { + Bangle.setUI(); + return; + } + + var B2 = process.env.HWVERSION===2, + loc = require("locale"), + menuItems = Object.keys(items), + options = items[""]; + if (options) menuItems.splice(menuItems.indexOf(""),1); + if (!(options instanceof Object)) options = {}; + + // show "< Back" item (or similar) as button instead (i.e. remove from the menu) + var back,backLbl; + for (var b of ['Back', 'Exit', 'Cancel']) { + if (!items[b] && items['< '+b]) b = '< '+b; + back = items[b]; + if (typeof back === "function") { + backLbl = loc.translate(b); + menuItems.splice(menuItems.indexOf(b),1); + break; + } + else back = undefined; + } + // font sizes + var small = B2?15:22, + large = B2?30:45; + if (options.selected === undefined) options.selected = 0; + var ar = Bangle.appRect, + x = ar.x, + x2 = ar.x2, + w = ar.w, + y = ar.y, + y2 = ar.y2; + if (options.title) y += 22; + var wrap = menuItems.length>3; // don't wrap if all items are always in view anyway + + var vc=Math.round((y+y2)/2), // vertical center + hc = Math.round((x+x2)/2), // horizontal center + ih = large+small*2; // active item height + + var getItem = idx => { + // we wrap out-of-range indexes + while (idx<0) idx+=menuItems.length; + idx = idx%menuItems.length; + var name = menuItems[idx]; + var item = items[name]; + var v; + if ("object"== typeof item) { + v = item.value; + if (item.format) v = item.format(v); + v = loc.translate(""+v); + } + return {lbl: loc.translate(name), v: v}; + }; + var l = { + lastIdx : null, // we want a complete redraw on first run + draw : function() { + var idx = options.selected, + edit = l.selectEdit; + g.reset(); + + // don't highlight whole item when editing + g.setColor(edit?g.theme.fg:g.theme.fgH) + .setBgColor(edit?g.theme.bg:g.theme.bgH) + .setFont('Vector', large); + var item = getItem(idx), + lw = g.stringWidth(item.lbl)+2; + if (lw+2 >= w) { // label width doesn't fit at large size: scale it down + g.setFont('Vector', Math.floor(large*ar.w/lw)); + } + g.clearRect(x,vc-ih/2,x2,vc+ih/2) + .setFontAlign(0,0,0).drawString(item.lbl,hc,vc); + + if (item.v !== undefined) { + g.setColor(g.theme.fgH).setBgColor(g.theme.bgH) // always highlighted: either as part of item, or while editing + .setFontAlign(0,1,0) + .setFont('Vector', small) + .clearRect(x,vc+ih/2-small-2,x2,vc+ih/2) + .drawString(item.v,hc,vc+ih/2-1); + if (edit) { + g.drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@",x2-23,vc+ih/2-small+(B2?1:5),{scale:2}); + } + } + if (l.lastIdx !== idx) { + // we scrolled: redraw all + l.lastIdx=idx; + g.reset(); + + if (options.title) { + if (B2) g.setFont('12x20'); + else g.setFont('6x8',2); + g.drawLine(x, y-2, x2, y-2) + .setFontAlign(0,1,0) + .drawString(options.title, (x+x2)/2, y-2); + } + + // clear prev/next items area + g.clearRect(x,y,x2,vc-ih/2-1) + .clearRect(x,vc+ih/2+1,x2,y2); + + // get display label by index + var lbl = idx => { + var item = getItem(idx); + if (item.v !== undefined) item.lbl+=': '+item.v; + return item.lbl; + } + // previous two items + g.setFontAlign(0, 1) + if (wrap||idx>0) g.setFont('Vector', small).drawString(lbl(idx-1), hc, vc-ih/2-5); + if (wrap||idx>1) g.setFont('Vector', small/2).drawString(lbl(idx-2), hc, vc-ih/2-small-10); + // next two items + g.setFontAlign(0, -1); + if (wrap||idx g.drawLine(x, y, x2, y); + if (idx===0) div(vc-ih/2-1); + if (idx===1) div(vc-ih/2-small-8); + // if (s === 2) div(vc-ih/2-small*1.5-13); + if (idx===menuItems.length-1) div(vc+ih/2+1); + if (idx===menuItems.length-2) div(vc+ih/2+small+6); + // if (s === 2) div(vc+ih/2+small*1.5+13); + } + + if (back) { + g.setBgColor(g.theme.bg2) + .setFont('Vector', small); + var bw=g.stringWidth(backLbl); + g.clearRect(x,y, x+bw+2, y+small+2); + var bx1=x, by1=y, bx2=x+bw+2, by2=y+small+2; + // g.drawRect(x,y, x+bw+2, y+small+2); + var poly = [ // button outline + bx1+2,by1, + bx2-2,by1, + bx2, by1+2, + bx2, by2-2, + bx2-2,by2, + bx1+2,by2, + bx1, by2-2, + bx1, by1+2, + ] + g.setColor(g.theme.bg2).fillPoly(poly, true) + .setColor(g.theme.fg2).drawPoly(poly, true) + .setFontAlign(-1,-1,0).drawString(backLbl, x+2,y+2); + } + } + g.flip(); + }, + select : function() { // same as default menu + var item = items[menuItems[options.selected]]; + if ("function" == typeof item) {l.lastIdx=null; item(l);} // force a redraw after callback + else if ("object" == typeof item) { + // if a number, go into 'edit mode' + if ("number" == typeof item.value) + l.selectEdit = l.selectEdit?undefined:item; + else { // else just toggle bools + if ("boolean" == typeof item.value) item.value=!item.value; + if (item.onchange) {l.lastIdx=null; item.onchange(item.value);} // force a redraw after callback + } + l.draw(); + } + }, + move : function(dir) { + if (l.selectEdit) { // same as default menu + var item = l.selectEdit; + item.value -= (dir||1)*(item.step||1); + if (item.min!==undefined && item.valueitem.max) item.value = item.wrap ? item.min : item.max; + if (item.onchange) {l.lastIdx=null; item.onchange(item.value);} // force a redraw after callback + } else { + if (B2) dir=-dir; // swipe vs button scrolling + if (!wrap && (options.selected+dir<0 || options.selected+dir>=menuItems.length)) { + return; + } + options.selected = (options.selected+dir+menuItems.length)%menuItems.length; + } + l.draw(); + } + }; + l.draw(); + Bangle.setUI("updown",dir => { + if (dir) l.move(dir); + else l.select(); + }); + if (back) { + // we have a back button: check touches before passing them to setUI's touchHandler + if (B2) { + Bangle.removeListener('touch', Bangle.touchHandler); + Bangle.backHandler = (b, xy) => { + // anywhere top-left (but above the active item) = back button + if (xy.x { + // left side = back button + if (b===1) back(); + } + } + // note: backHandler is cleaned up at the top of this file + Bangle.on('touch', Bangle.backHandler); + } + return l; +}; diff --git a/apps/menuwheel/icon.png b/apps/menuwheel/icon.png new file mode 100644 index 000000000..61f94a035 Binary files /dev/null and b/apps/menuwheel/icon.png differ diff --git a/apps/menuwheel/screenshot_b1_dark.png b/apps/menuwheel/screenshot_b1_dark.png new file mode 100644 index 000000000..c6dfb802b Binary files /dev/null and b/apps/menuwheel/screenshot_b1_dark.png differ diff --git a/apps/menuwheel/screenshot_b1_edit.png b/apps/menuwheel/screenshot_b1_edit.png new file mode 100644 index 000000000..a39b0a832 Binary files /dev/null and b/apps/menuwheel/screenshot_b1_edit.png differ diff --git a/apps/menuwheel/screenshot_b1_light.png b/apps/menuwheel/screenshot_b1_light.png new file mode 100644 index 000000000..35ac01fe9 Binary files /dev/null and b/apps/menuwheel/screenshot_b1_light.png differ diff --git a/apps/menuwheel/screenshot_b2_dark.png b/apps/menuwheel/screenshot_b2_dark.png new file mode 100644 index 000000000..1393838a3 Binary files /dev/null and b/apps/menuwheel/screenshot_b2_dark.png differ diff --git a/apps/menuwheel/screenshot_b2_edit.png b/apps/menuwheel/screenshot_b2_edit.png new file mode 100644 index 000000000..bca98a9a5 Binary files /dev/null and b/apps/menuwheel/screenshot_b2_edit.png differ diff --git a/apps/menuwheel/screenshot_b2_light.png b/apps/menuwheel/screenshot_b2_light.png new file mode 100644 index 000000000..4ffe08fe3 Binary files /dev/null and b/apps/menuwheel/screenshot_b2_light.png differ diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index 4f7df3859..196e85107 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -1,3 +1,14 @@ 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) +0.08: Fix rendering of long messages (fix #969) + buzz on new message (fix #999) +0.09: Message now disappears after 60s if no action taken and clock loads (fix 922) + Fix phone icon (#1014) diff --git a/apps/messages/README.md b/apps/messages/README.md index c243ec06a..e9aa128d1 100644 --- a/apps/messages/README.md +++ b/apps/messages/README.md @@ -8,9 +8,17 @@ and responded to. It is a replacement for the old `notify`/`gadgetbridge` apps. -## Usage +## Settings + +You can change settings by going to the global `Settings` app, then `App Settings` +and `Messages`: + +* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received +* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds +* `Unread Timer` - when a new message is received we go into the Messages app. +If there is no user input for this amount of time then the app will exit and return +to the clock where `MESSAGES` will be shown in the Widget bar. -... ## Requests diff --git a/apps/messages/app.js b/apps/messages/app.js index 6c7cf5fc9..c609acb4b 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -16,10 +16,12 @@ {"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"); +var fontSmall = "6x8"; var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; @@ -40,12 +42,20 @@ try { }; } - +/** this is a timeout if the app has started and is showing a single message +but the user hasn't seen it (eg no user input) - in which case +we should start a timeout for settings.unreadTimeout to return +to the clock. */ +var unreadTimeout; +/// List of all our messages 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,12 +65,20 @@ 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=="); + if (s=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA"); if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); if (msg.id=="back") return getBackImage(); @@ -102,7 +120,7 @@ function showMapMessage(msg) { msg.new = false; saveMessages(); layout = undefined; - checkMessages(); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); }); } @@ -117,7 +135,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: [ @@ -139,61 +157,120 @@ 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 - if (msg.src=="Maps") return showMapMessage(msg); - if (msg.id=="music") return showMusicMessage(msg); + if (msg.src=="Maps") { + cancelReloadTimeout(); // don't auto-reload to clock now + return showMapMessage(msg); + } + if (msg.id=="music") { + cancelReloadTimeout(); // don't auto-reload to clock now + return showMusicMessage(msg); + } // Normal text message display - var title=msg.title, titleFont = fontLarge; + var title=msg.title, titleFont = fontLarge, lines; if (title) { - var w = g.getWidth()-40; + var w = g.getWidth()-48; if (g.setFont(titleFont).stringWidth(title) > w) titleFont = fontMedium; - if (g.setFont(titleFont).stringWidth(title) > w) - title = g.wrapString(title, w).join("\n"); + if (g.setFont(titleFont).stringWidth(title) > w) { + lines = g.wrapString(title, w); + title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n"); + } } + var buttons = [ + {type:"btn", src:getBackImage(), cb:()=>{ + msg.new = false; saveMessages(); // read mail + cancelReloadTimeout(); // don't auto-reload to clock now + checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:1}); + }} // back + ]; + if (msg.positive) { + buttons.push({type:"btn", src:getPosImage(), cb:()=>{ + msg.new = false; saveMessages(); + cancelReloadTimeout(); // don't auto-reload to clock now + 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(); + cancelReloadTimeout(); // don't auto-reload to clock now + Bangle.messageResponse(msg,false); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + }}); + } + lines = g.wrapString(msg.body, g.getWidth()-10); + var body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n"); 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:()=>{ + cancelReloadTimeout(); // don't auto-reload to clock now + showMessageSettings(msg); + }}, { type:"v", fillx:1, c: [ - {type:"txt", font:fontMedium, label:msg.src||"Message", bgCol:colBg, fillx:1, pad:2 }, + {type:"txt", font:fontSmall, label:msg.src||"Message", bgCol:colBg, fillx:1, pad:2, halign:1 }, 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:"txt", font:fontMedium, label:body, fillx:1, filly:1, pad:2 }, + {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(); + // we don't have to time out of this screen... + cancelReloadTimeout(); // 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); @@ -212,7 +289,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"); @@ -231,7 +308,23 @@ function checkMessages(forceShowMenu) { }); } +function cancelReloadTimeout() { + if (!unreadTimeout) return; + clearTimeout(unreadTimeout); + unreadTimeout = undefined; +} + + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); -checkMessages(); +setTimeout(() => { + var unreadTimeoutSecs = (require('Storage').readJSON("messages.settings.json", true) || {}).unreadTimeout; + if (unreadTimeoutSecs===undefined) unreadTimeoutSecs=60; + if (unreadTimeoutSecs) + unreadTimeout = setTimeout(function() { + print("Message not seen - reloading"); + load(); + }, unreadTimeoutSecs*1000); + 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..e93a5e2ba 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -1,10 +1,10 @@ +/* Push a new message onto messages queue, event is: + {t:"add",id:int, src,title,subject,body,sender,tel, important:bool} // add new + {t:"add",id:int, id:"music", state, artist, track, etc} // add new + {t:"remove-",id:int} // remove + {t:"modify",id:int, title:string} // modified +*/ exports.pushMessage = function(event) { - /* event is: - {t:"add",id:int, src,title,subject,body,sender,tel, important:bool} // add new - {t:"add",id:int, id:"music", state, artist, track, etc} // add new - {t:"remove-",id:int} // remove - {t:"modify",id:int, title:string} // modified - */ var messages, inApp = "undefined"!=typeof MESSAGES; if (inApp) messages = MESSAGES; // we're in an app that has already loaded messages @@ -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); @@ -25,13 +28,34 @@ exports.pushMessage = function(event) { if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]); // ok, saved now - we only care if it's new if (event.t!="add") return; - // otherwise load after a delay, to ensure we have all the messages + // otherwise load messages/show widget + var loadMessages = Bangle.CLOCK || event.important; + // first, buzz + if (loadMessages && global.WIDGETS && WIDGETS.messages) + WIDGETS.messages.buzz(); + // after a delay load the app, 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 (loadMessages) 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); } +/// Remove all messages +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..fd8ce8f39 --- /dev/null +++ b/apps/messages/settings.js @@ -0,0 +1,42 @@ +(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; + if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; + 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) + }, + 'Unread timer': { + value: settings().unreadTimeout, + min: 0, max: 240, step : 10, + format: v => v?v+"s":"Off", + onchange: v => updateSetting("unreadTimeout", v) + }, + }; + E.showMenu(mainmenu); +}) diff --git a/apps/messages/widget.js b/apps/messages/widget.js index eda4a85a5..245a303fc 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -5,16 +5,39 @@ 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(quiet) { WIDGETS["messages"].t=Date.now(); // first time WIDGETS["messages"].l=Date.now()-10000; // last buzz - if (WIDGETS["messages"].c!==undefined) return; // already called + if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing 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(); }}; +/* We might have returned here if we were in the Messages app for a +message but then the watch was never viewed. In that case we don't +want to buzz but should still show that there are unread messages. */ +if (global.MESSAGES===undefined) (function() { + var messages = require("Storage").readJSON("messages.json",1)||[]; + if (messages.some(m=>m.new)) WIDGETS["messages"].show(true); +})(); 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/multiclock/ChangeLog b/apps/multiclock/ChangeLog index 9d02ae85e..442a5277a 100644 --- a/apps/multiclock/ChangeLog +++ b/apps/multiclock/ChangeLog @@ -6,6 +6,7 @@ 0.06: add minute tick for efficiency and nifty A clock 0.07: compatible with Bang;e.js 2 0.08: fix minute tick bug +0.09: use setUI clockupdown for controls + fix small display bug in nifty face diff --git a/apps/multiclock/README.md b/apps/multiclock/README.md index e8b8335ea..25c997329 100644 --- a/apps/multiclock/README.md +++ b/apps/multiclock/README.md @@ -5,7 +5,9 @@ This is a clock app that supports multiple clock faces. The user can switch betw ## Controls -Swipe left and right on both the Bangle and Bangle 2 switch between faces. BTN1 & BTH3 also switch faces on the Bangle. +Uses `setUI("clockupdown")` +BTN1 & BTH3 switch faces on the Bangle. +Touch upper right and lower right quadrant switch faces on the Bangle 2. ## Adding a new face Clock faces are described in javascript storage files named `name.face.js`. For example, the Analog Clock Face is described in `ana.face.js`. These files have the following structure: diff --git a/apps/multiclock/multiclock.app.js b/apps/multiclock/multiclock.app.js index c24e5c94b..0565a7040 100644 --- a/apps/multiclock/multiclock.app.js +++ b/apps/multiclock/multiclock.app.js @@ -67,7 +67,7 @@ function setButtons(){ startdraw(); } } - Bangle.setUI("leftright", newFace); + Bangle.setUI("clockupdown", newFace); } E.on('kill',()=>{ diff --git a/apps/multiclock/nifty.face.js b/apps/multiclock/nifty.face.js index 2c2af6063..54962da34 100644 --- a/apps/multiclock/nifty.face.js +++ b/apps/multiclock/nifty.face.js @@ -28,7 +28,7 @@ var now = new Date(); const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0)); const minutes = d02(now.getMinutes()); - const day = d02(now.getDay()); + const day = d02(now.getDate()); const month = d02(now.getMonth() + 1); const year = now.getFullYear(); const month2 = locale.month(now, 3); diff --git a/apps/multiclock/screen-ana.png b/apps/multiclock/screen-ana.png new file mode 100644 index 000000000..67d794aa3 Binary files /dev/null and b/apps/multiclock/screen-ana.png differ diff --git a/apps/multiclock/screen-big.png b/apps/multiclock/screen-big.png new file mode 100644 index 000000000..80544d552 Binary files /dev/null and b/apps/multiclock/screen-big.png differ diff --git a/apps/multiclock/screen-date.png b/apps/multiclock/screen-date.png new file mode 100644 index 000000000..21093f458 Binary files /dev/null and b/apps/multiclock/screen-date.png differ diff --git a/apps/multiclock/screen-nifty.png b/apps/multiclock/screen-nifty.png new file mode 100644 index 000000000..884456125 Binary files /dev/null and b/apps/multiclock/screen-nifty.png differ diff --git a/apps/multiclock/screen-sec.png b/apps/multiclock/screen-sec.png new file mode 100644 index 000000000..cc1149254 Binary files /dev/null and b/apps/multiclock/screen-sec.png differ diff --git a/apps/multiclock/screen-td.png b/apps/multiclock/screen-td.png new file mode 100644 index 000000000..edea06a2e Binary files /dev/null and b/apps/multiclock/screen-td.png differ diff --git a/apps/multiclock/screen-word.png b/apps/multiclock/screen-word.png new file mode 100644 index 000000000..ad029a60f Binary files /dev/null and b/apps/multiclock/screen-word.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 + +![](screenshot_1.png) + +### Or select 'Set By GPS' to start the GPS + +![](screenshot_2.png) + +### While the GPS is running you will see: + +![](screenshot_3.png) + +### When a GPS fix is received you will see: + +![](screenshot_4.png) + + + +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' }, + '{ load(); }, + 'City': { + value: 0 | locations.indexOf(s.location), + min: 0, max: 6, + format: v => locations[v], + onchange: v => { + if (v != 6) { + s.location = locations[v]; + s.lat = lats[v]; + s.lon = lons[v]; + save(); + } + } + }, + 'Set From GPS': ()=>{ setFromGPS(); } + } + return E.showMenu(mainmenu); +} + +loadSettings(); +showMainMenu(); diff --git a/apps/mylocation/mylocation.icon.js b/apps/mylocation/mylocation.icon.js new file mode 100644 index 000000000..bfb38d5ac --- /dev/null +++ b/apps/mylocation/mylocation.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///t/7j/P3/vB4cBqtVoAbHBQIABBQ0FBYdQBYsVBYdUERIkGHIQADHoguEGAwuEGAwKFBZg8DHQw8EBYNf/1Vq3/8oLDIwNf/Wpv//0oLG9Wq3/qBYJUCBYuqBaBqBBYW+BepHEBbybCBYP+BYSnErYLDyoLFAANq/r8Ga5T7MBZZUBAAhSCfhA6DBZhIGBQg8FHQg8GHQgwGFwowFBQwwDFwwLMlS7Bqta1AKEn2q1K1C1WgBYf/1WqBYIDB1QKCgYLC0taBYoXB/QICBY0//7vBAAQ8EEgIABCwwME9QVEA")) diff --git a/apps/mylocation/mylocation.png b/apps/mylocation/mylocation.png new file mode 100644 index 000000000..7148990a4 Binary files /dev/null and b/apps/mylocation/mylocation.png differ diff --git a/apps/mylocation/screenshot_1.png b/apps/mylocation/screenshot_1.png new file mode 100644 index 000000000..a9c61b6b3 Binary files /dev/null and b/apps/mylocation/screenshot_1.png differ diff --git a/apps/mylocation/screenshot_2.png b/apps/mylocation/screenshot_2.png new file mode 100644 index 000000000..4c4404540 Binary files /dev/null and b/apps/mylocation/screenshot_2.png differ diff --git a/apps/mylocation/screenshot_3.png b/apps/mylocation/screenshot_3.png new file mode 100644 index 000000000..81570670b Binary files /dev/null and b/apps/mylocation/screenshot_3.png differ diff --git a/apps/mylocation/screenshot_4.png b/apps/mylocation/screenshot_4.png new file mode 100644 index 000000000..ffae679c9 Binary files /dev/null and b/apps/mylocation/screenshot_4.png differ diff --git a/apps/mysticclock/bangle1-mystic-clock-screenshot.png b/apps/mysticclock/bangle1-mystic-clock-screenshot.png new file mode 100644 index 000000000..2aff6d69a Binary files /dev/null and b/apps/mysticclock/bangle1-mystic-clock-screenshot.png differ diff --git a/apps/mywelcome/ChangeLog b/apps/mywelcome/ChangeLog index b012da933..f2b54e42c 100644 --- a/apps/mywelcome/ChangeLog +++ b/apps/mywelcome/ChangeLog @@ -14,3 +14,4 @@ 0.10: Add birthday style 0.11: Skip double buffering, use 240x240 size 0.12: Fix swipe direction (#800) +0.13: Bangle.js 2 support diff --git a/apps/mywelcome/app.js b/apps/mywelcome/app-bangle1.js similarity index 100% rename from apps/mywelcome/app.js rename to apps/mywelcome/app-bangle1.js diff --git a/apps/mywelcome/app-bangle2.js b/apps/mywelcome/app-bangle2.js new file mode 100644 index 000000000..aeee6918d --- /dev/null +++ b/apps/mywelcome/app-bangle2.js @@ -0,0 +1,254 @@ +// exec each function from seq one after the other +function animate(seq,period) { + var c = g.getColor(); + var i = setInterval(function() { + if (seq.length) { + var f = seq.shift(); + g.setColor(c); + if (f) f(); + } else clearInterval(i); + },period); +} + +// Fade in to FG color with angled lines +function fade(col, callback) { + var n = 0; + function f() {"ram" + g.setColor(col); + for (var i=n;i<240;i+=10) g.drawLine(i,0,0,i).drawLine(i,240,240,i); + g.flip(); + n++; + if (n<10) setTimeout(f,0); + else callback(); + } + f(); +} + + +var SCENE_COUNT=11; +function getScene(n) { + if (n==0) return function() { + console.log("Start app"); + g.clear(1); + eval(require("Storage").read("mywelcome.custom.js")); + } + if (n==1) return function() { + g.reset().setBgColor(0).clearRect(0,0,176,176); + g.setFont("6x15"); + var n=0; + var l = Bangle.getLogo(); + var im = g.imageMetrics(l); + var i = setInterval(function() { + n+=0.1; + g.setColor(n,n,n); + g.drawImage(l,(176-im.width)/2,(176-im.height)/2); + if (n>=1) { + clearInterval(i); + setTimeout(()=>g.drawString("Open",44,104), 500); + setTimeout(()=>g.drawString("Hackable",44,116), 1000); + setTimeout(()=>g.drawString("Smart Watch",44,128), 1500); + } + },50); + }; + if (n==2) return function() { + var img = require("heatshrink").decompress(atob("ptR4n/j/4gH+8H5wl+jOukVVoHZ8dt/n//n37OtgH9sHhwHp4H5xmkGiH72MRje/LL/7iIAEE7sPEgoAC+AlagIlIiMQErPxDwUYxAABwIHCj8N7nOl3uEqa6BEggnFjfM5nCkUil3gEq5KDAAQmC6QmBE4JxSEhIABiQmB8QmSXoQlCYRMdEwIlCAAIlNhYlOiO85nNEyMPEoZwIAAcsYIYmPXoYlMiKaFExX/u9VEqLBBOYrCH+czmtVqJyDEpiaCOYsgSYszmc3qtTEqMR7hzG8AlGmd1OQglOOY6aEgYlCmmZoJMCTBrnD6SaIEoU/zOUuolSjbnBJgqaCEoU5zOXX4RyQYBBzCS4X5zNDqqZCJiERJg5zBEoVJEoM1JgYlQjhMHc4JLEmZMEEp6ZIJgPzS4WTmZMVTILmFYAK+BmglCmd1JgUYJiPNEorABEIOZygDBm5MCiJMQlhMH8ByBXwIlBJgUxJiMd5nOTIzlBTAK+BAANVq4jPAAS/HJgJyCTATAEACC/B4S/IJgIlCYAgAPiS/Kn5yEYANTEyPc5niOQxMB/LlCOapyJJgbpBYAZzROQK/Gl0ATIWfEoZzBc6IlB6SYGgBJBJgpzSlhyH8EAh5MBTIjnCuIlOjjlHTAJzC/LmDTSSYIEoTABOYIlETSKYHXwIABOYM0yYmETSCYHEobnDOYqaBExu8TAwlEc4U5EoiaCmK+NTAolFEwX0TQzBMXwXiEpTBCAAomNEoS+EEo4mIYIImKEoS+EEpDoBEyUbEo3gEo4mJdAImIJY4lJEycdEoPOOBYmPuIlE+HcJYhKKTZ1fhYkB2EAhnNcYMuEhomMr8A3YABEoJyB5gjOAAYmHm9VgELEoJMBEoXAEyXzE45YBJgXwEqx1I+ByDOYJyVJw5yCgEB3cQGgJMWJwQnCu6/CgFBigDB13S/glVAAf1qomCglEoADB1QDBADEPEoNVqEAolEgEKolKErJMDYAJMD0lE0AmaEoNaAgJMCFIYAahV/IgIiDOTgABNYJMEOToiCIoJMCOTzfCN4RMBOTxsDJIRyfIwZMBKQZzfJgRyfOYZMBOUBzCJgNKOT5zDJgLoCADxKBOAIABOT6aCAARyfOYRyjOYRyjOYlKEsBzEEsBzEOUJzDOUIABOUiaDOURzCOUZzCEscKCiY")); + var im = g.imageMetrics(img); + g.reset(); + g.setBgColor("#ff00ff"); + var y = 176, speed = 5; + function balloon(callback) { + y-=speed; + var x = (176-im.width)/2; + g.drawImage(img,x,y); + g.clearRect(x,y+81,x+77,y+81+speed); + if (y>30) setTimeout(balloon,0,callback); + else callback(); + } + fade("#ff00ff", function() { + balloon(function() { + g.setColor(-1).setFont("6x15:2").setFontAlign(0,0); + g.drawString("Welcome.",88,130); + }); + }); + setTimeout(function() { + var n=0; + var i = setInterval(function() { + n+=4; + g.scroll(0,-4); + if (n>150) + clearInterval(i); + },20); + },3500); + + }; + if (n==3) return function() { + g.reset(); + g.setBgColor("#ffff00").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 70, y = 25, h=25; + animate([ + ()=>g.drawString("Your",x,y+=h), + ()=>g.drawString("Bangle.js",x,y+=h), + ()=>g.drawString("has one",x,y+=h), + ()=>g.drawString("button",x,y+=h), + ()=>{g.setFont("12x20:2").setFontAlign(0,0,1).drawString("HERE!",150,88);} + ],200); + }; + if (n==4) return function() { + g.reset(); + g.setBgColor("#00ffff").setColor(0).clear(); + g.setFontAlign(0,0).setFont("6x15:2"); + g.drawString("Press",88,40).setFontAlign(0,-1); + g.setFont("12x20"); + g.drawString("To wake the\nscreen up, or to\nselect", 88,60); + }; + if (n==5) return function() { + g.reset(); + g.setBgColor("#00ffff").setColor(0).clear(); + g.setFontAlign(0,0).setFont("6x15:2"); + g.drawString("Long Press",88,40).setFontAlign(0,-1); + g.setFont("12x20"); + g.drawString("To go back to\nthe clock", 88,60); + }; + if (n==6) return function() { + g.reset(); + g.setBgColor("#ff0000").setColor(0).clear(); + g.setFontAlign(0,0).setFont("12x20"); + g.drawString("If Bangle.js ever\nstops, hold the\nbutton for\nten seconds.\n\nBangle.js will\nthen reboot.", 88,78); + }; + if (n==7) return function() { + g.reset(); + g.setBgColor("#0000ff").setColor(-1).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88, y = -20, h=60; + animate([ + ()=>{g.drawString("Bangle.js has a\nfull touchscreen",x,y+=h);}, + 0,0, + ()=>{g.drawString("Drag up and down\nto scroll and\ntap to select",x,y+=h);}, + ],300); + }; + if (n==8) return function() { + g.reset(); + g.setBgColor("#00ff00").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88, y = -35, h=80; + animate([ + ()=>{g.drawString("Bangle.js comes\nwith a few\napps installed",x,y+=h);}, + 0,0, + ()=>{g.drawString("To add more, visit\nbanglejs.com/apps",x,y+=h);}, + ],400); + }; + if (n==9) return function() { + g.reset(); + g.setBgColor("#ff0000").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88; + g.drawString("You can also make\nyour own apps!",x,30); + g.drawString("Check out\nbanglejs.com",x,130); + + var rx = 0, ry = 0; + // draw a cube + function draw() { + // rotate + rx += 0.1; + ry += 0.11; + var rcx=Math.cos(rx), + rsx=Math.sin(rx), + rcy=Math.cos(ry), + rsy=Math.sin(ry); + // Project 3D coordinates into 2D + function p(x,y,z) { + var t; + t = x*rcy + z*rsy; + z = z*rcy - x*rsy; + x=t; + t = y*rcx + z*rsx; + z = z*rcx - y*rsx; + y=t; + z += 4; + return [88 + 60*x/z, 78+ 60*y/z]; + } + + var a; + // draw a series of lines to make up our cube + var s = 30; + g.clearRect(88-s,78-s,88+s,78+s); + a = p(-1,-1,-1); g.moveTo(a[0],a[1]); + a = p(1,-1,-1); g.lineTo(a[0],a[1]); + a = p(1,1,-1); g.lineTo(a[0],a[1]); + a = p(-1,1,-1); g.lineTo(a[0],a[1]); + a = p(-1,-1,-1); g.lineTo(a[0],a[1]); + a = p(-1,-1,1); g.moveTo(a[0],a[1]); + a = p(1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,-1,1); g.lineTo(a[0],a[1]); + a = p(-1,-1,-1); g.moveTo(a[0],a[1]); + a = p(-1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,-1,-1); g.moveTo(a[0],a[1]); + a = p(1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,1,-1); g.moveTo(a[0],a[1]); + a = p(1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,1,-1); g.moveTo(a[0],a[1]); + a = p(-1,1,1); g.lineTo(a[0],a[1]); + } + + setInterval(draw,50); + }; + if (n==10) return function() { + g.reset(); + g.setBgColor("#ffffff");g.clear(); + g.setFontAlign(0,0); + g.setFont("12x20"); + + var x = 88, y = 10, h=21; + animate([ + ()=>g.drawString("That's it!",x,y+=h), + ()=>{g.drawString("Press",x,y+=h*2); + g.drawString("the button",x,y+=h); + g.drawString("to start",x,y+=h); + g.drawString("Bangle.js",x,y+=h);} + ],400); + } +} + +var sceneNumber = 0; + +function move(dir) { + if (dir>0 && sceneNumber+1 == SCENE_COUNT) return; // at the end + sceneNumber = (sceneNumber+dir)%SCENE_COUNT; + if (sceneNumber<0) sceneNumber=0; + clearInterval(); + getScene(sceneNumber)(); + if (sceneNumber>1) { + var l = SCENE_COUNT; + for (var i=0;i move(dir)); +setWatch(()=>{ + if (sceneNumber == SCENE_COUNT-1) + load(); + else + move(1); +}, BTN1, {repeat:true}); + +Bangle.setLCDTimeout(0); +Bangle.setLocked(0); +Bangle.setLCDPower(1); +move(0); diff --git a/apps/mywelcome/bangle1-customized-welcome-screenshot.png b/apps/mywelcome/bangle1-customized-welcome-screenshot.png new file mode 100644 index 000000000..5d5520c41 Binary files /dev/null and b/apps/mywelcome/bangle1-customized-welcome-screenshot.png differ diff --git a/apps/mywelcome/custom.html b/apps/mywelcome/custom.html index b021b7b1a..340f178e8 100644 --- a/apps/mywelcome/custom.html +++ b/apps/mywelcome/custom.html @@ -28,20 +28,20 @@ function getApp() { var line3 = document.getElementById("line3").value; var line4 = document.getElementById("line4").value; var style = document.getElementById("style").value; + // build the app's text using a templated String if (style=="Birthday") return `(function() { var ib = require("heatshrink").decompress(atob("jk0ggGDhOZAAWQCYwMEBxAMFAAIaHyc/+c5DgwMC/84Dg4aCBgwcDBoOf+Y4GBoQEBn4zCI44DBDQ4NEyf4BpgoIBoefxINMBhApEBrQAKBrrrGWpANZHBT7FBpYqIFAYcJBggNOFQwoFDgwMHBwoMIBwYMKBrkykANLmcwBu0zBrMDBv4AFN5gA/ADY")); var ir = require("heatshrink").decompress(atob("jk0ggGDhvdAAXQCYwMEBxAMFAAIaH6c/+c9DgwMC/8zDg4aC/4YCHIwNB7/zHAwNCAgM/DQwqDAYIaHBonT/oNMFBAND74NNBhApEBrQAKBrrrGWpANZHBT7FBpYqIFAYcJBgkA5oMF7gNFFQwoFDgwMHHIoMIAAPM5gMKBrk0oANLmcwBu0zBrMDBv4AFN5gA/ADYA=")); var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/+c3DgwMC/8yDg4aC/4YCHIwNBv/zHAwNCAgM/DQwqDAYIaHBolz+4NMFBANDv8nBpgMIFIgNaABQNddYy1IBrI4KfYoNLFRAoDDhIMEgHnBgt+BooqGFAoqGBg4OFBhAODBhQNcmUgBpczmAN2mYNZgYN/AApvMAH4Ab")); var igift = require("heatshrink").decompress(atob("q1QxH+ADOi0QbZ5nMHDQAbKgIACKa4ACKnJWVKghW0KgxWTKgxWyKhBWRKhBWwKhRWPKhRWuKhhWNKhhWtKpxWKKhys8KxBU8Ky5U+KypU/KyhU/KyhU/KynGKn5WTKn5WUKmHCADpJJE7uYABZUfKuuYKv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/AAv+Kv5VT/wADyIAaKpIlbABZSEKv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/ADNtKv6rdKzZVwKhAABy5V/Khw")); - - var W=240,H=240; + var W=g.getWidth(),H=g.getHeight(); + var titleFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; var blns = []; function updateFlake(f) { f.im = [ir,ig,ib][Math.round(Math.random()*100)%3]; f.s = 0.4+Math.random()*0.5; } - for (var i=0;i<6;i++) { var f = { y:Math.random()*H,x:(0.5+(i<3?i:i+5))*W/11, @@ -51,7 +51,6 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ updateFlake(f); blns.push(f); } - function draw() { blns.forEach(f=>{ f.y-=f.v;f.r+=f.t; @@ -63,7 +62,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ }); var x = W/2, y = H/2; g.drawImage(igift,x-43,y-80); - g.setFont("6x8",2).setFontAlign(0,0); + g.setFont(titleFont).setFontAlign(0,0); g.drawString(${JSON.stringify(line1)},x,y+=20); g.drawString(${JSON.stringify(line2)},x,y+=20); g.setFont("6x8"); @@ -71,16 +70,15 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ g.drawString(${JSON.stringify(line4)},x,y+=10); g.flip(); } - - g.clear(); + g.clear(1).setBgColor(0).setColor(-1).clearRect(0,0,W,H); setInterval(draw,50); })()`; // if (style=="Christmas") return `(function() { var isnow = require("heatshrink").decompress(atob("jEagQWTgfAAocf+gFDh4FDiARBggVB3AFBl3Agf8jfkn/AgX/v/9/+Agfv/2//YrBgfwh4wCgfghYFJCIYdFFIw1EIIpNFL44FFOIoAP")); var itree = require("heatshrink").decompress(atob("mtWxH+ADHHDTI0aGuXH5vNGmhqvTYIzBGtoxF6fTG4g4oGgQyBAAZssGoI0Ga1g1FGdo01ZgIAEGmHHNoLSuAAN/rdb0YFBGlgCBGYIABA4YArGYY1CGn4znAAM6GeVd5PQ5Iyurc/vQ0oGZFAn+d4XC3d5GddiGYIEBy+7zoEBGlFhoEcsQ9GT08+oFk1mkGdaVBMgNArnJ6/KzswGs/J6GlrlbqtbvPC5PCy8wGohniMIPJvIpCqmX3e7vI0BqhqlMIY0DqhtBqoEBa0xgBMIIoEqoABGQwzfsIhBv4qHABM50vQGjg1CGaN66DoBGt1ioGd5LoBGjo1PGYNhvLoCa7wnBqgvGA4YzCAgN5GUAsCqoDBmAHCAYU/wPQ0oSDGcBiDqkwAYcxoFd5PX6GdGjrIIqtUAAc3jk5vPC4fCy5pef5I2BTQMcnAHBy+7y95T0oADnFk1ekBpI2aGRUin7NGAA9hsIzVsIgHTAKZBZoPJ5LNDGhBpXGolcwOsrtcA4TNB3bNDGb/+sVin9AoGe6HX5InEvN/TkP+5XQwM/sRsBzqWB4QuKGjvC6HQ4QdDvKWBZYMwmAuHmFUCYNbqibX3fD5O7qolEZQQ0FBwgKDqgJBGiphEDwNUEgJbBFIQqCAgYOCB4IzCnE6GyhYFGoQnDABYzGAAQ1UAAo2NBoQSBnOB0t/Gjo2EABIPCoGe6HX4QzTGRIAEqtVF4QEBBQc4oE4y/J5PCvIxeABk/oADBvO73eXTyAyZMwM/Awd5vIOFGslAr2Av4PLNcU/jmA6HX5I1KasFcn8dTIOd5PJ4SZGGiNhAAIyNn0ckU+ZYe7AAJpJEYJnNGZk+n9kw9cBAcwGoN5aZg1JJJQABm8/oEjoDKC5ALCrUwqh/NrvQ6HDGp04n9doEdoE/sQJBZQZhCqgABGZk6zw0K/1dnVAoNAFwOlCYL1FubJBy4GCGh1AnOX4XC3YzHFYOeCgdV5PQ5OdD4rKBqqYNGYlbv+X3edGY3CGgKMDAAO7JAJgDAClcr2BEYgADaIZ0DL4uXGbDuB6HX5I1GsP+sNhOgWXIhBmWd4Od5PK4TwFGIJoBAYI2BAD0/jlcQoO7AAJaEGQQADGr0/sjNEvOdAoZmDGgw2ZsVAkeAZpQACGZI2VsU/kVGn1bZoPJZogpGGhA4GfRYwBoGC1mlBQbNFFoo0JNxAGCEod/wM6oFAn9iv/J6/Kzo1Ey9/MZQAKCg4GCFgTDEvPCSwI0BC5I0RN4ocEYYPQ5OdHgeXSwTFKGaJyKFYPC3f+MIdbpzFLAD4zB/1OqtbqtOGgYArGAIADGl9UAAI0wGQN5GoQ0vvIABGoI0uGYQABqo0zNOg0uaQY0/GllOGn40//w=")); - var W=g.getWidth(),H=g.getHeight(); + var titleFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; var flakes = []; for (var i=0;i<10;i++) { var f = { @@ -94,7 +92,6 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ f.v = f.s * (1+Math.random()); flakes.push(f); } - function draw() { flakes.forEach(f=>{ f.y+=f.v;f.r+=f.t; @@ -103,7 +100,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ }); var x = W/2, y = H/2; g.drawImage(itree,x-27,y-80); - g.setFont("6x8",2).setFontAlign(0,0); + g.setFont(titleFont).setFontAlign(0,0); g.drawString(${JSON.stringify(line1)},x,y+=20); g.drawString(${JSON.stringify(line2)},x,y+=20); g.setFont("6x8"); @@ -111,8 +108,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ g.drawString(${JSON.stringify(line4)},x,y+=10); g.flip(); } - - g.clear(); + g.clear(1).setBgColor(0).setColor(-1).clearRect(0,0,W,H); setInterval(draw,50); })(); `; diff --git a/apps/nato/bangle1-NATO-alphabet-screenshot.png b/apps/nato/bangle1-NATO-alphabet-screenshot.png new file mode 100644 index 000000000..87c864d3a Binary files /dev/null and b/apps/nato/bangle1-NATO-alphabet-screenshot.png differ diff --git a/apps/nato/bangle1-NATO-alphabet-screenshot2.png b/apps/nato/bangle1-NATO-alphabet-screenshot2.png new file mode 100644 index 000000000..0f4e3861e Binary files /dev/null and b/apps/nato/bangle1-NATO-alphabet-screenshot2.png differ diff --git a/apps/numerals/bangle1-numerals-screenshot.png b/apps/numerals/bangle1-numerals-screenshot.png new file mode 100644 index 000000000..b663f5935 Binary files /dev/null and b/apps/numerals/bangle1-numerals-screenshot.png differ diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 60b9d9ae3..6cb9d061e 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -7,3 +7,6 @@ 0.07: Move to 96px tiles - less files (64 -> 25) and speed up rendering 0.08: Update for drag event refactor 0.09: Use current theme cols when drawing GPS info +0.10: Improve scale factor calculation to fix scaling issues (#984) +0.11: Add slight offset to OSM data to align it properly (fix #984) + Fix alignment of satellite info text diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index c33acd8ad..62597ca20 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -25,11 +25,11 @@ function drawMarker() { var fix; Bangle.on('GPS',function(f) { fix=f; - g.reset().clearRect(0,y1,240,y1+8).setFont("6x8").setFontAlign(0,0); + g.reset().clearRect(0,y1,g.getWidth()-1,y1+8).setFont("6x8").setFontAlign(0,0); var txt = fix.satellites+" satellites"; if (!fix.fix) txt += " - NO FIX"; - g.drawString(txt,120,y1 + 4); + g.drawString(txt,g.getWidth()/2,y1 + 4); drawMarker(); }); Bangle.setGPSPower(1, "app"); diff --git a/apps/openstmap/custom.html b/apps/openstmap/custom.html index 88d94ed37..56dea1188 100644 --- a/apps/openstmap/custom.html +++ b/apps/openstmap/custom.html @@ -63,10 +63,17 @@ TODO: /* Can see possible tiles on http://leaflet-extras.github.io/leaflet-providers/preview/ However some don't allow cross-origin use */ var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast - //var TILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; + var PREVIEWTILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; //var TILELAYER = 'http://a.tile.stamen.com/toner/{z}/{x}/{y}.png'; // black and white + var map = L.map('map').locate({setView: true, maxZoom: 16}); - var tileLayer = L.tileLayer(TILELAYER, { + // Tiles used for Bangle.js itself + var bangleTileLayer = L.tileLayer(TILELAYER, { + maxZoom: 18, + attribution: 'Map data © OpenStreetMap contributors' + }); + // Tiles used for the may the user sees (faster) + var previewTileLayer = L.tileLayer(PREVIEWTILELAYER, { maxZoom: 18, attribution: 'Map data © OpenStreetMap contributors' }); @@ -83,7 +90,7 @@ TODO: } var mapFiles = []; - tileLayer.addTo(map); + previewTileLayer.addTo(map); function tilesLoaded(ctx, width, height) { var options = { @@ -122,16 +129,44 @@ TODO: } document.getElementById("getmap").addEventListener("click", function() { - var bounds = map.getBounds(); var zoom = map.getZoom(); - var centerlatlon = bounds.getCenter(); - var center = map.project(centerlatlon, zoom).divideBy(256); - var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE); - var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE); - center = center.floor(); - var tileGetters = []; + var centerlatlon = map.getBounds().getCenter(); + var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE); + // Reason for 16px adjustment below not 100% known, but it seems to + // align everything perfectly: https://github.com/espruino/BangleApps/issues/984 + var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE) + 16; + var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE) + 16; + center = center.floor(); // make sure we're in the middle of a tile + // JS version of Bangle.js's projection + function bproject(lat, lon) { + const degToRad = Math.PI / 180; // degree to radian conversion + const latMax = 85.0511287798; // clip latitude to sane values + const R = 6378137; // earth radius in m + if (lat > latMax) lat=latMax; + if (lat < -latMax) lat=-latMax; + var s = Math.sin(lat * degToRad); + return new L.Point( + (R * lon * degToRad), + (R * Math.log((1 + s) / (1 - s)) / 2) + ); + } + // Work out scale factors (how much from Bangle.project does one pixel equate to?) + var pc = map.unproject(center.multiplyBy(OSMTILESIZE), zoom); + var pd = map.unproject(center.multiplyBy(OSMTILESIZE).add({x:1,y:0}), zoom); + var bc = bproject(pc.lat, pc.lng) + var bd = bproject(pd.lat, pd.lng) + var scale = bc.distanceTo(bd); - // Render everything to a canvas - 512 x 512 px + // test + /*var p = bproject(centerlatlon.lat, centerlatlon.lng); + var q = bproject(mylat, mylon); + var testPt = { + x : (q.x-p.x)/scale + (MAPSIZE/2), + y : (MAPSIZE/2) - (q.y-p.y)/scale + };*/ + + var tileGetters = []; + // Render everything to a canvas... var canvas = document.getElementById("maptiles"); canvas.style.display=""; var ctx = canvas.getContext('2d'); @@ -147,10 +182,16 @@ TODO: tileGetters.push(new Promise(function(resolve,reject) { img.onload = function(){ ctx.drawImage(img,i*OSMTILESIZE - ox, j*OSMTILESIZE - oy); + /*if (testPt) { + ctx.fillStyle="green"; + ctx.fillRect(testPt.x-1, testPt.y-5, 3,10); + ctx.fillRect(testPt.x-5, testPt.y-1, 10,3); + }*/ resolve(); }; })); - img.src = tileLayer.getTileUrl(coords); + bangleTileLayer._tileZoom = previewTileLayer._tileZoom; + img.src = bangleTileLayer.getTileUrl(coords); })(i,j); } } @@ -163,7 +204,7 @@ TODO: imgx : canvas.width, imgy : canvas.height, tilesize : TILESIZE, - scale : 10000*Math.pow(2,16-zoom), // FIXME - this is probably wrong + scale : scale, // how much of Bangle.project(latlon) does one pixel equate to? lat : centerlatlon.lat, lon : centerlatlon.lng })}); diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js index 554a71ca3..d995aca25 100644 --- a/apps/openstmap/openstmap.js +++ b/apps/openstmap/openstmap.js @@ -34,8 +34,8 @@ exports.draw = function() { var cx = g.getWidth()/2; var cy = g.getHeight()/2; var p = Bangle.project({lat:m.lat,lon:m.lon}); - var ix = (p.x-map.center.x)*4096/map.scale + (map.imgx/2) - cx; - var iy = (map.center.y-p.y)*4096/map.scale + (map.imgy/2) - cy; + var ix = (p.x-map.center.x)/map.scale + (map.imgx/2) - cx; + var iy = (map.center.y-p.y)/map.scale + (map.imgy/2) - cy; //console.log(ix,iy); var tx = 0|(ix/map.tilesize); var ty = 0|(iy/map.tilesize); @@ -57,8 +57,8 @@ exports.latLonToXY = function(lat, lon) { var cx = g.getWidth()/2; var cy = g.getHeight()/2; return { - x : (q.x-p.x)*4096/map.scale + cx, - y : cy - (q.y-p.y)*4096/map.scale + x : (q.x-p.x)/map.scale + cx, + y : cy - (q.y-p.y)/map.scale }; }; @@ -66,6 +66,6 @@ exports.latLonToXY = function(lat, lon) { exports.scroll = function(x,y) { var a = Bangle.project({lat:this.lat,lon:this.lon}); var b = Bangle.project({lat:this.lat+1,lon:this.lon+1}); - this.lon += x * this.map.scale / ((a.x-b.x) * 4096); - this.lat -= y * this.map.scale / ((a.y-b.y) * 4096); + this.lon += x * this.map.scale / (a.x-b.x); + this.lat -= y * this.map.scale / (a.y-b.y); }; diff --git a/apps/openstmap/screenshot.png b/apps/openstmap/screenshot.png new file mode 100644 index 000000000..2895b562e Binary files /dev/null and b/apps/openstmap/screenshot.png differ diff --git a/apps/pastel/ChangeLog b/apps/pastel/ChangeLog index 1277f0d9d..2ede0e161 100644 --- a/apps/pastel/ChangeLog +++ b/apps/pastel/ChangeLog @@ -3,3 +3,6 @@ 0.03: Make it work with Gadgetbridge, Notifications fullscreen on a Bangle 2 0.04: Leave space at the bottom for Chrono widget, set back option at first option 0.05: Added 2 new fonts +0.06: Converted fonts to font modules +0.07: Added info line that cycles on BTN1/BTN3 (or vitual buttons on a bangle 2) +0.08: Added dependancy on MyLocation diff --git a/apps/pastel/README.md b/apps/pastel/README.md index 324c3915a..66ae0e189 100644 --- a/apps/pastel/README.md +++ b/apps/pastel/README.md @@ -1,20 +1,45 @@ -# Pastel Clock - a configurable clock with custom fonts and background +# Pastel Clock + + *a configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times* * Designed specifically for Bangle 1 and Bangle 2 * A choice of 7 different custom fonts * Supports the Light and Dark themes -* Has a settings menu, change font, enable/disable the grid and the date display - +* Has a settings menu, change font, enable/disable the grid +* On Bangle 1 use BTN1,BTN3 to cycle through the info display (Date, ID, Batt %, Ram % etc) +* On Bangle 2 touch the top right/top left to cycle through the info display (Date, ID, Batt %, Ram % etc) +* Uses mylocation.json from MyLocation app to calculate sunrise and sunset times for your location +* Uses pedometer widget to get latest step count +* Dependant apps are installed when Pastel installs I came up with the name Pastel due to the shade of the grid background. -![](screenshot_lato.jpg) -![](screenshot_architech.jpg) -![](screenshot_gochi.jpg) +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/) -![](screenshot_b1_light.jpg) -![](screenshot_b2_dark.jpg) +## Lato +![](screenshot_lato.png) -![](screenshot_monoton.jpg) -![](screenshot_elite.jpg) + +## Architect +![](screenshot_architect.png) + + +## Gochihand +![](screenshot_gochihand.png) + + +## Monoton +![](screenshot_monoton.png) + + +## Elite +![](screenshot_elite.png) + + +## Cabin Sketch +![](screenshot_cabinsketch.png) + + +## Orbitron +![](screenshot_orbitron.png) diff --git a/apps/pastel/f_architect.js b/apps/pastel/f_architect.js new file mode 100644 index 000000000..685b2fa03 --- /dev/null +++ b/apps/pastel/f_architect.js @@ -0,0 +1,9 @@ +var widths = atob("CBolByEeJykkJCYhCg=="); +var font = atob("AAAAAAAAAAAAAAAAYAAAAAAAADgAAAAAAAAeAAAAAAAAB4AAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAD4AAAAAAAA/AAAAAAAAH4AAAAAAAB/AAAAAAAAf4AAAAAAAD+AAAAAAAA/wAAAAAAAH+AAAAAAAB/gAAAAAAAP8AAAAAAAD/AAAAAAAAf4AAAAAAAH+AAAAAAAA/gAAAAAAAP8AAAAAAAB/AAAAAAAAfwAAAAAAAH8AAAAAAAA/AAAAAAAAPwAAAAAAAB8AAAAAAAAfAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAP/8AAAAAAH//4AAAAAB///wAAAAAf/APgAAAAD/gAeAAAAA/wAA8AAAAH8AABwAAAA/AAAHgAAAHwAAAeAAAA+AAAA4AAADgAAADgAAAcAAAAOAAABwAAAA4AAAOAAAADgAAA4AAAAOAAADgAAAA4AAAOAAAADgAAA4AAAAOAAADgAAAB4AAAOAAAAHAAAA4AAAAcAAADwAAADwAAAHAAAAOAAAAeAAAB4AAAA4AAAPAAAADwAAB4AAAAHwAAPgAAAAPgAD8AAAAAf4D/gAAAAAf//4AAAAAAf/+AAAAAAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAP////4AAAB/////gAAAH////+AAAAf////gAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAADwAADAAAAAeAAAeAAAAD4AAD4AAAAfAAAfgAAAD4AAD+AAAAPAAAf4AAAB8AAH/AAAAHgAA/8AAAAcAAH/wAAADwAA/vAAAAOAAP48AAAA4AB/DgAAADgAf4OAAAAPAD+A4AAAA8A/wHgAAAD8/8AcAAAAH//gBwAAAAP/wAPAAAAAf8AA8AAAAAAAADgAAAAAAAAeAAAAAAAAB4AAAAAAAAHgAAAAAAAA+AAAAAAAAD4AAAAAAAAPAAAAAAAAA8AAAAAAAAHwAAAAAAAAfAAAAAAAAA4AAAAAAAABAAAAAAIAAAAAAAADwAAAAAAAAPAAAAAAAAA8AAAAAAAADgAAAAAAAAeAAAAAAAAB4AYAAAAAAHgBwAAAAAAeAPABAAAADwA8AGAAAAPAHgAYAAAA8AeADgAAADwDwAOAAAAOAPAB4AAAB4B8AHgAAAHgPwA8AAAAeA+ADwAAAB4H4AeAAAAHgfgD4AAAAeD+AfAAAAB4e4D8AAAAHj7gfgAAAAf/PH8AAAAB/4//gAAAAH/D/8AAAAAP4H/gAAAAA+Af8AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAADwAAAAAAAAfAAAAAAAAD8AAAAAAAA/wAAAAAAAH/AAAAAAAA/8AAAAAAAPxwAAAAAAB+HAAAAAAAPwcAAAAAAB+BwAAAAAAfwPAAAAAAD+A8AAAAAAfwDwAAAAAD+APAAAAAAPwA8AAAAAB+ADwAAAAAP/////AAAA/////8AAAB/////wAAAD/////AAAAD////8AAAAAAH8AAAAAAAAeAAAAAAAAB4AAAAAAAAHgAAAAAAAAeAAAAAAAAB4AAAAAAAAHgAAAAAAAAcAAAAAAAABwAAAAAAAAHAAAAAAAAAcAAAAAAAABwAAAAAAAAGAAAAAAAAAAAAAAAAAAOAAAAAAAH/8AAAAAAf//wAAAAAD///AAAAAAP//8AAAAAA///wAAAAAAPgPAB4AAAA+A4APgAAAD4DgA+AAAAPAeAB4AAAA8BwAHgAAADwHAAeAAAAPAcAB4AAAB4BgAHgAAAHgGAAeAAAAeAYAD4AAAB4BgAPAAAAPgGAA8AAAA8AYADwAAADwBwAOAAAAPAHAB4AAAA8AcAHgAAAHwB4A8AAAAeAHgHgAAAB4APh+AAAAHgA//wAAAA+AB/+AAAADwAD/wAAAAPAAD8AAAAA8AAAAAAAAHwAAAAAAAAfAAAAAAAAB4AAAAAAAAHgAAAAAAAAeAAAAAAAAB4AAAAAAAAHAAAAAAAAAcAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAH//AAAAAAB///AAAAAAP//+AAAAAD///8AAAAAf+B/4AAAAD/AA/wAAAA/wAA/gAAAD8AAB+AAAAfAAAD8AAAD4AAAPwAAAfAAAB/AAAB4AAAP+AAAPAAAB/4AAA8AAAP/gAAHgAAB++AAAeAAAPz4AABwAAB+PgAAHAAAPw+AAAcAAA+D4AABgAAHwPgAAAAAA/A+AAAAAAD4H4AAAAAAfAfAAAAAAB4D8AAAAAAPgPgAAAAAA8B+AAAAAADwPwAAAAAAPA+AAAAAAA8P4AAAAAAD//AAAAAAAP/4AAAAAAAf+AAAAAAAA/gAAAAAAAAAAAAAAAIAAAAAAAABwAAAAAAAAHAAAAAAAAAcAAAAAAAABwAAAAAAAAHAAAAAAAAAcAAAAAAAABwAAAAAAAAHAAAAAAAAAcAAAAAAAABwAAAAAAAAHAAAAAAAAAcAAAAAAAADwAAAAAAAAPAAAAAAAAA8AAAAAAAADwAAAAAAAAPAAAP4AAAA8AAP/gAAADwAH/+AAAAfAB//wAAAB8Af//AAAAHwH/4AAAAAfB/4AAAAAB8f8AAAAAAH//AAAAAAAf/wAAAAAAB/8AAAAAAAP/gAAAAAAA/4AAAAAAAD/AAAAAAAAPwAAAAAAAA+AAAAAAAADwAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAH+AAAAAAAA/8AAAAAAAP/4AAAAAfB//gAAAAH/Pw/AAAAA//8A8AAAAH//gDwAAAA//8AHgAAAD4fwAeAAAAeA+AB4AAAB4DwADgAAAPAPAAOAAAA4A4AA4AAADgDgADgAAAOAOAAOAAABwAwAA4AAAHAHAADgAAAcAcAAOAAABwBwAA4AAAHAPAAHgAAAcA8AAcAAABwDgABwAAAHAeAAHAAAAcB8AA4AAABwPwAHgAAAHg/AAcAAAAeH8ADwAAAB4/4AeAAAAD//gD4AAAAP+fA/AAAAAfx//4AAAAAAD//AAAAAAAP/wAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAA/wAAAAAAAH/gAAAAAAA/+AAAAAAAH/8AAAAAAA/nwAAAAAAD4PAAAAAAAeA8AAAAAADwDwAAAAAAPAPAAAAAAB4A8AAwAAAHgDwAHgAAAeAPAAeAAADwA8AD4AAAPADwAfgAAA8AOAB8AAADwA4APwAAAPADgB+AAAA8AeAPwAAAD4B4B/AAAAHgHgf4AAAAfA+D+AAAAA/D5/wAAAAB///+AAAAAH///gAAAAAH//4AAAAAAP/+AAAAAAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAA4AAAAAAAADwDAAAAAAAOAeAAAAAAAYB4AAAAAAAAHgAAAAAAAAMAAAAAAAAAAAAA="); + +exports.add = function(graphics) { + graphics.prototype.setFontArchitect = function(scale) { + // Actual height 40 (41 - 2) + this.setFontCustom(font, 46, widths, 58+(scale<<8)+(1<<16)); + } +}; diff --git a/apps/pastel/f_cabin.js b/apps/pastel/f_cabin.js new file mode 100644 index 000000000..916677565 --- /dev/null +++ b/apps/pastel/f_cabin.js @@ -0,0 +1,9 @@ +var widths = atob("ECMtGCEiJSIkHyYlDw=="); +var font = atob("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"); + +exports.add = function(graphics) { + graphics.prototype.setFontCabinSketch = function() { + // Actual height 48 (51 - 4) + this.setFontCustom(font, 46, widths, 65+(1<<8)+(1<<16)); + } +}; diff --git a/apps/pastel/f_elite.js b/apps/pastel/f_elite.js new file mode 100644 index 000000000..a5cac2838 --- /dev/null +++ b/apps/pastel/f_elite.js @@ -0,0 +1,7 @@ + +exports.add = function(graphics) { + graphics.prototype.setFontSpecialElite = function(scale) { + // Actual height 40 (39 - 0) + this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAYAAAAAAAfwAAAAAAP/AAAAAAH/4AAAAAB/+AAAAAAf/gAAAAAH/4AAAAAB/+AAAAAAf/gAAAAAH/4AAAAAAv8AAAAAAN6AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAfAAAAAAAPwAAAAAAP8AAAAAAH+AAAAAAH+AAAAAAD+AAAAAAD/AAAAAAD/AAAAAAB/AAAAAAB/AAAAAAB/AAAAAAB/gAAAAAB/gAAAAAB/gAAAAAA/gAAAAAB/wAAAAAA/4AAAAAA/wAAAAAA/4AAAAAA/4AAAAAAf8AAAAAAP8AAAAAAD8AAAAAAA8AAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//wAAAAP///gAAAH/9/+AAAD/gAf4AAB/AAB+AAA/AAAHwAAPAAAA+AADgAAAPgAAwAAAD4AAcAAAAfAAHAAAAHwABwAAAB8AA4AAAAfAAOAAAAHwABwAAAB8AAcAAAA/AAHgAAAPgAB+AAAH4AAPgAAD8AAD+AAD+AAA/4Af/AAAB////AAAAP///wAAAAP//gAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAOAAAAHAADgAAAD4AB4AAAA+AAeAAAAPgAHgAAAD4AB4AAAAeAAeAAAAHgAHgAAAB4AB4AAAAcAAeAAAAPAAH4AAP/wAB/////+AAf/////gAH/////4AB///+/+AAAAQAAPgAAAAAAB4AAAAAAAeAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAPgAAAAAADwAAAAAAA+AAAAAAAPgAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAcAAAB+AAfwAAA/wAf/AAA/8Af/wAAf/AP/8AAGPwP//AADh8D48AAA4OB8OAAAOAAfDgAAHAAPg4AABwADwPAAAcAB8DwAAHAAeAeAABwAHAHgAAcADwB8AAHAB4APgAB4A+AB4AAPAfAAeAAD4fgADgAAf/4AA4AAD/8AAeAAAf+AAfAAAB8AAPwAAAAAAD4AAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAf8AAB/wAH/wAAf8AB/8AAH+AAffgAB4AcDx4AAcAPAAfAAHAPwAHwABwHwAA8AAcB+AAPAAHA/gADwABw/4AA8AAcf+AAPAAHP/gAHwABz74AB8AAf8fAA/AAH8DwAPgAD/A8AHwAA/gHwP8AAPwA//+AADwAH//AAAAAA//gAAAAAD/wAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAP8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAB/PAAAAAA/jwAAAAAfg8AAAAAPwPAAAAAH4DwAAAAD4A8HAAAD8APBwAAB+ADw8AAA+AA8PAAA/AAPDgAAPgADw8AAHwAB8/AAD+B///wAA/////8AAP/////AAB+f///wAAAAAHx8AAAAAB8PAAAAAAPDwAAAAADw8AAAAAA4PAAAAAAODwAAAAADgcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAB/gAAH//wf8AAB//+H/gAAf//h/4AAHJ/wf/AABwD4D/4AAeA+AAeAAHgPAAHgAB4DwAB4AAeA4AAeAAHgOAAHgAA4DgAB4AAOA8AAeAADgPAAHgAB4D4ADwAAeAeAB4AAHAHwAeAABgAfAPgAAYAD8fgAAAAA//wAAAAAH/4AAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAA//wAAAAB///AAAAB////AAAB/7//wAAA/AfB+AAAfAPAPwAAPgHgB+AAHwBwAPgAB4A8AB8AAeAPAAfAAPADwAHwADgA8AB8AA8APAAfAAPADwAHwADwA8AB8AA8AHgA+AAP8B4APgAD/wfAH4AA/8D4D8AAH/A///AAB/wH//gAAH8A//wAAA8AH/4AAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAB/4AAAAAA/8AAAAAAP+AAAAAAD+AAAAAAAeAAAAAAAHAAAAAAADwAAAAAAB8AAAAAAAfAAAAAAAHwAAA/8AD+AAB//AA/gAB//wAP4AB//gAB+AB//AAAfwB//AAAH8A/wAAAA/A/wAAAAHw/wAAAAB8/wAAAAAffwAAAAAP/wAAAAAD/4AAAAAA/4AAAAAAP8AAAAAAD4AAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAP/wAAAH8H/+AAAH/z//wAAD////+AAB///h/gAA/B/gH4AAPAP4A/AAHwB8AHwAB4APAB8AAeADgAfAAHgA4ADwABwAOAA8AAcADgAPAAHAA4ADwAB4AeAA8AAfAHwAPAADwB8AHgAA+A/gD4AAPgP4B+AAB+P/h/gAAP////wAAB/8f/4AAAP8D/8AAAAAAf8AAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAA/4APAAAA//gH4AAAP/8D/gAAP4Pg/8AADwB8P/AAB4AfD/4AAeAD4d+AAHAA+AfwADwAHgH8AA4AA8A/AAOAAPAPgADgADwD4AA8AA4B+AAPAAeAfAAB4AHgHgAAeADwD4AAHwA8A8AAA+AfA/AAAHp/h/gAAA3//+gAAAB//+gAAAAd//gAAAACf/wAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAABwB/AAAAB/A/8AAAA/wf/gAAAP+H/4AAAH/h/+AAAB/8f/gAAAP+H/4AAAD/h/+AAAAfwf/gAAAH8C/wAAAAAA3oAAAAAABwAAAAAAAAAAAAAAAAAAAA=="), 46, atob("ERwfHB0cHxsdHB4dEQ=="), 50+(scale<<8)+(1<<16)); + } +}; diff --git a/apps/pastel/f_gochihand.js b/apps/pastel/f_gochihand.js new file mode 100644 index 000000000..8ef926f39 --- /dev/null +++ b/apps/pastel/f_gochihand.js @@ -0,0 +1,10 @@ + +var widths = atob("GRMtICcqJiopKiwoGQ=="); +var font = atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAA+AAAAAAAAAAAAfwAAAAAAAAAAAH+AAAAAAAAAAAB/gAAAAAAAAAAAf4AAAAAAAAAAAH+AAAAAAAAAAAB/gAAAAAAAAAAAP4AAAAAAAAAAAD8AAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAAAP+AAAAAAAAAAA//gAAAAAAAAAH//4AAAAAAAAA///+AAAAAAAAP////gAAAAAAB/////4AAAAAAP/////+AAAAAD//////+AAAAA///////wAAAAAf//////AAAAAAP/////4AAAAAAD/////AAAAAAAA////4AAAAAAAAP//+AAAAAAAAAD//gAAAAAAAAAA/8AAAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAH//AAAAAAAAAAH//8AAAAAAAAAH///wAAAAAAAAD///+AAAAAAAAB////wAAAAAAAA////+AAAAAAAAf////gAAAAAAAP/8//8AAAAAAAD/wAf/AAAAAAAB/wAD/4AAAAAAA/4AAP+AAAAAAAP8AAB/wAAAAAAD/AAAf8AAAAAAB/gAAD/AAAAAAAf4AAA/wAAAAAAH8AAAP8AAAAAAB/AAAD/AAAAAAAfwAAA/wAAAAAAP8AAAH8AAAAAAD/AAAD/AAAAAAAf4AAA/wAAAAAAH+AAAP8AAAAAAB/gAAD/AAAAAAAf4AAA/wAAAAAAH/AAAP4AAAAAAB/wAAH+AAAAAAAP+AAB/gAAAAAAD/wAA/wAAAAAAA/+AAf8AAAAAAAH/wAH+AAAAAAAB/+AH/gAAAAAAAP/4D/wAAAAAAAB////4AAAAAAAAf///+AAAAAAAAD////AAAAAAAAAf///gAAAAAAAAD///wAAAAAAAAAP//wAAAAAAAAAA//4AAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAA/gAAAAAAAAAAAf4AAAAAAAAAAAP+AAAAAAAAAAAH/gAAAAAAAAAAB/wAAAAAAAAAAA/4AAAAAAAAAAAf8AAAAAAAAAAAH/AAAAAAAAAAAD/gAAAAAAAAAAA/wAAAAAAAAAAAf4AAAAAAAAAAAP+AAAAAAAAAAAD/AAAAAAAAAAAB/wAAAAAAAAAAAf+AAAAAAAAAAAH/4AAAAAAAAAAD//8AAAAAAAAAA////4AAAAAAAAP/////gAAAAAAB/////8AAAAAAAf/////AAAAAAAB/////wAAAAAAAP////8AAAAAAAAP////AAAAAAAAAH///wAAAAAAAAAAf/4AAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAHwAAAAAAD8AAAD+AAAAAAB/AAAB/gAAAAAA/4AAA/8AAAAAAf+AAAf/AAAAAAH/gAAH/wAAAAAD/wAAD/8AAAAAB/4AAB//AAAAAAf8AAA//wAAAAAH+AAAf/8AAAAAD/AAAP//AAAAAA/wAAD//wAAAAAP4AAB//8AAAAAD+AAA///AAAAAA/gAAf8/wAAAAAP4AAP+P8AAAAAD/AAP/j/AAAAAA/wAH/w/wAAAAAP+AH/4P8AAAAAD/wD/8D/AAAAAA//P/+A/wAAAAAH////AP+AAAAAB////AB/gAAAAAP///gAf4AAAAAD///wAH+AAAAAAf//wAB/gAAAAAD//4AAf4AAAAAAP/4AAH+AAAAAAA/wAAB/gAAAAAAAAAAAf4AAAAAAAAAAAH+AAAAAAAAAAAA/gAAAAAAAAAAAPwAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAD8AAAAAAAAAAAB/gAAAAAAAAAAAf8AAAAAAAAAAAP+AAAAAAAAAAAD/gAAAAAAAAAAB/wAAAcAAAAAAAf8AAAPwAAAAAAH+AAAD/AAAAAAB/gAAA/4AAAAAA/wAAAP/AAAAAAP8AAAD/4AAAAAD/AB+A//AAAAAA/wA/wH/wAAAAAP4AP8A/+AAAAAD+AD/AD/gAAAAA/gB/wAf8AAAAAP8Af8AH/AAAAAD/AH/AA/wAAAAA/wB/gAP8AAAAAP+Af4AD/AAAAAD/gP+AA/wAAAAAf+H/gAP8AAAAAH///4AD/AAAAAB////AA/wAAAAAP///wAP8AAAAAB///+AD/AAAAAAf///gA/gAAAAAD///+Af4AAAAAAP/n/4f+AAAAAAB/g////AAAAAAAAAP///wAAAAAAAAB///4AAAAAAAAAP//8AAAAAAAAAB///AAAAAAAAAAP//AAAAAAAAAAA//gAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAH+AAAAAAAAAAAH/wAAAAAAAAAAD/+AAAAAAAAAAD//gAAAAAAAAAB//8AAAAAAAAAB///AAAAAAAAAA///wAAAAAAAAAf//8AAAAAAAAAf/7/AAAAAAAAAP/4/4AAAAAAAAP/4H+AAAAAAAAH/8B/gAAAAAAB//8Af4AAAAAAA//+AH+AAAAAAAP//AB/gAAAAAAH//wAf4AAAAAAB///AH+AAAAAAAf//+B/gAAAAAAH///+f4AAAAAAA/////+AAAAAAAH/////wAAAAAAA/////8AAAAAAAAf////8AAAAAAAAf////+AAAAAAAA/////4AAAAAAAA/////AAAAAAAAB////wAAAAAAAAB///8AAAAAAAAAD///AAAAAAAAAA///wAAAAAAAAAP//4AAAAAAAAAD/D8AAAAAAAAAA/wAAAAAAAAAAAH4AAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAf4AP/wAAAAAAAf/AH/+AAAAAAAP/4D//wAAAAAAD//A//+AAAAAAB//wP//wAAAAAAf/8D//8AAAAAAH//g///gAAAAAB//4H//4AAAAAAf//AAf/AAAAAAP9/wAD/wAAAAAD/P+AAf8AAAAAA/j/gAD/AAAAAAP4f4AA/4AAAAAD+H/AAH+AAAAAA/h/wAB/gAAAAAP4P+AAf4AAAAAD+D/gAH+AAAAAA/g/4AA/gAAAAAP4H/AAP4AAAAAD+B/wAD+AAAAAB/gP+AA/gAAAAAf4D/gAP4AAAAAH+Af8AH+AAAAAB/gH/gB/gAAAAAf4B/4Af4AAAAAH+AP/AH8AAAAAB/gB/4D/AAAAAAf4Af/h/wAAAAAD+AD///4AAAAAA/gA///+AAAAAAP4AH///AAAAAAD+AA///gAAAAAA/gAH//wAAAAAAH4AA//4AAAAAAAMAAD/8AAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAH//wAAAAAAAAAH///AAAAAAAAAD///4AAAAAAAAD////gAAAAAAAB////8AAAAAAAA/////gAAAAAAAf////4AAAAAAAH/4B//AAAAAAAD/wAP/4AAAAAAA/4AD/+AAAAAAAf8AB//wAAAAAAH+AAf/8AAAAAAD/AAP//gAAAAAA/wAD//4AAAAAAP8AA/n+AAAAAAD/AAf5/gAAAAAA/wAH8P8AAAAAAP8AB/D/AAAAAAD/AA/w/wAAAAAA/4AP8P8AAAAAAP+AD+D/AAAAAAD/wA/g/wAAAAAAf8AP4P8AAAAAAH+AD+D/AAAAAAA/gA/g/wAAAAAAHwAP8P8AAAAAAAwAD/D/AAAAAAAAAA/x/wAAAAAAAAAP//4AAAAAAAAAD//+AAAAAAAAAAf//AAAAAAAAAAH//wAAAAAAAAAA//4AAAAAAAAAAH/8AAAAAAAAAAB/+AAAAAAAAAAAH/AAAAAAAAAAAAeAAAAAAAD+AAAAAAAAAAAA/gAAAAAAAAAAAP4AAAAAAAAAAAD+AAAAAAAAAAAA/gAeAAAAAAAAAP4AfwAAAAAAAAD+AH8AAAAAAAAA/gB/AAAAAAAAAP4AfwAAAAAAAAD+AH8AAAAAAAAA/gB/AAAAAAAAAP4AfwAAAAAAAAD+AH8AAAAAAAAA/wD/AAAAAAAAAP8A/wAAAAAAAAD/AP8AAAAAAAAA/wD/AAAAAAAAAP8A/wAAAAAAAAD/gP8AAAAAAAAA/4D/AAAAAAAAAH/A/wAAAAAAAAB/4P+AHwAAAAAAf//////AAAAAAD//////wAAAAAA//////8AAAAAAH//////AAAAAAB//////wAAAAAAP/////8AAAAAAB/////+AAAAAAAH/////AAAAAAAAAP+AAAAAAAAAAAB/gAAAAAAAAAAAf4AAAAAAAAAAAH+AAAAAAAAAAAB/gAAAAAAAAAAAf4AAAAAAAAAAAH+AAAAAAAAAAAB/gAAAAAAAAAAAP4AAAAAAAAAAAD+AAAAAAAAAAAA/gAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAD//AAAAAAAAD+D//8AAAAAAAD/9///gAAAAAAB/////8AAAAAAA//////AAAAAAAf/////4AAAAAAH//////AAAAAAD///4H/wAAAAAA///4Af+AAAAAAP4f+AD/gAAAAAH+D/gAf4AAAAAB/A/4AH/AAAAAAfwP+AA/wAAAAAH8D/wAP8AAAAAB/A/8AD/AAAAAAfwP/AAfwAAAAAH+D/wAH8AAAAAB/g/8AB/AAAAAAf4P/AAfwAAAAAH+D/wAH8AAAAAB/w/8AB/AAAAAAP+P+AA/wAAAAAD/x/gAP8AAAAAA///8AD+AAAAAAH///gB/gAAAAAB///4Af4AAAAAAP///gP8AAAAAAB///+H/AAAAAAAP/////gAAAAAAB/////4AAAAAAAH////8AAAAAAAAAH//+AAAAAAAAAA///AAAAAAAAAAD//gAAAAAAAAAAP/wAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAAAB//wAAAAAAAAAA//+AAAAAAAAAAf//wAAAAAAAAAH//8AAAAAAAAAD///gAAAAAAAAA///4AAAAAAAAAf4H/AAAAAAAAAH+B/wAAAAAAAAB/AP8AAAAAAAAA/wD/AAAAAAAAAP4A/wAAAAAAAAD+AP8AAAAAAAAB/gD/AAAAAAAAAf4A/wAAAAAAAAH+AP8AAAAAAAAB/AD/AAAAAAAAAfwB/gAAAAAAAAH8Af4AAAAAAAAB/AP8AAAAAAAAAfwD/AAAAAAAAAH8B/wAAAAAAAAB/Af4AAAAAAAAAf4P8AAAAAAAAAH+H/AAAAAAAAAB/j/gAAAAAAAAAf//////wAAAAAH///////gAAAAA///////8AAAAAP///////AAAAAD///////wAAAAAf//////4AAAAAH//////+AAAAAB///////gAAAAAP//////wAAAAAB/gAAAAAAAAAAAfgAAAAAAAAAAADwAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAACAAAAAAAAAPgAD4AAAAAAAAH8AB/AAAAAAAAB/gAf4AAAAAAAAf4AH+AAAAAAAAH+AB/gAAAAAAAB/gAf4AAAAAAAAf4AH+AAAAAAAAD+AA/gAAAAAAAA/AAPwAAAAAAAADgAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + +exports.add = function(graphics) { + graphics.prototype.setFontGochiHand = function() { + // Actual height 54 (59 - 6) + this.setFontCustom(font, 46, widths, 80+(1<<8)+(1<<16)); + } +}; diff --git a/apps/pastel/f_lato.js b/apps/pastel/f_lato.js new file mode 100644 index 000000000..a7c13fd30 --- /dev/null +++ b/apps/pastel/f_lato.js @@ -0,0 +1,10 @@ + +var widths = atob("DhglJSUlJSUlJSUlEA=="); +var font = atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAHwAAAAAAAAA/gAAAAAAAAH/AAAAAAAAAf8AAAAAAAAB/wAAAAAAAAD+AAAAAAAAAHwAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAB/AAAAAAAAAf8AAAAAAAAP/wAAAAAAAD/8AAAAAAAB//AAAAAAAAf/wAAAAAAAP/4AAAAAAAD/+AAAAAAAA//AAAAAAAAf/wAAAAAAAH/4AAAAAAAD/+AAAAAAAA//AAAAAAAAf/wAAAAAAAH/4AAAAAAAD/+AAAAAAAA//AAAAAAAAf/wAAAAAAAD/4AAAAAAAAP+AAAAAAAAA/AAAAAAAAADwAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//+AAAAAAA////AAAAAAP////gAAAAD/////AAAAA//////AAAAH/////+AAAA//gAH/8AAAH/gAAB/4AAA/4AAAB/wAAD+AAAAB/AAAfwAAAAD+AAB+AAAAAH4AAH4AAAAAfgAAfAAAAAA+AAD8AAAAAD8AAPwAAAAAPwAA/AAAAAA/AAD8AAAAAD8AAPwAAAAAPwAAfAAAAAA+AAB+AAAAAD4AAH4AAAAAfgAAfwAAAAD+AAA/gAAAAfwAAD/gAAAH/AAAH/gAAB/4AAAP/4AB//AAAAf/////4AAAA//////AAAAA/////4AAAAB////+AAAAAA////AAAAAAAf//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAB8AAAAAAAAAPwAAAD4AAAB/AAAAPgAAAP4AAAA+AAAB/AAAAD4AAAP8AAAAPgAAA/gAAAA+AAAH8AAAAD4AAA/gAAAAPgAAH8AAAAA+AAA/gAAAAD4AAH///////gAAf//////+AAB///////4AAH///////gAAf//////+AAB///////4AAAAAAAAAPgAAAAAAAAA+AAAAAAAAAD4AAAAAAAAAPgAAAAAAAAA+AAAAAAAAAD4AAAAAAAAAPgAAAAAAAAA+AAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAADwAAAA+AAAA/AAAAH4AAAP8AAAA/gAAB/wAAAH+AAAP/AAAA/4AAB/4AAAH/gAAH+AAAA/+AAA/gAAAH/4AAD8AAAA/vgAAfgAAAH8+AAB+AAAA/n4AAHwAAAH8fgAA/AAAA/h+AAD8AAAH8H4AAPwAAA/gfgAA/AAAH8B+AAD8AAA/gH4AAPwAAH8AfgAAfAAB/gB+AAB+AAP8AH4AAH8AB/gAfgAAP4Af8AB+AAA/8f/gAH4AAB///8AAfgAAH///gAB+AAAP//4AAH4AAAP//AAAfgAAAf/wAAB+AAAAP4AAAD4AAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAHgAAAAA8AAA/gAAAAPwAAD/AAAAD/AAAP+AAAAf8AAA/8AAAD/wAAB/4AAAf+AAAB/wAAD/gAAAB/AAAP4AAAAD+AAB/AAAAAH4AAH4AAAAAfgAAfgAAAAA+AAB8AAAAAD8AAPwAB4AAPwAA/AAHgAA/AAD8AAfAAD8AAPwAD8AAPwAA/AAPwAA/AAD8AA/AAD4AAHwAH8AAfgAAfgAf4AB+AAB/AD/gAP4AAD+AffAB/AAAP//9/Af8AAAf//n///gAAB//+P//8AAAD//wf//gAAAD/+A//8AAAAH/gB//gAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAB+AAAAAAAAAf4AAAAAAAAD/gAAAAAAAAf+AAAAAAAAH/4AAAAAAAA//gAAAAAAAH++AAAAAAAB/z4AAAAAAAP+PgAAAAAAB/g+AAAAAAAf8D4AAAAAAD/gPgAAAAAAf4A+AAAAAAH/AD4AAAAAA/wAPgAAAAAH+AA+AAAAAB/wAD4AAAAAP8AAPgAAAAB/gAA+AAAAAf8AAD4AAAAD/AAAPgAAAAf4AAA+AAAAB///////4AAH///////gAAf//////+AAB///////4AAH///////gAAAAAAA+AAAAAAAAAD4AAAAAAAAAPgAAAAAAAAA+AAAAAAAAAD4AAAAAAAAAPgAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAADwAAAAAAQAAfgAAAAA/gAB+AAAAD/+AAH8AAAP//4AAPwAAH///gAAfgAAf//+AAB+AAB///4AAH4AAH/wPgAAPgAAfgB8AAA/AAB+AHwAAD8AAH4AfAAAPwAAfgB8AAA/AAB+AHwAAD8AAH4AfAAAPwAAfgB+AAA+AAB+AH4AAD4AAH4AfgAAfgAAfgA/AAD+AAB+AD8AAPwAAH4AP4AD/AAAfgAf4Af4AAB+AB////AAAH4AD///4AAAfAAH///AAAB8AAP//4AAAHwAAf//AAAAAAAAf/wAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAB//gAAAAAAAf//gAAAAAAH///gAAAAAA////AAAAAAP///8AAAAAB//Af4AAAAAf/wAfwAAAAD/8AA/AAAAAf/wAB+AAAAH/+AAH4AAAA/7wAAPgAAAH/PAAA/AAAB/58AAD8AAAP+HwAAPwAAB/wfAAA/AAAf+B8AAD8AAD/wHwAAPwAAf8AfAAA+AAB/gB8AAD4AAH8AH4AAfgAAfgAfgAB+AAB4AA/AAPwAAHAAD+AB/AAAYAAP+Af4AAAAAAf///AAAAAAA///8AAAAAAB///gAAAAAAD//4AAAAAAAH//AAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAH4AAAAAAAAAfgAAAAAAAAB+AAAAAAAAAH4AAAAAAgAAfgAAAAAOAAB+AAAAAD4AAH4AAAAA/gAAfgAAAAP+AAB+AAAAD/4AAH4AAAA//AAAfgAAAP/4AAB+AAAD/+AAAH4AAA//gAAAfgAAP/4AAAB+AAD/+AAAAH4AA//gAAAAfgAP/4AAAAB+AB/+AAAAAH4Af/gAAAAAfgH/4AAAAAB+B/+AAAAAAH4f/gAAAAAAfn/4AAAAAAB//+AAAAAAAH//gAAAAAAAf/4AAAAAAAB/+AAAAAAAAH/gAAAAAAAAf4AAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAwAD/+AAAAA/8Af/8AAAAP/4D//4AAAB//4f//wAAAP//j///gAAB///P8H/AAAP////AH8AAA/gH/wAP4AAH4AH+AAfgAAfgAf4AA+AAB8AA/gAD4AAHwAD8AAPwAA+AAHwAAfAAD4AAfAAB8AAPgAB8AAHwAA+AAHwAAfAAD4AAfAAB8AAHwAD8AAPwAAfAAPwAA+AAB+AB/gAD4AAH4AH+AAfgAAP4B/8AD+AAA/8//4AfwAAB///P8H/AAAD//8///4AAAH//h///AAAAP/4D//4AAAAP/AH//AAAAAHAAP/4AAAAAAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAAP/8AAAAAAAD//4AAAAAAAf//4AAAAAAD///gAAAAAAf///AAAIAAB/gP+AABgAAP4AP4AAeAAA/AAfwAD4AAH4AA/AAfgAAfgAB8AH+AAB8AAHwA/4AAPwAAfAH/gAA/AAB8B/8AAD8AAHwP/AAAPwAAfB/4AAA/AAB8P+AAAD8AAHj/wAAAHwAAef8AAAAfgAD7/gAAAB+AAP/8AAAAD8AB//AAAAAP4AP/4AAAAAf4D/+AAAAAB////wAAAAAD///8AAAAAAH///gAAAAAAH//4AAAAAAAP/+AAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAfAAAAAD8AAD+AAAAAf4AAP4AAAAB/gAB/wAAAAH+AAH/AAAAAf4AAP4AAAAA/AAA/gAAAAB4AAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); + +exports.add = function(graphics) { + graphics.prototype.setFontLato = function() { + // Actual height 50 (53 - 4) + this.setFontCustom(font, 46, widths, 64+(1<<8)+(1<<16)); + } +}; diff --git a/apps/pastel/f_latosmall.js b/apps/pastel/f_latosmall.js new file mode 100644 index 000000000..8ceb61ccf --- /dev/null +++ b/apps/pastel/f_latosmall.js @@ -0,0 +1,10 @@ + +var widths = atob("BAgJDQ0RDwUHBwkNBQgFCA0NDQ0NDQ0NDQ0GBg0NDQkSDw4PEQ0MEBEHCg8LFBESDRIODA0QDxYODg4HCAcNCQcLDAoMDAcLDAYGDAYSDAwMDAkKCAwLEQsLCgcHBw0A"); +var font = atob("AAAAAAAAAAAAAAAAAAAAAAAAEA/84D/zgAAEAAAAAAAAAAAA+AAD8AAAAAAAAAD4AAPgAAAAAAAAAABAADGIAM/gB/8A/+AD8YAAx+AD/4B/4APxgAjGAAIAAAAAAAAAAAAADwMAfg4DnBgMMHg///P/5gMGGAwc4Bg/AEB4AAAAA4AAHwAA5gYDCDgMIcAxjgB84ADnAAA4AAHOABz8AOMYBwwgMDCAgP4AAfAAAAAAAAAAeAAH8APY4B/BgMcGAw4YDBxgMDmA4HwBwPAAB8AAf4ABhgAACAAAAD4AAPgAAAAAAAAAAAAAH/gB//wfAHzgAHAAAAAAAAAAAOAAcfAPwf/8Af+AAAAAAAAAAAANgAAUAABwAAfwAAcAADQAAJAAAAAAAAAAAGAAAYAABgAAGAAP/gA/+AAGAAAYAABgAAGAAAQAAAAAAAEAAA7AAD4AAAAAAAAAAAAGAAAYAABgAAGAAAYAAAgAAAAAAAAAADgAAOAAAYAAAAAAPAAD4AB8AAfAAHwAD4AAeAABAAAADgAB/wAf/wBwHAMAGAwAYDABgMAGA4A4B4PAD/4AH/AAAAAAAAAAAAAYAgDgGAcAYDgBgP/+A//4AABgAAGAAAYAAAAAAAAAAAAQBgHgOAcB4DgPgMA2AwGYDAxgOOGAfwYB+BgBgGAAAAAAAADA4AcDwDgDgMAGAwgYDDBgMcGA5w4B9/ADj4AAAAAAAAABgAAOAAB4AAfgADmAAcYADhgA4GAD//gP/+AAGAAAYAAAgAAAAAADAH4OA/gYDGBgMYGAxgYDGDgMccAw/wCB8AAAAAAAAAAYAAH4AB/wAPjgB8GAOwYDzBgOMGAg44AD/AAH4AACAAAAAAAAAwAADAAAMAGAwB4DAfAMHwAw8ADPAAPwAA+AADgAAAAAAAAAA4+AH38A/44DHBgMMGAwwYDHBgOeOAffwA4/AABwAAAAAAAAAAAAPgAB/AAOMGAww4DBngMF4Aw/ADj4AH+AAPwAAAAAAAAADg4AODgAwGAAAAAAAAAAAABAAAODsA4PgBAYAAAAAAAAAQAABgAAPAAA8AAG4AAZgADHAAMMABgwAAAAAAAAAAAAAAAAEQAAZgABmAAGYAAZgABmAAGYAAZgABmAAGYAAAAAAAAAAAAAAAAGDAAMMAAxwABmAAG4AAPAAA8AABgAAEAAAAAAAAAEAAA4AADABgMHOAw84DGAAP4AAfAAAAAAACAAD/gAePADgGAYAMBh8YMPxgxxGDGEIIYwgxOCDH8IMYRgYBGAwMwD/hAD8AAAAAAAGAAB4AAfgAP4AD+AA/YAPhgA4GAD4YAD9gAD+AAD+AAB+AAB4AABgAAAAAAAD//gP/+AwYYDBhgMGGAwYYDDhgOOGA/84B+/ABh4AAAAAAAAA/gAH/gA+/AHAcA4A4DgBgMAGAwAYDABgMAGA4A4BgDAGAMAAAAAAAAAAAA//4D//gMAGAwAYDABgMAGAwAYDABgOAOAYAwB4PAD/4AH/AAHwAAAAAAAAAAAAP/+A//4DDBgMMGAwwYDDBgMMGAwwYDABgMAGAAAAAAAAAAAA//4D//gMGAAwYADBgAMGAAwYADBgAMGAAwAAAAAAA/gAH/AA++AHAcA4A4DgBgMAGAwAYDABgMGGAwYYDhjgGH8AAfwAAAAAAAAAAAD//gP/+A//4ADAAAMAAAwAADAAAMAAAwAADAAAMAA//4D//gAAAAAAAAAAAAAAA//4D//gAAAAAAAAAAAAAAAAAYAABgAAGAAA4AAHgP/8A//gAAAAAAAAAAAAAAAP/+A//4ADAAAMAAB4AAPwABzgAOHABwPAOAeAwA4CAAgAAAAAAAAAAAP/+A//4AABgAAGAAAYAABgAAGAAAYAABgAAAA//4D//gP/+AeAAAeAAAeAAA+AAA8AAA4AAHgAB4AAeAAHwAA8AAPAAA//4D//gAAAAAAAAAAAAAAA//4D//gHAAAOAAAeAAA8AAA4AABwAADwAADgAAHAP/+A//4AAAAAAAAAAAAP4AD/4AeDwBwHAOAOAwAYDABgMAGAwAYDABgOAOAcBwB4PAD/4AD+AABAAAAAAAAAAAAAP/+A//4DBgAMGAAwYADBgAMGAA44AB/AAH4AAHAAAAAAA/gAP/gB4PAHAcA4A4DABgMAGAwAYDABgMAGA4A4BwHwHg/gP/nAP4MAEAQAAAAAAAAAAA//4D//gMGAAwYADBgAMHAAw/ADneAH4eAPA4AABgAAAAAAwA8DAH4OA5wYDDBgMMGAw4YDBjgOH8AYPgAAIAAAAAwAADAAAMAAAwAADAAAP/+A//4DAAAMAAAwAADAAAMAAAAAAAAAAAAAA//AD//AAAcAAA4AABgAAGAAAYAABgAAOAABwD//AP/4A/8AAAAAOAAA+AAB+AAB/AAA/AAA/AAAeAAD4AA/AAPwAH8AB+AAPgAA4AAAAAAOAAA/AAB/gAA/wAAf4AAPgAB+AA/gAfwAH4AA8AAD8AAD+AAB/AAB/gAA+AAH4AD/AD/gA/wAD4AAMAAAgAYDgDgPAeAeHgAe8AA/AAA4AAHwAB/wAPHgDwPgOAOAgAYAAAAIAAA4AADwAAHwAAHgAAHgAAP+AA/4APgAB4AAeAADwAAMAAAgAAAAAAMAGAwA4DAPgMB+AwPYDDxgMeGAzwYD8BgPgGA8AYDABgAAAAAAAH//8f//xAABEAAEAAAAAAAHAAAPAAAPgAAPgAAHwAAHwAAHgAADAAAAEAAEQAAR///H//8AAAAAAAAAAAAAAABgAAeAADwAA8AADgAAHgAAPAAAOAAAIAAAAAAAAAAAAQAABAAAEAAAQAABAAAEAAAQAABAAAAAAAAgAADAAAOAAAIAAAAAAAAAAAAAAAHAAY+ADnYAMYgAxiADGYAORAAf+AA/4AAAAAAAB//4H//gAYMADAYAMBgAwGADAYAPHgAf8AA/gAAAAAAAAA/gAH/AA4OADAYAMBgAwGADAYAMDgAQEAAAAAB+AAf8ADx4AMBgAwGADAYAMBgAYMB//4H//gAAAAAAAAB8AAf8ADpwAMhgAyGADIYAMhgA6GAB4wADhAAAAACAAAMAAH/+A//4DMAAMwAAzAAABAcAff4D/5gMbmAwmYDCZgMZmA/mYD8fAMA4AgAAAAAAAAAf/+B//4AGAAAwAADAAAMAAA4AAD/4AH/gAAAAAAACAAAc/+Bz/4CAAAAAAAAABiAAGc//5z//CAAAAAAAAAAAAAAf/+B//4AAYAADgAAfAAHuAA4cADAYAIAgAAAAAAAAAAAf/+B//4AAAAAAAAAAAAAAAA/+AD/4AEAAAwAADAAAMAAA/+AB/4AH/gAwAADAAAMAAA4AAD/4AD/gAAAAAAAAAAAA/+AD/4AGAAAwAADAAAMAAAwAAD/4AH/gAAAAAAAAD+AAf8ADg4AMBgAwGADAYAMBgA4OAB/wAD+AADgAAAAAP/+A//4BgwAMBgAwGADAYAMBgA8eAB/wAD8AAAAAAAAAB+AAf8ADx4AMBgAwGADAYAMBgAYMAD//gP/+AAAAAAAAP/gA/+ABwAAOAAAwAADAAAMAAAAAAAAQAHhgA/GADMYAMxgAxmADH4AEPAAAQAAAAAMAAAwAAf/wD//gAwGADAYAMBgAAAAAAAAP/AA/+AAAYAABgAAGAAAYAADAA/+AD/4AAAAAAAADgAAPgAAfgAAPwAAPgAAeAAHwAD8AA/AADgAAIAAA4AAD8AAD+AAB+AAB4AA/AAfgADwAAPgAAfwAAP4AAHgAB+AA/gAPwAA4AAAAAAAAgAwGADh4AHvAAPwAAOAAB8AAe8ADh4AMBgAgCACAAAOAAA+AAA/BgA/eAA/wAD8AA/AAPgAD4AAOAAAgGADA4AMHgAx+ADOYANxgA+GADgYAMBgAAAAAMAD//4f9/xgADEAAEAAAAAAAAAAAAAAB///n//+AAAAAAAAAAAAAABAABGAAMf9/w//+AAwAAAAAAAAAA4AADgAAYAABgAAHAAAMAAAwAADAAAcAADgAAAAAAAA"); + +exports.add = function(graphics) { + graphics.prototype.setFontLatoSmall = function() { + // Actual height 21 (20 - 0) + this.setFontCustom(font, 32, widths, 22+(1<<8)+(1<<16)); + } +}; diff --git a/apps/pastel/f_monoton.js b/apps/pastel/f_monoton.js new file mode 100644 index 000000000..34de6eca1 --- /dev/null +++ b/apps/pastel/f_monoton.js @@ -0,0 +1,7 @@ + +exports.add = function(graphics) { + graphics.prototype.setFontMonoton = function(scale) { + // Actual height 44 (43 - 0) + this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAABmwAAAAAAzYAAAAAAZsAAAAAAM2AAAAAAGbAAAAAADNgAAAAABmwAAAAAAzYAAAAAAZsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAD+AAAAAAf8AAAAAD/ggAAAAf8HwAAAD/g/4AAAf8H/AAAD/g/4OAAf8H/B/AD/g/4P+Af8H/B/wAfg/4P+AAMH/B/wAAA/4H+AAAD/A/4AAAB4H/AAAAAA/4AAAAAH/AAAAAAP4AAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAH//gAAAAf//8AAAA/AAPgAAA8f/x8AAB4//+PAAB5+APxwABzwfwecAAzj//jnAA7n+P85gA7ngAPO4AbnH/xzsAdnP/+c3AN3PAHndgGzOAA5m4DbuAAO7MD9mAADN2Bs3AAB2bA2bAAAbNgbNgAANmwNmwAAGzYGzYAADZsDdmAADN2B+7AABuzAbMwABmbgNneAD3NgHZ3+/3MwBuc//nO4A7nB8HGYAM58AfOcAHeP/+OcABzx/8ecAAc+AA+cAAHH//8cAAB4//48AAAPg+B8AAAD+AP4AAAAP//wAAAAA/+AAAAAAAAAAAAAAAAAAABsAAAAAAA2AAAAAAAbAAAAAAANgAAAAAAGwAAAAAADf////8ABv////+AA3/////AAbAAAAAAAN/////wAG/////4ADYAAAAAABv////+AA3/////AAb/////gANgAAAAAAG/////4ADf////8AAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAADcAAAA2wBs2AADbYA2bAADtsAbZgADm2AftwAHjbAN24AHNtgGzYAPO2wDZsAPebYBs2AOeNsA2bAec22AbNge87bANmwc55tgG7c8542wD9355zbYA2Z5zztsAbODzjm2ANz/nnjbADc/nnhtgBnCPHA2wA74fPAbYAOf+OANsADj8eAG2AA8A+ADbAAP/8AAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAABgG6AAAAuwDdsAAG3YBs2AADZuA27AABu3A/ZgAA7dgbtwAAduwN2w2zG3YGzYbZjZsDZsNsxs2Bs2G2Y2bA2bDbMbNgbNhtmNmwNmw2zGzYG7MbdnbsD939m/d2A2Z/7PM3AbuBtwO7AOz73eeZgDc/9n+dwB3H2Y8cwAZ4HnA84AGf/5/84ADz/OP44AAeALwB4AAH/+//4AAA/+H/wAAADwAfAAAAAAAAAAAAAAAAAAAAAAAZsAAAAAB82AAAAAD+bAAAAAHzNgAAAAPjmwAAAAfHzYAAAB+P5sAAAD8fM2AAAHw+ObAAAPj8fNgAAfH4/mwAA+Ph8zYAAcfH4ZsAAA+Px82AAB8fD+bAAD4+HzNgABh8PhmwAAH4/AzYAAPx+AZsAAPD4AM2AAGHwP+bfgAfgH/NvwA/AABmwAA8AAAzYAAYAA/5t+AAAAf82/AAAAAGbAAAAAADNgAAAAAAAAAAAAAAAAAAAAAAAGAAE///ADAAGf//gBwADP//wCcABmAAADmAAz//8C7gAZ//+DMwAMwAAA3YAGf//hZsADP//xu3ABn//4zdgAzDNsNuwAZhu2GzYAMw2bDZsAGYbNhs2ADMNmw2bABmGzYbNgAzDZsdmwAZhs2M3YAMw3d+zcAGYZm+ZsADMOzgd2ABmDM883AAzB3P87AAZgZx47gAMwOcB5gAGYDn/5gADMA4/zwAAAAPADwAAAAD8fgAAAAAf/gAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///4AAAD////AAAHwAADwAAHH//8eAAHP///ngAHfgAB8wAHeH/8PcADcf//x3ADsf//+ZgBu8AADu4B2c//8zMA3d///M2AbNwAB2bgduxs2bswP2Z2/O3YGzYzbDZsDZsbths2Bs2Nmw2bA2bGzYbNgbNjZsNmwNmxs2GzYH7c2bHbsDtmbtzdmA2bM3fs3AbMHZnO7AM3BuYOZgHZAzP+dgBmAMx+cwA7gHeAcwAMgB3584AHAAc/84ABgAHHx4AAAAB4D4AAAAAf/wAAAAAD/gAAAAAAAAAAAAAAAAAAZsAAAAAAM2AAAAAAGbAAAAAADNgAAAAABmwAAAAAAzYAAAAAAZsAAAAAAM2AAAAPAGbAAAB/gDNgAAP+ABmwAD/wYAzYAf+D8AZsD/wf8AM2f8D/gAGT/gf8HgAf8D/g/wB/g/8H/AA8H/g/4MAA/8H/B+AH/g/4P+AH4H/B/wADA/4P+AAAH/B/wAAA/4P+AAAAfB/wAAAAAP+AAAAAB/wAAAAAD+AAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAGAAwAAAA/8H/gAAB//v/8AAB4B/APgADz+PP54ABn/x//OABng8eDzAB3HHOcdwA3P9z/nYA7P/d/5mAbOBmYO7ANmebvzNwP3fs392YGzc3ZmbsDZsZsxs2Bs2M2Y2bA2bGbMbNgbNjNmNmwNmxmzGzYGzYzZjZsDZsZsxs2Bs2M2Y2bA2bGbMbNgbNzNmNmwP2Zm7s3YDbv7M+7MBszt3OZuA3MGZwd2ANn/uf8zAGY+zn47gDvAc4A7gA78/Pj5gAOf/z/zwADj8cPjwAA+A/gHgAAH/9//gAAA/4P/AAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAD/4AAAAAHw/AAAAAHADwADAAHP8cABgAHf/nAA4AHeB5gDuAHcfOYAzADc/7uBdwBs8ezBmYBu4DdgbsA2Z924s3AbN+bM3bgfsxt2duwNm4zbG3YGzYZtjZsDZsM2xs2Bs2GbY2bA2bDNsbNgbNhv2NmwP242zO3YHbszbm7sBs3AAHZuA2Z///M2Abuf//O7AGzh/8ObgDc8AA+dgB3P//+dwAdx//8cwAGeAAA8wADn///44AA8///54AAPgAAB4AAB+AAPwAAAP///gAAAA//+AAAAAAAAAAAAAAAAAAAAAAAAAAAADbBmwAAABtgzYAAAA2wZsAAAAbYM2AAAANsGbAAAAG2DNgAAADbBmwAAABtgzYAAAA2wZsAAAAAAAAAAAAAAAAAAA="), 46, atob("DRYpFR0eHiImHygmDQ=="), 49+(scale<<8)+(1<<16)); + } +}; diff --git a/apps/pastel/f_orbitron.js b/apps/pastel/f_orbitron.js new file mode 100644 index 000000000..b58056c0e --- /dev/null +++ b/apps/pastel/f_orbitron.js @@ -0,0 +1,11 @@ + + +var widths = atob("ChcmEiUlISUlHiYlCg=="); +var font = atob("AAAAAAAAAAAAAAAAAAAAPAAAAAAB4AAAAAAPAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAPwAAAAAD8AAAAAA+AAAAAAPgAAAAAD4AAAAAB+AAAAAAfgAAAAAHwAAAAAB8AAAAAAfAAAAAAPwAAAAAD4AAAAAA+AAAAAAPgAAAAAH4AAAAAB+AAAAAAfAAAAAAHwAAAAAB8AAAAAA/AAAAAAPwAAAAAD4AAAAAAAAAAAAAAAAAAAAAB///+AAA////8AAP////wAD/////AAfgAA/4ADwAAH/AAeAAB94ADwAAfPAAeAAH54ADwAB+PAAeAAPh4ADwAD4PAAeAA+B4ADwAPgPAAeAD8B4ADwAfAPAAeAHwB4ADwB8APAAeAfAB4ADwH4APAAeB+AB4ADwPgAPAAeD4AB4ADw+AAPAAePwAB4ADz8AAPAAefAAB4AD3wAAPAAf8AAB4AD/////AAf////4AB////+AAH////gAAH///gAAAAAAAAAAAAAAAAAACAAAAAAAwAAAAAAOAAAAAADwAAAAAB+AAAAAAfwAAAAAH4AAAAAB+AAAAAAfgAAAAAD4AAAAAAf////4AD/////AAf////4AD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAf/4AA+AP//AAPwD//4AD+Af//AAfgH4H4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4AD//8APAAf//gB4AB//4APAAH/+AB4AAH+AAHAAAAAAAAAAAAAAAAAAAAAAAAAOAAA4AAHwAAHgAB+AAA+AAfwAAH4AD4AAAfAAeAAAB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAfAPgB4AD//8APAAP////4AA////+AAD////wAAAAP/8AAAAAAAAAAAAAAAAAAAAPgAAAAAD8AAAAAA/gAAAAAP8AAAAAB/gAAAAAf8AAAAAH3gAAAAB88AAAAAfHgAAAAHw8AAAAB+HgAAAAfg8AAAAH4HgAAAA+A8AAAAPgHgAAAD4A8AAAA/AHgAAAPwA8AAAD8AHgAAA/AA8AAAPwAHgAAB8AA8AAAfgAHgAAD/////AAf////4AD/////AAf////4AAAAA8AAAAAAHgAAAAAA8AAAAAAHgAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAD//4BwAAf//APgAD//4B+AAf//AP4AD8H4A/AAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAP//4ADwA///AAeAD//wADwAP/8AAcAAP8AAAAAAAAAAAAAAAAAAAAAAAAAB///+AAA////8AAP////wAD/////AAfg/AH4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAAAPAB4AAAB///AAAAH//4AAAAf/+AAAAB//gAAAAB/gAAAAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAf////4AB/////AAP////4AA/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+D/wAAH////gAB////+AAf////4AD8D+A/AAeAHgB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA8APAAf////4AB/////AAP////wAAf/v/8AAA/gP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAH/+ABgAB//4AOAAf//gB4AD4B8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA+APAAf////4AB////+AAP////wAAf///4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAB4AADwAAPAAAeAAB4AADwAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); + +exports.add = function(graphics) { + // Actual height 32 (35 - 4) + graphics.prototype.setFontOrbitron = function() { + this.setFontCustom(font, 46, widths, 45+(1<<8)+(1<<16)); + } +}; diff --git a/apps/pastel/pastel.app.js b/apps/pastel/pastel.app.js index 1fe3e4a58..aa4f6abf8 100644 --- a/apps/pastel/pastel.app.js +++ b/apps/pastel/pastel.app.js @@ -1,72 +1,96 @@ - -Graphics.prototype.setFontOrbitron = function() { -// Actual height 32 (35 - 4) -var widths = atob("ChcmEiUlISUlHiYlCg=="); -var font = atob("AAAAAAAAAAAAAAAAAAAAPAAAAAAB4AAAAAAPAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAPwAAAAAD8AAAAAA+AAAAAAPgAAAAAD4AAAAAB+AAAAAAfgAAAAAHwAAAAAB8AAAAAAfAAAAAAPwAAAAAD4AAAAAA+AAAAAAPgAAAAAH4AAAAAB+AAAAAAfAAAAAAHwAAAAAB8AAAAAA/AAAAAAPwAAAAAD4AAAAAAAAAAAAAAAAAAAAAB///+AAA////8AAP////wAD/////AAfgAA/4ADwAAH/AAeAAB94ADwAAfPAAeAAH54ADwAB+PAAeAAPh4ADwAD4PAAeAA+B4ADwAPgPAAeAD8B4ADwAfAPAAeAHwB4ADwB8APAAeAfAB4ADwH4APAAeB+AB4ADwPgAPAAeD4AB4ADw+AAPAAePwAB4ADz8AAPAAefAAB4AD3wAAPAAf8AAB4AD/////AAf////4AB////+AAH////gAAH///gAAAAAAAAAAAAAAAAAACAAAAAAAwAAAAAAOAAAAAADwAAAAAB+AAAAAAfwAAAAAH4AAAAAB+AAAAAAfgAAAAAD4AAAAAAf////4AD/////AAf////4AD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAf/4AA+AP//AAPwD//4AD+Af//AAfgH4H4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4AD//8APAAf//gB4AB//4APAAH/+AB4AAH+AAHAAAAAAAAAAAAAAAAAAAAAAAAAOAAA4AAHwAAHgAB+AAA+AAfwAAH4AD4AAAfAAeAAAB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAfAPgB4AD//8APAAP////4AA////+AAD////wAAAAP/8AAAAAAAAAAAAAAAAAAAAPgAAAAAD8AAAAAA/gAAAAAP8AAAAAB/gAAAAAf8AAAAAH3gAAAAB88AAAAAfHgAAAAHw8AAAAB+HgAAAAfg8AAAAH4HgAAAA+A8AAAAPgHgAAAD4A8AAAA/AHgAAAPwA8AAAD8AHgAAA/AA8AAAPwAHgAAB8AA8AAAfgAHgAAD/////AAf////4AD/////AAf////4AAAAA8AAAAAAHgAAAAAA8AAAAAAHgAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAD//4BwAAf//APgAD//4B+AAf//AP4AD8H4A/AAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAP//4ADwA///AAeAD//wADwAP/8AAcAAP8AAAAAAAAAAAAAAAAAAAAAAAAAB///+AAA////8AAP////wAD/////AAfg/AH4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAeAPAB4ADwB4APAAAAPAB4AAAB///AAAAH//4AAAAf/+AAAAB//gAAAAB/gAAAAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAeAAAAAADwAAAAAAf////4AB/////AAP////4AA/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+D/wAAH////gAB////+AAf////4AD8D+A/AAeAHgB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA4APAAeAHAB4ADwA8APAAf////4AB/////AAP////wAAf/v/8AAA/gP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAH/+ABgAB//4AOAAf//gB4AD4B8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA8APAAeAHgB4ADwA+APAAf////4AB////+AAP////wAAf///4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAB4AADwAAPAAAeAAB4AADwAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); -var scale = 1; // size multiplier for this font -g.setFontCustom(font, 46, widths, 45+(scale<<8)+(1<<16)); -}; - -Graphics.prototype.setFontCabinSketch = function() { -// Actual height 48 (51 - 4) -var widths = atob("ECMtGCEiJSIkHyYlDw=="); -var font = atob("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"); -var scale = 1; // size multiplier for this font -g.setFontCustom(font, 46, widths, 65+(scale<<8)+(1<<16)); -}; - -Graphics.prototype.setFontGochiHand = function() { -// Actual height 54 (59 - 6) -var widths = atob("GRMtICcqJiopKiwoGQ=="); -var font = atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAA+AAAAAAAAAAAAfwAAAAAAAAAAAH+AAAAAAAAAAAB/gAAAAAAAAAAAf4AAAAAAAAAAAH+AAAAAAAAAAAB/gAAAAAAAAAAAP4AAAAAAAAAAAD8AAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAAAP+AAAAAAAAAAA//gAAAAAAAAAH//4AAAAAAAAA///+AAAAAAAAP////gAAAAAAB/////4AAAAAAP/////+AAAAAD//////+AAAAA///////wAAAAAf//////AAAAAAP/////4AAAAAAD/////AAAAAAAA////4AAAAAAAAP//+AAAAAAAAAD//gAAAAAAAAAA/8AAAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAH//AAAAAAAAAAH//8AAAAAAAAAH///wAAAAAAAAD///+AAAAAAAAB////wAAAAAAAA////+AAAAAAAAf////gAAAAAAAP/8//8AAAAAAAD/wAf/AAAAAAAB/wAD/4AAAAAAA/4AAP+AAAAAAAP8AAB/wAAAAAAD/AAAf8AAAAAAB/gAAD/AAAAAAAf4AAA/wAAAAAAH8AAAP8AAAAAAB/AAAD/AAAAAAAfwAAA/wAAAAAAP8AAAH8AAAAAAD/AAAD/AAAAAAAf4AAA/wAAAAAAH+AAAP8AAAAAAB/gAAD/AAAAAAAf4AAA/wAAAAAAH/AAAP4AAAAAAB/wAAH+AAAAAAAP+AAB/gAAAAAAD/wAA/wAAAAAAA/+AAf8AAAAAAAH/wAH+AAAAAAAB/+AH/gAAAAAAAP/4D/wAAAAAAAB////4AAAAAAAAf///+AAAAAAAAD////AAAAAAAAAf///gAAAAAAAAD///wAAAAAAAAAP//wAAAAAAAAAA//4AAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAA/gAAAAAAAAAAAf4AAAAAAAAAAAP+AAAAAAAAAAAH/gAAAAAAAAAAB/wAAAAAAAAAAA/4AAAAAAAAAAAf8AAAAAAAAAAAH/AAAAAAAAAAAD/gAAAAAAAAAAA/wAAAAAAAAAAAf4AAAAAAAAAAAP+AAAAAAAAAAAD/AAAAAAAAAAAB/wAAAAAAAAAAAf+AAAAAAAAAAAH/4AAAAAAAAAAD//8AAAAAAAAAA////4AAAAAAAAP/////gAAAAAAB/////8AAAAAAAf/////AAAAAAAB/////wAAAAAAAP////8AAAAAAAAP////AAAAAAAAAH///wAAAAAAAAAAf/4AAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAHwAAAAAAD8AAAD+AAAAAAB/AAAB/gAAAAAA/4AAA/8AAAAAAf+AAAf/AAAAAAH/gAAH/wAAAAAD/wAAD/8AAAAAB/4AAB//AAAAAAf8AAA//wAAAAAH+AAAf/8AAAAAD/AAAP//AAAAAA/wAAD//wAAAAAP4AAB//8AAAAAD+AAA///AAAAAA/gAAf8/wAAAAAP4AAP+P8AAAAAD/AAP/j/AAAAAA/wAH/w/wAAAAAP+AH/4P8AAAAAD/wD/8D/AAAAAA//P/+A/wAAAAAH////AP+AAAAAB////AB/gAAAAAP///gAf4AAAAAD///wAH+AAAAAAf//wAB/gAAAAAD//4AAf4AAAAAAP/4AAH+AAAAAAA/wAAB/gAAAAAAAAAAAf4AAAAAAAAAAAH+AAAAAAAAAAAA/gAAAAAAAAAAAPwAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAD8AAAAAAAAAAAB/gAAAAAAAAAAAf8AAAAAAAAAAAP+AAAAAAAAAAAD/gAAAAAAAAAAB/wAAAcAAAAAAAf8AAAPwAAAAAAH+AAAD/AAAAAAB/gAAA/4AAAAAA/wAAAP/AAAAAAP8AAAD/4AAAAAD/AB+A//AAAAAA/wA/wH/wAAAAAP4AP8A/+AAAAAD+AD/AD/gAAAAA/gB/wAf8AAAAAP8Af8AH/AAAAAD/AH/AA/wAAAAA/wB/gAP8AAAAAP+Af4AD/AAAAAD/gP+AA/wAAAAAf+H/gAP8AAAAAH///4AD/AAAAAB////AA/wAAAAAP///wAP8AAAAAB///+AD/AAAAAAf///gA/gAAAAAD///+Af4AAAAAAP/n/4f+AAAAAAB/g////AAAAAAAAAP///wAAAAAAAAB///4AAAAAAAAAP//8AAAAAAAAAB///AAAAAAAAAAP//AAAAAAAAAAA//gAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAH+AAAAAAAAAAAH/wAAAAAAAAAAD/+AAAAAAAAAAD//gAAAAAAAAAB//8AAAAAAAAAB///AAAAAAAAAA///wAAAAAAAAAf//8AAAAAAAAAf/7/AAAAAAAAAP/4/4AAAAAAAAP/4H+AAAAAAAAH/8B/gAAAAAAB//8Af4AAAAAAA//+AH+AAAAAAAP//AB/gAAAAAAH//wAf4AAAAAAB///AH+AAAAAAAf//+B/gAAAAAAH///+f4AAAAAAA/////+AAAAAAAH/////wAAAAAAA/////8AAAAAAAAf////8AAAAAAAAf////+AAAAAAAA/////4AAAAAAAA/////AAAAAAAAB////wAAAAAAAAB///8AAAAAAAAAD///AAAAAAAAAA///wAAAAAAAAAP//4AAAAAAAAAD/D8AAAAAAAAAA/wAAAAAAAAAAAH4AAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAf4AP/wAAAAAAAf/AH/+AAAAAAAP/4D//wAAAAAAD//A//+AAAAAAB//wP//wAAAAAAf/8D//8AAAAAAH//g///gAAAAAB//4H//4AAAAAAf//AAf/AAAAAAP9/wAD/wAAAAAD/P+AAf8AAAAAA/j/gAD/AAAAAAP4f4AA/4AAAAAD+H/AAH+AAAAAA/h/wAB/gAAAAAP4P+AAf4AAAAAD+D/gAH+AAAAAA/g/4AA/gAAAAAP4H/AAP4AAAAAD+B/wAD+AAAAAB/gP+AA/gAAAAAf4D/gAP4AAAAAH+Af8AH+AAAAAB/gH/gB/gAAAAAf4B/4Af4AAAAAH+AP/AH8AAAAAB/gB/4D/AAAAAAf4Af/h/wAAAAAD+AD///4AAAAAA/gA///+AAAAAAP4AH///AAAAAAD+AA///gAAAAAA/gAH//wAAAAAAH4AA//4AAAAAAAMAAD/8AAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAH//wAAAAAAAAAH///AAAAAAAAAD///4AAAAAAAAD////gAAAAAAAB////8AAAAAAAA/////gAAAAAAAf////4AAAAAAAH/4B//AAAAAAAD/wAP/4AAAAAAA/4AD/+AAAAAAAf8AB//wAAAAAAH+AAf/8AAAAAAD/AAP//gAAAAAA/wAD//4AAAAAAP8AA/n+AAAAAAD/AAf5/gAAAAAA/wAH8P8AAAAAAP8AB/D/AAAAAAD/AA/w/wAAAAAA/4AP8P8AAAAAAP+AD+D/AAAAAAD/wA/g/wAAAAAAf8AP4P8AAAAAAH+AD+D/AAAAAAA/gA/g/wAAAAAAHwAP8P8AAAAAAAwAD/D/AAAAAAAAAA/x/wAAAAAAAAAP//4AAAAAAAAAD//+AAAAAAAAAAf//AAAAAAAAAAH//wAAAAAAAAAA//4AAAAAAAAAAH/8AAAAAAAAAAB/+AAAAAAAAAAAH/AAAAAAAAAAAAeAAAAAAAD+AAAAAAAAAAAA/gAAAAAAAAAAAP4AAAAAAAAAAAD+AAAAAAAAAAAA/gAeAAAAAAAAAP4AfwAAAAAAAAD+AH8AAAAAAAAA/gB/AAAAAAAAAP4AfwAAAAAAAAD+AH8AAAAAAAAA/gB/AAAAAAAAAP4AfwAAAAAAAAD+AH8AAAAAAAAA/wD/AAAAAAAAAP8A/wAAAAAAAAD/AP8AAAAAAAAA/wD/AAAAAAAAAP8A/wAAAAAAAAD/gP8AAAAAAAAA/4D/AAAAAAAAAH/A/wAAAAAAAAB/4P+AHwAAAAAAf//////AAAAAAD//////wAAAAAA//////8AAAAAAH//////AAAAAAB//////wAAAAAAP/////8AAAAAAB/////+AAAAAAAH/////AAAAAAAAAP+AAAAAAAAAAAB/gAAAAAAAAAAAf4AAAAAAAAAAAH+AAAAAAAAAAAB/gAAAAAAAAAAAf4AAAAAAAAAAAH+AAAAAAAAAAAB/gAAAAAAAAAAAP4AAAAAAAAAAAD+AAAAAAAAAAAA/gAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAD//AAAAAAAAD+D//8AAAAAAAD/9///gAAAAAAB/////8AAAAAAA//////AAAAAAAf/////4AAAAAAH//////AAAAAAD///4H/wAAAAAA///4Af+AAAAAAP4f+AD/gAAAAAH+D/gAf4AAAAAB/A/4AH/AAAAAAfwP+AA/wAAAAAH8D/wAP8AAAAAB/A/8AD/AAAAAAfwP/AAfwAAAAAH+D/wAH8AAAAAB/g/8AB/AAAAAAf4P/AAfwAAAAAH+D/wAH8AAAAAB/w/8AB/AAAAAAP+P+AA/wAAAAAD/x/gAP8AAAAAA///8AD+AAAAAAH///gB/gAAAAAB///4Af4AAAAAAP///gP8AAAAAAB///+H/AAAAAAAP/////gAAAAAAB/////4AAAAAAAH////8AAAAAAAAAH//+AAAAAAAAAA///AAAAAAAAAAD//gAAAAAAAAAAP/wAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAAAB//wAAAAAAAAAA//+AAAAAAAAAAf//wAAAAAAAAAH//8AAAAAAAAAD///gAAAAAAAAA///4AAAAAAAAAf4H/AAAAAAAAAH+B/wAAAAAAAAB/AP8AAAAAAAAA/wD/AAAAAAAAAP4A/wAAAAAAAAD+AP8AAAAAAAAB/gD/AAAAAAAAAf4A/wAAAAAAAAH+AP8AAAAAAAAB/AD/AAAAAAAAAfwB/gAAAAAAAAH8Af4AAAAAAAAB/AP8AAAAAAAAAfwD/AAAAAAAAAH8B/wAAAAAAAAB/Af4AAAAAAAAAf4P8AAAAAAAAAH+H/AAAAAAAAAB/j/gAAAAAAAAAf//////wAAAAAH///////gAAAAA///////8AAAAAP///////AAAAAD///////wAAAAAf//////4AAAAAH//////+AAAAAB///////gAAAAAP//////wAAAAAB/gAAAAAAAAAAAfgAAAAAAAAAAADwAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAACAAAAAAAAAPgAD4AAAAAAAAH8AB/AAAAAAAAB/gAf4AAAAAAAAf4AH+AAAAAAAAH+AB/gAAAAAAAB/gAf4AAAAAAAAf4AH+AAAAAAAAD+AA/gAAAAAAAA/AAPwAAAAAAAADgAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); -var scale = 1; // size multiplier for this font -g.setFontCustom(font, 46, widths, 80+(scale<<8)+(1<<16)); -}; - -Graphics.prototype.setFontLatoSmall = function() { -// Actual height 21 (20 - 0) -var widths = atob("BAgJDQ0RDwUHBwkNBQgFCA0NDQ0NDQ0NDQ0GBg0NDQkSDw4PEQ0MEBEHCg8LFBESDRIODA0QDxYODg4HCAcNCQcLDAoMDAcLDAYGDAYSDAwMDAkKCAwLEQsLCgcHBw0A"); -var font = atob("AAAAAAAAAAAAAAAAAAAAAAAAEA/84D/zgAAEAAAAAAAAAAAA+AAD8AAAAAAAAAD4AAPgAAAAAAAAAABAADGIAM/gB/8A/+AD8YAAx+AD/4B/4APxgAjGAAIAAAAAAAAAAAAADwMAfg4DnBgMMHg///P/5gMGGAwc4Bg/AEB4AAAAA4AAHwAA5gYDCDgMIcAxjgB84ADnAAA4AAHOABz8AOMYBwwgMDCAgP4AAfAAAAAAAAAAeAAH8APY4B/BgMcGAw4YDBxgMDmA4HwBwPAAB8AAf4ABhgAACAAAAD4AAPgAAAAAAAAAAAAAH/gB//wfAHzgAHAAAAAAAAAAAOAAcfAPwf/8Af+AAAAAAAAAAAANgAAUAABwAAfwAAcAADQAAJAAAAAAAAAAAGAAAYAABgAAGAAP/gA/+AAGAAAYAABgAAGAAAQAAAAAAAEAAA7AAD4AAAAAAAAAAAAGAAAYAABgAAGAAAYAAAgAAAAAAAAAADgAAOAAAYAAAAAAPAAD4AB8AAfAAHwAD4AAeAABAAAADgAB/wAf/wBwHAMAGAwAYDABgMAGA4A4B4PAD/4AH/AAAAAAAAAAAAAYAgDgGAcAYDgBgP/+A//4AABgAAGAAAYAAAAAAAAAAAAQBgHgOAcB4DgPgMA2AwGYDAxgOOGAfwYB+BgBgGAAAAAAAADA4AcDwDgDgMAGAwgYDDBgMcGA5w4B9/ADj4AAAAAAAAABgAAOAAB4AAfgADmAAcYADhgA4GAD//gP/+AAGAAAYAAAgAAAAAADAH4OA/gYDGBgMYGAxgYDGDgMccAw/wCB8AAAAAAAAAAYAAH4AB/wAPjgB8GAOwYDzBgOMGAg44AD/AAH4AACAAAAAAAAAwAADAAAMAGAwB4DAfAMHwAw8ADPAAPwAA+AADgAAAAAAAAAA4+AH38A/44DHBgMMGAwwYDHBgOeOAffwA4/AABwAAAAAAAAAAAAPgAB/AAOMGAww4DBngMF4Aw/ADj4AH+AAPwAAAAAAAAADg4AODgAwGAAAAAAAAAAAABAAAODsA4PgBAYAAAAAAAAAQAABgAAPAAA8AAG4AAZgADHAAMMABgwAAAAAAAAAAAAAAAAEQAAZgABmAAGYAAZgABmAAGYAAZgABmAAGYAAAAAAAAAAAAAAAAGDAAMMAAxwABmAAG4AAPAAA8AABgAAEAAAAAAAAAEAAA4AADABgMHOAw84DGAAP4AAfAAAAAAACAAD/gAePADgGAYAMBh8YMPxgxxGDGEIIYwgxOCDH8IMYRgYBGAwMwD/hAD8AAAAAAAGAAB4AAfgAP4AD+AA/YAPhgA4GAD4YAD9gAD+AAD+AAB+AAB4AABgAAAAAAAD//gP/+AwYYDBhgMGGAwYYDDhgOOGA/84B+/ABh4AAAAAAAAA/gAH/gA+/AHAcA4A4DgBgMAGAwAYDABgMAGA4A4BgDAGAMAAAAAAAAAAAA//4D//gMAGAwAYDABgMAGAwAYDABgOAOAYAwB4PAD/4AH/AAHwAAAAAAAAAAAAP/+A//4DDBgMMGAwwYDDBgMMGAwwYDABgMAGAAAAAAAAAAAA//4D//gMGAAwYADBgAMGAAwYADBgAMGAAwAAAAAAA/gAH/AA++AHAcA4A4DgBgMAGAwAYDABgMGGAwYYDhjgGH8AAfwAAAAAAAAAAAD//gP/+A//4ADAAAMAAAwAADAAAMAAAwAADAAAMAA//4D//gAAAAAAAAAAAAAAA//4D//gAAAAAAAAAAAAAAAAAYAABgAAGAAA4AAHgP/8A//gAAAAAAAAAAAAAAAP/+A//4ADAAAMAAB4AAPwABzgAOHABwPAOAeAwA4CAAgAAAAAAAAAAAP/+A//4AABgAAGAAAYAABgAAGAAAYAABgAAAA//4D//gP/+AeAAAeAAAeAAA+AAA8AAA4AAHgAB4AAeAAHwAA8AAPAAA//4D//gAAAAAAAAAAAAAAA//4D//gHAAAOAAAeAAA8AAA4AABwAADwAADgAAHAP/+A//4AAAAAAAAAAAAP4AD/4AeDwBwHAOAOAwAYDABgMAGAwAYDABgOAOAcBwB4PAD/4AD+AABAAAAAAAAAAAAAP/+A//4DBgAMGAAwYADBgAMGAA44AB/AAH4AAHAAAAAAA/gAP/gB4PAHAcA4A4DABgMAGAwAYDABgMAGA4A4BwHwHg/gP/nAP4MAEAQAAAAAAAAAAA//4D//gMGAAwYADBgAMHAAw/ADneAH4eAPA4AABgAAAAAAwA8DAH4OA5wYDDBgMMGAw4YDBjgOH8AYPgAAIAAAAAwAADAAAMAAAwAADAAAP/+A//4DAAAMAAAwAADAAAMAAAAAAAAAAAAAA//AD//AAAcAAA4AABgAAGAAAYAABgAAOAABwD//AP/4A/8AAAAAOAAA+AAB+AAB/AAA/AAA/AAAeAAD4AA/AAPwAH8AB+AAPgAA4AAAAAAOAAA/AAB/gAA/wAAf4AAPgAB+AA/gAfwAH4AA8AAD8AAD+AAB/AAB/gAA+AAH4AD/AD/gA/wAD4AAMAAAgAYDgDgPAeAeHgAe8AA/AAA4AAHwAB/wAPHgDwPgOAOAgAYAAAAIAAA4AADwAAHwAAHgAAHgAAP+AA/4APgAB4AAeAADwAAMAAAgAAAAAAMAGAwA4DAPgMB+AwPYDDxgMeGAzwYD8BgPgGA8AYDABgAAAAAAAH//8f//xAABEAAEAAAAAAAHAAAPAAAPgAAPgAAHwAAHwAAHgAADAAAAEAAEQAAR///H//8AAAAAAAAAAAAAAABgAAeAADwAA8AADgAAHgAAPAAAOAAAIAAAAAAAAAAAAQAABAAAEAAAQAABAAAEAAAQAABAAAAAAAAgAADAAAOAAAIAAAAAAAAAAAAAAAHAAY+ADnYAMYgAxiADGYAORAAf+AA/4AAAAAAAB//4H//gAYMADAYAMBgAwGADAYAPHgAf8AA/gAAAAAAAAA/gAH/AA4OADAYAMBgAwGADAYAMDgAQEAAAAAB+AAf8ADx4AMBgAwGADAYAMBgAYMB//4H//gAAAAAAAAB8AAf8ADpwAMhgAyGADIYAMhgA6GAB4wADhAAAAACAAAMAAH/+A//4DMAAMwAAzAAABAcAff4D/5gMbmAwmYDCZgMZmA/mYD8fAMA4AgAAAAAAAAAf/+B//4AGAAAwAADAAAMAAA4AAD/4AH/gAAAAAAACAAAc/+Bz/4CAAAAAAAAABiAAGc//5z//CAAAAAAAAAAAAAAf/+B//4AAYAADgAAfAAHuAA4cADAYAIAgAAAAAAAAAAAf/+B//4AAAAAAAAAAAAAAAA/+AD/4AEAAAwAADAAAMAAA/+AB/4AH/gAwAADAAAMAAA4AAD/4AD/gAAAAAAAAAAAA/+AD/4AGAAAwAADAAAMAAAwAAD/4AH/gAAAAAAAAD+AAf8ADg4AMBgAwGADAYAMBgA4OAB/wAD+AADgAAAAAP/+A//4BgwAMBgAwGADAYAMBgA8eAB/wAD8AAAAAAAAAB+AAf8ADx4AMBgAwGADAYAMBgAYMAD//gP/+AAAAAAAAP/gA/+ABwAAOAAAwAADAAAMAAAAAAAAQAHhgA/GADMYAMxgAxmADH4AEPAAAQAAAAAMAAAwAAf/wD//gAwGADAYAMBgAAAAAAAAP/AA/+AAAYAABgAAGAAAYAADAA/+AD/4AAAAAAAADgAAPgAAfgAAPwAAPgAAeAAHwAD8AA/AADgAAIAAA4AAD8AAD+AAB+AAB4AA/AAfgADwAAPgAAfwAAP4AAHgAB+AA/gAPwAA4AAAAAAAAgAwGADh4AHvAAPwAAOAAB8AAe8ADh4AMBgAgCACAAAOAAA+AAA/BgA/eAA/wAD8AA/AAPgAD4AAOAAAgGADA4AMHgAx+ADOYANxgA+GADgYAMBgAAAAAMAD//4f9/xgADEAAEAAAAAAAAAAAAAAB///n//+AAAAAAAAAAAAAABAABGAAMf9/w//+AAwAAAAAAAAAA4AADgAAYAABgAAHAAAMAAAwAADAAAcAADgAAAAAAAA"); -var scale = 1; // size multiplier for this font -g.setFontCustom(font, 32, widths, 22+(scale<<8)+(1<<16)); -}; - -Graphics.prototype.setFontLato = function() { -// Actual height 50 (53 - 4) -var widths = atob("DhglJSUlJSUlJSUlEA=="); -var font = atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAHwAAAAAAAAA/gAAAAAAAAH/AAAAAAAAAf8AAAAAAAAB/wAAAAAAAAD+AAAAAAAAAHwAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAB/AAAAAAAAAf8AAAAAAAAP/wAAAAAAAD/8AAAAAAAB//AAAAAAAAf/wAAAAAAAP/4AAAAAAAD/+AAAAAAAA//AAAAAAAAf/wAAAAAAAH/4AAAAAAAD/+AAAAAAAA//AAAAAAAAf/wAAAAAAAH/4AAAAAAAD/+AAAAAAAA//AAAAAAAAf/wAAAAAAAD/4AAAAAAAAP+AAAAAAAAA/AAAAAAAAADwAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//+AAAAAAA////AAAAAAP////gAAAAD/////AAAAA//////AAAAH/////+AAAA//gAH/8AAAH/gAAB/4AAA/4AAAB/wAAD+AAAAB/AAAfwAAAAD+AAB+AAAAAH4AAH4AAAAAfgAAfAAAAAA+AAD8AAAAAD8AAPwAAAAAPwAA/AAAAAA/AAD8AAAAAD8AAPwAAAAAPwAAfAAAAAA+AAB+AAAAAD4AAH4AAAAAfgAAfwAAAAD+AAA/gAAAAfwAAD/gAAAH/AAAH/gAAB/4AAAP/4AB//AAAAf/////4AAAA//////AAAAA/////4AAAAB////+AAAAAA////AAAAAAAf//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAB8AAAAAAAAAPwAAAD4AAAB/AAAAPgAAAP4AAAA+AAAB/AAAAD4AAAP8AAAAPgAAA/gAAAA+AAAH8AAAAD4AAA/gAAAAPgAAH8AAAAA+AAA/gAAAAD4AAH///////gAAf//////+AAB///////4AAH///////gAAf//////+AAB///////4AAAAAAAAAPgAAAAAAAAA+AAAAAAAAAD4AAAAAAAAAPgAAAAAAAAA+AAAAAAAAAD4AAAAAAAAAPgAAAAAAAAA+AAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAADwAAAA+AAAA/AAAAH4AAAP8AAAA/gAAB/wAAAH+AAAP/AAAA/4AAB/4AAAH/gAAH+AAAA/+AAA/gAAAH/4AAD8AAAA/vgAAfgAAAH8+AAB+AAAA/n4AAHwAAAH8fgAA/AAAA/h+AAD8AAAH8H4AAPwAAA/gfgAA/AAAH8B+AAD8AAA/gH4AAPwAAH8AfgAAfAAB/gB+AAB+AAP8AH4AAH8AB/gAfgAAP4Af8AB+AAA/8f/gAH4AAB///8AAfgAAH///gAB+AAAP//4AAH4AAAP//AAAfgAAAf/wAAB+AAAAP4AAAD4AAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAHgAAAAA8AAA/gAAAAPwAAD/AAAAD/AAAP+AAAAf8AAA/8AAAD/wAAB/4AAAf+AAAB/wAAD/gAAAB/AAAP4AAAAD+AAB/AAAAAH4AAH4AAAAAfgAAfgAAAAA+AAB8AAAAAD8AAPwAB4AAPwAA/AAHgAA/AAD8AAfAAD8AAPwAD8AAPwAA/AAPwAA/AAD8AA/AAD4AAHwAH8AAfgAAfgAf4AB+AAB/AD/gAP4AAD+AffAB/AAAP//9/Af8AAAf//n///gAAB//+P//8AAAD//wf//gAAAD/+A//8AAAAH/gB//gAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAB+AAAAAAAAAf4AAAAAAAAD/gAAAAAAAAf+AAAAAAAAH/4AAAAAAAA//gAAAAAAAH++AAAAAAAB/z4AAAAAAAP+PgAAAAAAB/g+AAAAAAAf8D4AAAAAAD/gPgAAAAAAf4A+AAAAAAH/AD4AAAAAA/wAPgAAAAAH+AA+AAAAAB/wAD4AAAAAP8AAPgAAAAB/gAA+AAAAAf8AAD4AAAAD/AAAPgAAAAf4AAA+AAAAB///////4AAH///////gAAf//////+AAB///////4AAH///////gAAAAAAA+AAAAAAAAAD4AAAAAAAAAPgAAAAAAAAA+AAAAAAAAAD4AAAAAAAAAPgAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAADwAAAAAAQAAfgAAAAA/gAB+AAAAD/+AAH8AAAP//4AAPwAAH///gAAfgAAf//+AAB+AAB///4AAH4AAH/wPgAAPgAAfgB8AAA/AAB+AHwAAD8AAH4AfAAAPwAAfgB8AAA/AAB+AHwAAD8AAH4AfAAAPwAAfgB+AAA+AAB+AH4AAD4AAH4AfgAAfgAAfgA/AAD+AAB+AD8AAPwAAH4AP4AD/AAAfgAf4Af4AAB+AB////AAAH4AD///4AAAfAAH///AAAB8AAP//4AAAHwAAf//AAAAAAAAf/wAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAB//gAAAAAAAf//gAAAAAAH///gAAAAAA////AAAAAAP///8AAAAAB//Af4AAAAAf/wAfwAAAAD/8AA/AAAAAf/wAB+AAAAH/+AAH4AAAA/7wAAPgAAAH/PAAA/AAAB/58AAD8AAAP+HwAAPwAAB/wfAAA/AAAf+B8AAD8AAD/wHwAAPwAAf8AfAAA+AAB/gB8AAD4AAH8AH4AAfgAAfgAfgAB+AAB4AA/AAPwAAHAAD+AB/AAAYAAP+Af4AAAAAAf///AAAAAAA///8AAAAAAB///gAAAAAAD//4AAAAAAAH//AAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAH4AAAAAAAAAfgAAAAAAAAB+AAAAAAAAAH4AAAAAAgAAfgAAAAAOAAB+AAAAAD4AAH4AAAAA/gAAfgAAAAP+AAB+AAAAD/4AAH4AAAA//AAAfgAAAP/4AAB+AAAD/+AAAH4AAA//gAAAfgAAP/4AAAB+AAD/+AAAAH4AA//gAAAAfgAP/4AAAAB+AB/+AAAAAH4Af/gAAAAAfgH/4AAAAAB+B/+AAAAAAH4f/gAAAAAAfn/4AAAAAAB//+AAAAAAAH//gAAAAAAAf/4AAAAAAAB/+AAAAAAAAH/gAAAAAAAAf4AAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAwAD/+AAAAA/8Af/8AAAAP/4D//4AAAB//4f//wAAAP//j///gAAB///P8H/AAAP////AH8AAA/gH/wAP4AAH4AH+AAfgAAfgAf4AA+AAB8AA/gAD4AAHwAD8AAPwAA+AAHwAAfAAD4AAfAAB8AAPgAB8AAHwAA+AAHwAAfAAD4AAfAAB8AAHwAD8AAPwAAfAAPwAA+AAB+AB/gAD4AAH4AH+AAfgAAP4B/8AD+AAA/8//4AfwAAB///P8H/AAAD//8///4AAAH//h///AAAAP/4D//4AAAAP/AH//AAAAAHAAP/4AAAAAAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAAP/8AAAAAAAD//4AAAAAAAf//4AAAAAAD///gAAAAAAf///AAAIAAB/gP+AABgAAP4AP4AAeAAA/AAfwAD4AAH4AA/AAfgAAfgAB8AH+AAB8AAHwA/4AAPwAAfAH/gAA/AAB8B/8AAD8AAHwP/AAAPwAAfB/4AAA/AAB8P+AAAD8AAHj/wAAAHwAAef8AAAAfgAD7/gAAAB+AAP/8AAAAD8AB//AAAAAP4AP/4AAAAAf4D/+AAAAAB////wAAAAAD///8AAAAAAH///gAAAAAAH//4AAAAAAAP/+AAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAfAAAAAD8AAD+AAAAAf4AAP4AAAAB/gAB/wAAAAH+AAH/AAAAAf4AAP4AAAAA/AAA/gAAAAB4AAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); -var scale = 1; // size multiplier for this font -g.setFontCustom(font, 46, widths, 64+(scale<<8)+(1<<16)); -}; - -Graphics.prototype.setFontArchitect = function() { -// Actual height 40 (41 - 2) -var widths = atob("CBolByEeJykkJCYhCg=="); -var font = atob("AAAAAAAAAAAAAAAAYAAAAAAAADgAAAAAAAAeAAAAAAAAB4AAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAD4AAAAAAAA/AAAAAAAAH4AAAAAAAB/AAAAAAAAf4AAAAAAAD+AAAAAAAA/wAAAAAAAH+AAAAAAAB/gAAAAAAAP8AAAAAAAD/AAAAAAAAf4AAAAAAAH+AAAAAAAA/gAAAAAAAP8AAAAAAAB/AAAAAAAAfwAAAAAAAH8AAAAAAAA/AAAAAAAAPwAAAAAAAB8AAAAAAAAfAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAP/8AAAAAAH//4AAAAAB///wAAAAAf/APgAAAAD/gAeAAAAA/wAA8AAAAH8AABwAAAA/AAAHgAAAHwAAAeAAAA+AAAA4AAADgAAADgAAAcAAAAOAAABwAAAA4AAAOAAAADgAAA4AAAAOAAADgAAAA4AAAOAAAADgAAA4AAAAOAAADgAAAB4AAAOAAAAHAAAA4AAAAcAAADwAAADwAAAHAAAAOAAAAeAAAB4AAAA4AAAPAAAADwAAB4AAAAHwAAPgAAAAPgAD8AAAAAf4D/gAAAAAf//4AAAAAAf/+AAAAAAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAP////4AAAB/////gAAAH////+AAAAf////gAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAADwAADAAAAAeAAAeAAAAD4AAD4AAAAfAAAfgAAAD4AAD+AAAAPAAAf4AAAB8AAH/AAAAHgAA/8AAAAcAAH/wAAADwAA/vAAAAOAAP48AAAA4AB/DgAAADgAf4OAAAAPAD+A4AAAA8A/wHgAAAD8/8AcAAAAH//gBwAAAAP/wAPAAAAAf8AA8AAAAAAAADgAAAAAAAAeAAAAAAAAB4AAAAAAAAHgAAAAAAAA+AAAAAAAAD4AAAAAAAAPAAAAAAAAA8AAAAAAAAHwAAAAAAAAfAAAAAAAAA4AAAAAAAABAAAAAAIAAAAAAAADwAAAAAAAAPAAAAAAAAA8AAAAAAAADgAAAAAAAAeAAAAAAAAB4AYAAAAAAHgBwAAAAAAeAPABAAAADwA8AGAAAAPAHgAYAAAA8AeADgAAADwDwAOAAAAOAPAB4AAAB4B8AHgAAAHgPwA8AAAAeA+ADwAAAB4H4AeAAAAHgfgD4AAAAeD+AfAAAAB4e4D8AAAAHj7gfgAAAAf/PH8AAAAB/4//gAAAAH/D/8AAAAAP4H/gAAAAA+Af8AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAADwAAAAAAAAfAAAAAAAAD8AAAAAAAA/wAAAAAAAH/AAAAAAAA/8AAAAAAAPxwAAAAAAB+HAAAAAAAPwcAAAAAAB+BwAAAAAAfwPAAAAAAD+A8AAAAAAfwDwAAAAAD+APAAAAAAPwA8AAAAAB+ADwAAAAAP/////AAAA/////8AAAB/////wAAAD/////AAAAD////8AAAAAAH8AAAAAAAAeAAAAAAAAB4AAAAAAAAHgAAAAAAAAeAAAAAAAAB4AAAAAAAAHgAAAAAAAAcAAAAAAAABwAAAAAAAAHAAAAAAAAAcAAAAAAAABwAAAAAAAAGAAAAAAAAAAAAAAAAAAOAAAAAAAH/8AAAAAAf//wAAAAAD///AAAAAAP//8AAAAAA///wAAAAAAPgPAB4AAAA+A4APgAAAD4DgA+AAAAPAeAB4AAAA8BwAHgAAADwHAAeAAAAPAcAB4AAAB4BgAHgAAAHgGAAeAAAAeAYAD4AAAB4BgAPAAAAPgGAA8AAAA8AYADwAAADwBwAOAAAAPAHAB4AAAA8AcAHgAAAHwB4A8AAAAeAHgHgAAAB4APh+AAAAHgA//wAAAA+AB/+AAAADwAD/wAAAAPAAD8AAAAA8AAAAAAAAHwAAAAAAAAfAAAAAAAAB4AAAAAAAAHgAAAAAAAAeAAAAAAAAB4AAAAAAAAHAAAAAAAAAcAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAH//AAAAAAB///AAAAAAP//+AAAAAD///8AAAAAf+B/4AAAAD/AA/wAAAA/wAA/gAAAD8AAB+AAAAfAAAD8AAAD4AAAPwAAAfAAAB/AAAB4AAAP+AAAPAAAB/4AAA8AAAP/gAAHgAAB++AAAeAAAPz4AABwAAB+PgAAHAAAPw+AAAcAAA+D4AABgAAHwPgAAAAAA/A+AAAAAAD4H4AAAAAAfAfAAAAAAB4D8AAAAAAPgPgAAAAAA8B+AAAAAADwPwAAAAAAPA+AAAAAAA8P4AAAAAAD//AAAAAAAP/4AAAAAAAf+AAAAAAAA/gAAAAAAAAAAAAAAAIAAAAAAAABwAAAAAAAAHAAAAAAAAAcAAAAAAAABwAAAAAAAAHAAAAAAAAAcAAAAAAAABwAAAAAAAAHAAAAAAAAAcAAAAAAAABwAAAAAAAAHAAAAAAAAAcAAAAAAAADwAAAAAAAAPAAAAAAAAA8AAAAAAAADwAAAAAAAAPAAAP4AAAA8AAP/gAAADwAH/+AAAAfAB//wAAAB8Af//AAAAHwH/4AAAAAfB/4AAAAAB8f8AAAAAAH//AAAAAAAf/wAAAAAAB/8AAAAAAAP/gAAAAAAA/4AAAAAAAD/AAAAAAAAPwAAAAAAAA+AAAAAAAADwAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAH+AAAAAAAA/8AAAAAAAP/4AAAAAfB//gAAAAH/Pw/AAAAA//8A8AAAAH//gDwAAAA//8AHgAAAD4fwAeAAAAeA+AB4AAAB4DwADgAAAPAPAAOAAAA4A4AA4AAADgDgADgAAAOAOAAOAAABwAwAA4AAAHAHAADgAAAcAcAAOAAABwBwAA4AAAHAPAAHgAAAcA8AAcAAABwDgABwAAAHAeAAHAAAAcB8AA4AAABwPwAHgAAAHg/AAcAAAAeH8ADwAAAB4/4AeAAAAD//gD4AAAAP+fA/AAAAAfx//4AAAAAAD//AAAAAAAP/wAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAA/wAAAAAAAH/gAAAAAAA/+AAAAAAAH/8AAAAAAA/nwAAAAAAD4PAAAAAAAeA8AAAAAADwDwAAAAAAPAPAAAAAAB4A8AAwAAAHgDwAHgAAAeAPAAeAAADwA8AD4AAAPADwAfgAAA8AOAB8AAADwA4APwAAAPADgB+AAAA8AeAPwAAAD4B4B/AAAAHgHgf4AAAAfA+D+AAAAA/D5/wAAAAB///+AAAAAH///gAAAAAH//4AAAAAAP/+AAAAAAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAA4AAAAAAAADwDAAAAAAAOAeAAAAAAAYB4AAAAAAAAHgAAAAAAAAMAAAAAAAAAAAAA="); -var scale = 1; // size multiplier for this font -g.setFontCustom(font, 46, widths, 58+(scale<<8)+(1<<16)); -}; - -Graphics.prototype.setFontMonoton = function(scale) { - // Actual height 44 (43 - 0) - g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAABmwAAAAAAzYAAAAAAZsAAAAAAM2AAAAAAGbAAAAAADNgAAAAABmwAAAAAAzYAAAAAAZsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAD+AAAAAAf8AAAAAD/ggAAAAf8HwAAAD/g/4AAAf8H/AAAD/g/4OAAf8H/B/AD/g/4P+Af8H/B/wAfg/4P+AAMH/B/wAAA/4H+AAAD/A/4AAAB4H/AAAAAA/4AAAAAH/AAAAAAP4AAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAH//gAAAAf//8AAAA/AAPgAAA8f/x8AAB4//+PAAB5+APxwABzwfwecAAzj//jnAA7n+P85gA7ngAPO4AbnH/xzsAdnP/+c3AN3PAHndgGzOAA5m4DbuAAO7MD9mAADN2Bs3AAB2bA2bAAAbNgbNgAANmwNmwAAGzYGzYAADZsDdmAADN2B+7AABuzAbMwABmbgNneAD3NgHZ3+/3MwBuc//nO4A7nB8HGYAM58AfOcAHeP/+OcABzx/8ecAAc+AA+cAAHH//8cAAB4//48AAAPg+B8AAAD+AP4AAAAP//wAAAAA/+AAAAAAAAAAAAAAAAAAABsAAAAAAA2AAAAAAAbAAAAAAANgAAAAAAGwAAAAAADf////8ABv////+AA3/////AAbAAAAAAAN/////wAG/////4ADYAAAAAABv////+AA3/////AAb/////gANgAAAAAAG/////4ADf////8AAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAADcAAAA2wBs2AADbYA2bAADtsAbZgADm2AftwAHjbAN24AHNtgGzYAPO2wDZsAPebYBs2AOeNsA2bAec22AbNge87bANmwc55tgG7c8542wD9355zbYA2Z5zztsAbODzjm2ANz/nnjbADc/nnhtgBnCPHA2wA74fPAbYAOf+OANsADj8eAG2AA8A+ADbAAP/8AAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAABgG6AAAAuwDdsAAG3YBs2AADZuA27AABu3A/ZgAA7dgbtwAAduwN2w2zG3YGzYbZjZsDZsNsxs2Bs2G2Y2bA2bDbMbNgbNhtmNmwNmw2zGzYG7MbdnbsD939m/d2A2Z/7PM3AbuBtwO7AOz73eeZgDc/9n+dwB3H2Y8cwAZ4HnA84AGf/5/84ADz/OP44AAeALwB4AAH/+//4AAA/+H/wAAADwAfAAAAAAAAAAAAAAAAAAAAAAAZsAAAAAB82AAAAAD+bAAAAAHzNgAAAAPjmwAAAAfHzYAAAB+P5sAAAD8fM2AAAHw+ObAAAPj8fNgAAfH4/mwAA+Ph8zYAAcfH4ZsAAA+Px82AAB8fD+bAAD4+HzNgABh8PhmwAAH4/AzYAAPx+AZsAAPD4AM2AAGHwP+bfgAfgH/NvwA/AABmwAA8AAAzYAAYAA/5t+AAAAf82/AAAAAGbAAAAAADNgAAAAAAAAAAAAAAAAAAAAAAAGAAE///ADAAGf//gBwADP//wCcABmAAADmAAz//8C7gAZ//+DMwAMwAAA3YAGf//hZsADP//xu3ABn//4zdgAzDNsNuwAZhu2GzYAMw2bDZsAGYbNhs2ADMNmw2bABmGzYbNgAzDZsdmwAZhs2M3YAMw3d+zcAGYZm+ZsADMOzgd2ABmDM883AAzB3P87AAZgZx47gAMwOcB5gAGYDn/5gADMA4/zwAAAAPADwAAAAD8fgAAAAAf/gAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///4AAAD////AAAHwAADwAAHH//8eAAHP///ngAHfgAB8wAHeH/8PcADcf//x3ADsf//+ZgBu8AADu4B2c//8zMA3d///M2AbNwAB2bgduxs2bswP2Z2/O3YGzYzbDZsDZsbths2Bs2Nmw2bA2bGzYbNgbNjZsNmwNmxs2GzYH7c2bHbsDtmbtzdmA2bM3fs3AbMHZnO7AM3BuYOZgHZAzP+dgBmAMx+cwA7gHeAcwAMgB3584AHAAc/84ABgAHHx4AAAAB4D4AAAAAf/wAAAAAD/gAAAAAAAAAAAAAAAAAAZsAAAAAAM2AAAAAAGbAAAAAADNgAAAAABmwAAAAAAzYAAAAAAZsAAAAAAM2AAAAPAGbAAAB/gDNgAAP+ABmwAD/wYAzYAf+D8AZsD/wf8AM2f8D/gAGT/gf8HgAf8D/g/wB/g/8H/AA8H/g/4MAA/8H/B+AH/g/4P+AH4H/B/wADA/4P+AAAH/B/wAAA/4P+AAAAfB/wAAAAAP+AAAAAB/wAAAAAD+AAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAGAAwAAAA/8H/gAAB//v/8AAB4B/APgADz+PP54ABn/x//OABng8eDzAB3HHOcdwA3P9z/nYA7P/d/5mAbOBmYO7ANmebvzNwP3fs392YGzc3ZmbsDZsZsxs2Bs2M2Y2bA2bGbMbNgbNjNmNmwNmxmzGzYGzYzZjZsDZsZsxs2Bs2M2Y2bA2bGbMbNgbNzNmNmwP2Zm7s3YDbv7M+7MBszt3OZuA3MGZwd2ANn/uf8zAGY+zn47gDvAc4A7gA78/Pj5gAOf/z/zwADj8cPjwAA+A/gHgAAH/9//gAAA/4P/AAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAD/4AAAAAHw/AAAAAHADwADAAHP8cABgAHf/nAA4AHeB5gDuAHcfOYAzADc/7uBdwBs8ezBmYBu4DdgbsA2Z924s3AbN+bM3bgfsxt2duwNm4zbG3YGzYZtjZsDZsM2xs2Bs2GbY2bA2bDNsbNgbNhv2NmwP242zO3YHbszbm7sBs3AAHZuA2Z///M2Abuf//O7AGzh/8ObgDc8AA+dgB3P//+dwAdx//8cwAGeAAA8wADn///44AA8///54AAPgAAB4AAB+AAPwAAAP///gAAAA//+AAAAAAAAAAAAAAAAAAAAAAAAAAAADbBmwAAABtgzYAAAA2wZsAAAAbYM2AAAANsGbAAAAG2DNgAAADbBmwAAABtgzYAAAA2wZsAAAAAAAAAAAAAAAAAAA="), 46, atob("DRYpFR0eHiImHygmDQ=="), 49+(scale<<8)+(1<<16)); -} - -Graphics.prototype.setFontSpecialElite = function(scale) { - // Actual height 40 (39 - 0) - g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAYAAAAAAAfwAAAAAAP/AAAAAAH/4AAAAAB/+AAAAAAf/gAAAAAH/4AAAAAB/+AAAAAAf/gAAAAAH/4AAAAAAv8AAAAAAN6AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAfAAAAAAAPwAAAAAAP8AAAAAAH+AAAAAAH+AAAAAAD+AAAAAAD/AAAAAAD/AAAAAAB/AAAAAAB/AAAAAAB/AAAAAAB/gAAAAAB/gAAAAAB/gAAAAAA/gAAAAAB/wAAAAAA/4AAAAAA/wAAAAAA/4AAAAAA/4AAAAAAf8AAAAAAP8AAAAAAD8AAAAAAA8AAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//wAAAAP///gAAAH/9/+AAAD/gAf4AAB/AAB+AAA/AAAHwAAPAAAA+AADgAAAPgAAwAAAD4AAcAAAAfAAHAAAAHwABwAAAB8AA4AAAAfAAOAAAAHwABwAAAB8AAcAAAA/AAHgAAAPgAB+AAAH4AAPgAAD8AAD+AAD+AAA/4Af/AAAB////AAAAP///wAAAAP//gAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAOAAAAHAADgAAAD4AB4AAAA+AAeAAAAPgAHgAAAD4AB4AAAAeAAeAAAAHgAHgAAAB4AB4AAAAcAAeAAAAPAAH4AAP/wAB/////+AAf/////gAH/////4AB///+/+AAAAQAAPgAAAAAAB4AAAAAAAeAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAPgAAAAAADwAAAAAAA+AAAAAAAPgAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAcAAAB+AAfwAAA/wAf/AAA/8Af/wAAf/AP/8AAGPwP//AADh8D48AAA4OB8OAAAOAAfDgAAHAAPg4AABwADwPAAAcAB8DwAAHAAeAeAABwAHAHgAAcADwB8AAHAB4APgAB4A+AB4AAPAfAAeAAD4fgADgAAf/4AA4AAD/8AAeAAAf+AAfAAAB8AAPwAAAAAAD4AAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAf8AAB/wAH/wAAf8AB/8AAH+AAffgAB4AcDx4AAcAPAAfAAHAPwAHwABwHwAA8AAcB+AAPAAHA/gADwABw/4AA8AAcf+AAPAAHP/gAHwABz74AB8AAf8fAA/AAH8DwAPgAD/A8AHwAA/gHwP8AAPwA//+AADwAH//AAAAAA//gAAAAAD/wAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAP8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAB/PAAAAAA/jwAAAAAfg8AAAAAPwPAAAAAH4DwAAAAD4A8HAAAD8APBwAAB+ADw8AAA+AA8PAAA/AAPDgAAPgADw8AAHwAB8/AAD+B///wAA/////8AAP/////AAB+f///wAAAAAHx8AAAAAB8PAAAAAAPDwAAAAADw8AAAAAA4PAAAAAAODwAAAAADgcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAB/gAAH//wf8AAB//+H/gAAf//h/4AAHJ/wf/AABwD4D/4AAeA+AAeAAHgPAAHgAB4DwAB4AAeA4AAeAAHgOAAHgAA4DgAB4AAOA8AAeAADgPAAHgAB4D4ADwAAeAeAB4AAHAHwAeAABgAfAPgAAYAD8fgAAAAA//wAAAAAH/4AAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAA//wAAAAB///AAAAB////AAAB/7//wAAA/AfB+AAAfAPAPwAAPgHgB+AAHwBwAPgAB4A8AB8AAeAPAAfAAPADwAHwADgA8AB8AA8APAAfAAPADwAHwADwA8AB8AA8AHgA+AAP8B4APgAD/wfAH4AA/8D4D8AAH/A///AAB/wH//gAAH8A//wAAA8AH/4AAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAB/4AAAAAA/8AAAAAAP+AAAAAAD+AAAAAAAeAAAAAAAHAAAAAAADwAAAAAAB8AAAAAAAfAAAAAAAHwAAA/8AD+AAB//AA/gAB//wAP4AB//gAB+AB//AAAfwB//AAAH8A/wAAAA/A/wAAAAHw/wAAAAB8/wAAAAAffwAAAAAP/wAAAAAD/4AAAAAA/4AAAAAAP8AAAAAAD4AAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAP/wAAAH8H/+AAAH/z//wAAD////+AAB///h/gAA/B/gH4AAPAP4A/AAHwB8AHwAB4APAB8AAeADgAfAAHgA4ADwABwAOAA8AAcADgAPAAHAA4ADwAB4AeAA8AAfAHwAPAADwB8AHgAA+A/gD4AAPgP4B+AAB+P/h/gAAP////wAAB/8f/4AAAP8D/8AAAAAAf8AAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAA/4APAAAA//gH4AAAP/8D/gAAP4Pg/8AADwB8P/AAB4AfD/4AAeAD4d+AAHAA+AfwADwAHgH8AA4AA8A/AAOAAPAPgADgADwD4AA8AA4B+AAPAAeAfAAB4AHgHgAAeADwD4AAHwA8A8AAA+AfA/AAAHp/h/gAAA3//+gAAAB//+gAAAAd//gAAAACf/wAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAABwB/AAAAB/A/8AAAA/wf/gAAAP+H/4AAAH/h/+AAAB/8f/gAAAP+H/4AAAD/h/+AAAAfwf/gAAAH8C/wAAAAAA3oAAAAAABwAAAAAAAAAAAAAAAAAAAA=="), 46, atob("ERwfHB0cHxsdHB4dEQ=="), 50+(scale<<8)+(1<<16)); -} - +var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); +require("f_latosmall").add(Graphics); const SETTINGS_FILE = "pastel.json"; -let settings = undefined; +const LOCATION_FILE = "mylocation.json"; +let settings; +let location; function loadSettings() { - //Console.log("loadSettings()"); settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; settings.grid = settings.grid||false; - settings.date = settings.date||false; settings.font = settings.font||"Lato"; - //console.log(settings); +} + +// requires the myLocation app +function loadLocation() { + location = require("Storage").readJSON(LOCATION_FILE,1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; +} + +function extractTime(d){ + var h = d.getHours(), m = d.getMinutes(); + return(("0"+h).substr(-2) + ":" + ("0"+m).substr(-2)); +} + +var sunRise = "00:00"; +var sunSet = "00:00"; +var drawCount = 0; + +function updateSunRiseSunSet(now, lat, lon, line){ + // get today's sunlight times for lat/lon + var times = SunCalc.getTimes(new Date(), lat, lon); + + // format sunrise time from the Date object + sunRise = extractTime(times.sunrise); + sunSet = extractTime(times.sunset); +} + +function loadFonts() { + // load font files based on settings.font + if (settings.font == "Architect") + require("f_architect").add(Graphics); + else if (settings.font == "GochiHand") + require("f_gochihand").add(Graphics); + else if (settings.font == "CabinSketch") + require("f_cabin").add(Graphics); + else if (settings.font == "Orbitron") + require("f_orbitron").add(Graphics); + else if (settings.font == "Monoton") + require("f_monoton").add(Graphics); + else if (settings.font == "Elite") + require("f_elite").add(Graphics); + else + require("f_lato").add(Graphics); +} + +function stepsWidget() { + if (WIDGETS.activepedom !== undefined) { + return WIDGETS.activepedom; + } else if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom; + } + return undefined; +} + +const infoData = { + ID_BLANK: { calc: () => '' }, + ID_DATE: { calc: () => {var d = (new Date).toString().split(" "); return d[2] + ' ' + d[1] + ' ' + d[3];} }, + ID_DAY: { calc: () => {var d = require("locale").dow(new Date).toLowerCase(); return d[0].toUpperCase() + d.substring(1);} }, + ID_SR: { calc: () => 'Sunrise: ' + sunRise }, + ID_SS: { calc: () => 'Sunset: ' + sunSet }, + ID_STEP: { calc: () => 'Steps: ' + stepsWidget().getSteps() }, + ID_BATT: { calc: () => 'Battery: ' + E.getBattery() + '%' }, + ID_MEM: { calc: () => {var val = process.memory(); return 'Ram: ' + Math.round(val.usage*100/val.total) + '%';} }, + ID_ID: { calc: () => {var val = NRF.getAddress().split(':'); return 'Id: ' + val[4] + val[5];} }, + ID_FW: { calc: () => 'Fw: ' + process.env.VERSION } +}; + +const infoList = Object.keys(infoData).sort(); +let infoMode = infoList[0]; + +function nextInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === infoList.length - 1) infoMode = infoList[0]; + else infoMode = infoList[idx + 1]; + } +} + +function prevInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === 0) infoMode = infoList[infoList.length - 1]; + else infoMode = infoList[idx - 1]; + } } var mm_prev = "xx"; @@ -149,12 +173,13 @@ function draw() { } } - if (settings.date) { - g.setFontLatoSmall(); - g.setFontAlign(1, -1); - g.drawString(day + " ", w, h - 24 - 24); - g.drawString(month_day + " ", w, h - 24); - } + g.setFontLatoSmall(); + g.setFontAlign(0, -1); + g.drawString((infoData[infoMode].calc()), w/2, h - 24 - 24); + + if (drawCount % 3600 == 0) + updateSunRiseSunSet(new Date(), location.lat, location.lon); + drawCount++; } // Only update when display turns on @@ -168,11 +193,19 @@ Bangle.on('lcdPower', function(on) { draw(); }); +Bangle.setUI("clockupdown", btn=> { + if (btn<0) prevInfo(); + if (btn>0) nextInfo(); + draw(); +}); + loadSettings(); +loadFonts(); +loadLocation(); + g.clear(); var secondInterval = setInterval(draw, 1000); draw(); -// Show launcher when button pressed -Bangle.setUI("clock"); + Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/pastel/pastel.settings.js b/apps/pastel/pastel.settings.js index a8aadd58f..fad36964d 100644 --- a/apps/pastel/pastel.settings.js +++ b/apps/pastel/pastel.settings.js @@ -4,7 +4,6 @@ // initialize with default settings... let s = { 'grid': false, - 'date': false, 'font': "Lato" } @@ -43,14 +42,6 @@ s.grid = !s.grid save() }, - }, - 'Show Date': { - value: s.date, - format: () => (s.date ? 'Yes' : 'No'), - onchange: () => { - s.date = !s.date - save() - }, } }) }) diff --git a/apps/pastel/screenshot_architech.jpg b/apps/pastel/screenshot_architech.jpg deleted file mode 100644 index b13ecc54a..000000000 Binary files a/apps/pastel/screenshot_architech.jpg and /dev/null differ diff --git a/apps/pastel/screenshot_architect.png b/apps/pastel/screenshot_architect.png new file mode 100644 index 000000000..27f74c82f Binary files /dev/null and b/apps/pastel/screenshot_architect.png differ diff --git a/apps/pastel/screenshot_b2_dark.jpg b/apps/pastel/screenshot_b2_dark.jpg deleted file mode 100644 index 3c2ffb7ae..000000000 Binary files a/apps/pastel/screenshot_b2_dark.jpg and /dev/null differ diff --git a/apps/pastel/screenshot_cabinsketch.png b/apps/pastel/screenshot_cabinsketch.png new file mode 100644 index 000000000..d5a90031b Binary files /dev/null and b/apps/pastel/screenshot_cabinsketch.png differ diff --git a/apps/pastel/screenshot_elite.jpg b/apps/pastel/screenshot_elite.jpg deleted file mode 100644 index b881830ed..000000000 Binary files a/apps/pastel/screenshot_elite.jpg and /dev/null differ diff --git a/apps/pastel/screenshot_elite.png b/apps/pastel/screenshot_elite.png new file mode 100644 index 000000000..d2da9842a Binary files /dev/null and b/apps/pastel/screenshot_elite.png differ diff --git a/apps/pastel/screenshot_gochi.jpg b/apps/pastel/screenshot_gochi.jpg deleted file mode 100644 index a3c34e4d4..000000000 Binary files a/apps/pastel/screenshot_gochi.jpg and /dev/null differ diff --git a/apps/pastel/screenshot_gochihand.png b/apps/pastel/screenshot_gochihand.png new file mode 100644 index 000000000..2c69407e2 Binary files /dev/null and b/apps/pastel/screenshot_gochihand.png differ diff --git a/apps/pastel/screenshot_lato.jpg b/apps/pastel/screenshot_lato.jpg deleted file mode 100644 index b99272bf9..000000000 Binary files a/apps/pastel/screenshot_lato.jpg and /dev/null differ diff --git a/apps/pastel/screenshot_lato.png b/apps/pastel/screenshot_lato.png new file mode 100644 index 000000000..56932bd00 Binary files /dev/null and b/apps/pastel/screenshot_lato.png differ diff --git a/apps/pastel/screenshot_monoton.jpg b/apps/pastel/screenshot_monoton.jpg deleted file mode 100644 index 8abfe3bc9..000000000 Binary files a/apps/pastel/screenshot_monoton.jpg and /dev/null differ diff --git a/apps/pastel/screenshot_monoton.png b/apps/pastel/screenshot_monoton.png new file mode 100644 index 000000000..18036e651 Binary files /dev/null and b/apps/pastel/screenshot_monoton.png differ diff --git a/apps/pastel/screenshot_orbitron.png b/apps/pastel/screenshot_orbitron.png new file mode 100644 index 000000000..4e5242ee8 Binary files /dev/null and b/apps/pastel/screenshot_orbitron.png differ diff --git a/apps/pebble/ChangeLog b/apps/pebble/ChangeLog new file mode 100644 index 000000000..fc3ff3ba4 --- /dev/null +++ b/apps/pebble/ChangeLog @@ -0,0 +1,3 @@ +0.01: first release +0.02: included deployment of pebble.settings.js in apps.json +0.03: Changed time+calendar font to LECO1976Regular, changed to slanting boot diff --git a/apps/pebble/LECO 1976-Regular.otf b/apps/pebble/LECO 1976-Regular.otf new file mode 100644 index 000000000..05a318224 Binary files /dev/null and b/apps/pebble/LECO 1976-Regular.otf differ diff --git a/apps/pebble/README.md b/apps/pebble/README.md new file mode 100644 index 000000000..4b0233781 --- /dev/null +++ b/apps/pebble/README.md @@ -0,0 +1,17 @@ +# Pebble + + *a Pebble style clock with configurable background color, to keep the revolution going* + +* Designed specifically for Bangle 2 +* A choice of 6 different background colous through its setting menu. Goto Settings, App/Widget settings, Pebble. +* Supports the Light and Dark themes +* Uses pedometer widget to get latest step count +* Dependant apps are installed when Pebble installs +* Uses the whole screen, widgets are made invisible but still run in the background +* When battery is less than 30% main screen goes Red + +![](pebble_screenshot.png) +![](pebble_screenshot2.png) +![](pebble_screenshot3.png) + +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/pebble/pebble.app.js b/apps/pebble/pebble.app.js new file mode 100644 index 000000000..ce9ab3340 --- /dev/null +++ b/apps/pebble/pebble.app.js @@ -0,0 +1,120 @@ +Graphics.prototype.setFontLECO1976Regular42 = function(scale) { + // Actual height 42 (41 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAA/AAAAAAAAH/AAAAAAAA//AAAAAAAP//AAAAAAB///AAAAAAP///AAAAAB////AAAAAf////AAAAD////4AAAAf////AAAAH////4AAAA////+AAAAA////wAAAAA///+AAAAAA///gAAAAAA//8AAAAAAA//gAAAAAAA/4AAAAAAAA/AAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////gD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4B/gH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/wB////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/wB////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAH+AAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("ERkmHyYmJiYmJCYmEQ=="), 60+(scale<<8)+(1<<16)); +} + +Graphics.prototype.setFontLECO1976Regular22 = function(scale) { + // Actual height 22 (21 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/nA/+cD/5wP/nAAAAAAAAPwAA/gAD+AAPwAAAAAD+AAP4AA/gAAAAAAAAAAAAAcOAP//A//8D//wP//AHDgAcOAP//A//8D//wP//AHDgAAAAAAAAH/jgf+OB/44H/jj8OP/w4//Dj/8OPxw/4HD/gcP+Bw/4AAAAAAAP+AA/8AD/wQOHHA4c8D//wP/8A//gAD4AAfAAH/8A//wP//A84cDjhwIP/AA/8AB/wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8ABwAAAAAAAAD8AAP4AA/gAD8AAAAAAAAAAAEAAD+AB//A///v/D//gB/wABwAAAAAADgAA/wAf/4P8///wf/4AP8AAOAAAAAAAAAyAAHcAAPwAD/gAP/AA/8AA/AAH8AAMwAAAAAAAAAAAAADgAAOAAA4AAf8AD/wAP/AA/8AAOAAA4AADgAAAAAAAAAAD8AAfwAB/AAD8AAAAAAAADgAAOAAA4AADgAAOAAA4AADgAAAAAAAAAADgAAOAAA4AADgAAAAAAAAABwAB/AA/8A//gP/gA/wADwAAIAAAAAAD//wP//A//8D//wOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA4AcDgBwOAHA//8D//wP//A//8AABwAAHAAAcAAAAAAAA+f8D5/wPn/A+f8DhxwOHHA4ccDhxwP/HA/8cD/xwP/HAAAAAAAAOAHA4AcDhxwOHHA4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/wAP/AA/8AD/wAAHAAAcAABwAAHAA//8D//wP//A//8AAAAAAAA/98D/3wP/fA/98DhxwOHHA4ccDhxwOH/A4f8Dh/wOH/AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccDh/wOH/A4f8Dh/wAAAAAAAD4AAPgAA+AADgAAOAAA4AADgAAP//A//8D//wP//AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA//8D//wP//A//8AAAAAAAAOA4A4DgDgOAOA4AAAAAAAAOA/A4H8DgfwOA/AAAAAAAAB4AAPwAA/AAD8AAf4ABzgAPPAA8cAHh4AAAAAAAAAAAAHHAAccABxwAHHAAccABxwAHHAAccABxwAHHAAAAAAAAAOHAA4cADzwAPPAAf4AB/gAD8AAPwAAeAAB4AAAAAAAAA+AAD4AAPgAA+ecDh9wOH3A4fcDhwAP/AA/8AD/wAP/AAAAAAAAAP//4///j//+P//44ADjn/OOf845/zjnHOP8c4//zj//OP/84AAAAAAAP//A//8D//wP//A4cADhwAOHAA4cAD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA//8D//wP9/A/j8AAAAAAAA//8D//wP//A//8DgBwOAHA4AcDgBwOAHA4AcDgBwOAHAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA8A8D//wH/+AP/wAf+AAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4ccDhxwOAHA4AcAAAAAAAA//8D//wP//A//8DhwAOHAA4cADhwAOHAA4cADgAAOAAAAAAD//wP//A//8D//wOAHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA//8D//wP//A//8ABwAAHAAAcAABwAP//A//8D//wP//AAAAAAAAP//A//8D//wP//AAAAAAAAOAHA4AcDgBwOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA//8D//wP//A//8AHwAA/AAP8AB/wAPn/A8f8DB/wIH/AAAAAAAAP//A//8D//wP//AAAcAABwAAHAAAcAABwAAHAAAAAAAAP//A//8D//wP//Af8AAP+AAH/AAD8AAHwAD/AB/wAf8AP+AA//8D//wP//AAAAAAAAP//A//8D//wP//AfwAAfwAAfwAAfwAAfwP//A//8D//wAAAAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHAA4cADhwAOHAA/8AD/wAP/AA/8AAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//+P//4///j//+AAA4AADgAAAP//A//8D//wP//A4eADh+AOH8A4f4D/3wP/HA/8MD/wQAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA4AADgAAOAAA//8D//wP//A//8DgAAOAAA4AADgAAAAAA//8D//wP//A//8AABwAAHAAAcAABwP//A//8D//wP//AAAADAAAPgAA/wAD/4AB/8AA/8AAfwAB/AA/8Af+AP/AA/wAD4AAMAAA4AAD+AAP/gA//8AH/wAB/AAf8Af/wP/4A/4AD/gAP/4AH/8AB/wAB/AB/8D//wP/gA/gADgAAIABA4AcDwDwPw/Afn4Af+AA/wAD/AA//AH5+A/D8DwDwOAHAgAEAAAAP/AA/8AD/wAP/AAAf8AB/wAH/AAf8D/wAP/AA/8AD/wAAAAAAAADh/wOH/A4f8Dh/wOHHA4ccDhxwOHHA/8cD/xwP/HA/8cAAAAAAAAf//9///3///f//9wAA3AADcAAMAAAOAAA/gAD/wAH/8AB/8AA/wAAPAAAEAAAAHAADcAANwAB3///f//9///wAA"), 32, atob("BwYLDg4UDwYJCQwMBgkGCQ4MDg4ODg4NDg4GBgwMDA4PDg4ODg4NDg4GDQ4MEg8ODQ8ODgwODhQODg4ICQg="), 22+(scale<<8)+(1<<16)); +} + +const SETTINGS_FILE = "pebble.json"; +let settings; + +function loadSettings() { + settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'bg': '#0f0', 'color': 'Green'}; +} + +var img = require("heatshrink").decompress(atob("oFAwkEogA/AH4A/AH4A/AH4A/AE8AAAoeXoAfeDQUBmcyD7A+Dh///8QD649CiAfaHwUvD4sEHy0DDYIfEICg+Cn4fHICY+DD4nxcgojOHwgfEIAYfRCIQaDD4ZAFD5r7DH4//kAfRCIZ/GAAnwD5p9DX44fTHgYSBf4ofVDAQEBl4fFUAgfOXoQzBgIfFBAIfPP4RAEAoYAB+cRiK/SG4h/WIBAfXIA7CBAAswD55AHn6fUIBMCD65AHl4gCmcziAfQQJqfQQJpiDgk0IDXxQLRAEECaBM+QgRYRYgUIA0CD4ggSQJiDCiAKBICszAAswD55AHABKBVD7BAFABIqBD5pAFABPxD55AOD6BADiIAJQAyxLABwf/gaAPAH4A/AH4ARA==")); + +const h = g.getHeight(); +const w = g.getWidth(); +const ha = 2*h/5 - 4; +const h2 = 3*h/5 - 10; +const h3 = 7*h/8; + +let batteryWarning = false; + +function draw() { + let date = new Date(); + let da = date.toString().split(" "); + let timeStr = da[4].substr(0,5); + const t = 6; + + // turn the warning on once we have dipped below 30% + if (E.getBattery() < 30) + batteryWarning = true; + + // turn the warning off once we have dipped above 40% + if (E.getBattery() > 40) + batteryWarning = false; + + g.reset(); + g.setColor(settings.bg); + g.fillRect(0, 0, w, h2 - t); + + // contrast bar + g.setColor(g.theme.fg); + g.fillRect(0, h2 - t, w, h2); + + // day and steps + if (settings.color == 'Blue' || settings.color == 'Red') + g.setColor('#fff'); // white on blue or red best contrast + else + g.setColor('#000'); // otherwise black regardless of theme + + g.setFontLECO1976Regular22(); + g.setFontAlign(0, -1); + g.drawString(da[0].toUpperCase(), w/4, ha); // day of week + g.drawString(getSteps(), 3*w/4, ha); + + // time + // white on red for battery warning + g.setColor(!batteryWarning ? g.theme.bg : '#f00'); + g.fillRect(0, h2, w, h3); + + g.setFontLECO1976Regular42(); + g.setFontAlign(0, -1); + g.setColor(!batteryWarning ? g.theme.fg : '#fff'); + g.drawString(timeStr, w/2, h2 + 8); + + // contrast bar + g.setColor(g.theme.fg); + g.fillRect(0, h3, w, h3 + t); + + // the bottom + g.setColor(settings.bg); + g.fillRect(0, h3 + t, w, h); + + g.setColor(settings.bg); + g.drawImage(img, w/2 + ((w/2) - 64)/2, 1, { scale: 1 }); + drawCalendar(((w/2) - 42)/2, 14, 42, 4, da[2]); +} + +// at x,y width:wi thicknes:th +function drawCalendar(x,y,wi,th,str) { + g.setColor(g.theme.fg); + g.fillRect(x, y, x + wi, y + wi); + g.setColor(g.theme.bg); + g.fillRect(x + th, y + th, x + wi - th, y + wi - th); + g.setColor(g.theme.fg); + + let hook_t = 6; + // first calendar hook, one third in + g.fillRect(x + (wi/3) - (th/2), y - hook_t, x + wi/3 + th - (th/2), y + hook_t); + // second calendar hook, two thirds in + g.fillRect(x + (2*wi/3) -(th/2), y - hook_t, x + 2*wi/3 + th - (th/2), y + hook_t); + + g.setFontLECO1976Regular22(); + g.setFontAlign(0, 0); + g.drawString(str, x + wi/2, y + wi/2 + th); +} + +function getSteps() { + if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom.getSteps(); + } + return '????'; +} + +g.clear(); +Bangle.loadWidgets(); +/* + * we are not drawing the widgets as we are taking over the whole screen + * so we will blank out the draw() functions of each widget + */ +for (let wd of WIDGETS) {wd.draw=()=>{};} +loadSettings(); +setInterval(draw, 15000); // refresh every 15s +draw(); +Bangle.setUI("clock"); diff --git a/apps/pebble/pebble.icon.js b/apps/pebble/pebble.icon.js new file mode 100644 index 000000000..ecd7feb7f --- /dev/null +++ b/apps/pebble/pebble.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("oFAwgNKiIAIFqofegIf/DAUzAAMyAwUQD60T/4ACD7Q/cPxIf/YCofcDhYiSXYYfuUZgf/D/4f/D6USkUgD/4fuogAID6vtDw/UD6vu6geF73kb6vuEAtN9wfYMIneD7JADDwIfaIAJdBD7YgBHwQfbAAgfkf6Qf/D/4feogAID6oAND/4f/iAdJD/4f/D/4fUDxYABD74iODiAftTZgfnYYczAAMyD7UT/4ACH/S+bD8DAKD9Y=")) diff --git a/apps/pebble/pebble.png b/apps/pebble/pebble.png new file mode 100644 index 000000000..10f5adb56 Binary files /dev/null and b/apps/pebble/pebble.png differ diff --git a/apps/pebble/pebble.settings.js b/apps/pebble/pebble.settings.js new file mode 100644 index 000000000..b60600316 --- /dev/null +++ b/apps/pebble/pebble.settings.js @@ -0,0 +1,38 @@ +(function(back) { + const SETTINGS_FILE = "pebble.json"; + + // initialize with default settings... + let s = {'bg': '#0f0', 'color': 'Green'} + + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + let settings = storage.readJSON(SETTINGS_FILE, 1) || s; + const saved = settings || {} + for (const key in saved) { + s[key] = saved[key] + } + + function save() { + settings = s + storage.write(SETTINGS_FILE, settings) + } + + var color_options = ['Green','Orange','Cyan','Perple','Red','Blue']; + var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f']; + + E.showMenu({ + '': { 'title': 'Pebble Clock' }, + '< Back': back, + 'Colour': { + value: 0 | color_options.indexOf(s.color), + min: 0, max: 5, + format: v => color_options[v], + onchange: v => { + s.color = color_options[v]; + s.bg = bg_code[v]; + save(); + }, + } + }); +}) diff --git a/apps/pebble/pebble_screenshot.png b/apps/pebble/pebble_screenshot.png new file mode 100644 index 000000000..169df2d22 Binary files /dev/null and b/apps/pebble/pebble_screenshot.png differ diff --git a/apps/pebble/pebble_screenshot2.png b/apps/pebble/pebble_screenshot2.png new file mode 100644 index 000000000..cd09e1c2f Binary files /dev/null and b/apps/pebble/pebble_screenshot2.png differ diff --git a/apps/pebble/pebble_screenshot3.png b/apps/pebble/pebble_screenshot3.png new file mode 100644 index 000000000..0de8df516 Binary files /dev/null and b/apps/pebble/pebble_screenshot3.png differ diff --git a/apps/pipboy/bangle1-pipboy-themed-clock-screenshot.png b/apps/pipboy/bangle1-pipboy-themed-clock-screenshot.png new file mode 100644 index 000000000..9f8d4a3e6 Binary files /dev/null and b/apps/pipboy/bangle1-pipboy-themed-clock-screenshot.png differ diff --git a/apps/pomodo/ChangeLog b/apps/pomodo/ChangeLog index 3630ae7b6..2fedc39e3 100644 --- a/apps/pomodo/ChangeLog +++ b/apps/pomodo/ChangeLog @@ -1,2 +1,2 @@ -0.02: Ported to Banglejs2. 0.01: New App! +0.02: Ported to Banglejs2. diff --git a/apps/pomodo/bangle2-pomodoro-screenshot.png b/apps/pomodo/bangle2-pomodoro-screenshot.png new file mode 100644 index 000000000..df599e314 Binary files /dev/null and b/apps/pomodo/bangle2-pomodoro-screenshot.png differ diff --git a/apps/pong/bangle1-pong-screenshot.png b/apps/pong/bangle1-pong-screenshot.png new file mode 100644 index 000000000..992583e97 Binary files /dev/null and b/apps/pong/bangle1-pong-screenshot.png differ diff --git a/apps/pooqroman/README.md b/apps/pooqroman/README.md new file mode 100644 index 000000000..b41a4a316 --- /dev/null +++ b/apps/pooqroman/README.md @@ -0,0 +1,42 @@ +# pooq Roman: a classic watch face with amusing dynamicity + +This is a normal watch face for telling the time. +It is unusual in that it supports the 24 hour clock by dynamically updating the labels on the face +(so, if you enable 24 hour mode, you will get to see a hand pointing to XXIII o'clock each evening). + +The date and day of the week can also be displayed, and they choose their own spelling depending on the available screen space. It's fun! + +## Options + +Because sometimes I don't want to burn what I'm cooking and other times I'm lazy and just want to know if it's afternoon yet, +you can alter the number of hands on the display. When the watch is unlocked, slide up to add minute and second hands, or down to remove the distraction. +There's also a setting that displays the second hand, but only if the watch is perfectly face-to-the-sky, in case you want +the ability to check the _exact_ time, hands free, without the impact on battery life this usually entails. + +Although we genrally obey the system-wide theming, you can long press on the display for a menu of additional options specific to the face. +You can also override the system 12/24 hour setting just for this face here, since it's, well, a rather different experience than with numeric displays. + +One other thing: there's some integration with system timers and alarms; they will show as small pips at the appropriate places +in the day around the display. When they come within an hour, the pips turn to crosses relating to the minute hand, and the minute +hand turns itself on. When timers are mere seconds away, the display changes again and the second hand activates itself, so you +can watch as your doom approaches. + +## Limitations + +Since this is intended as a design exercise, it does not and will probably never support the Bangle's standard widgets. +Sorry about that, but control of all the pixels was just too important to me. + +There's also no support for internationalisation at present. This irks me, but... well, talk to me about it if there's a language you'd like. + +## The future + +The design is begging for integration with host-device calendars, and proper time zone/DST support. We'll see what the future holds. + +## Feedback + +[I'd be happy to hear your feedback](https://www.github.com/stephenPspackman) if you have comments or find any bugs, or (most especially) +if you find this work interesting. + +## By + +Made by [Stephen P Spackman](https://www.github.com/stephenPspackman). diff --git a/apps/pooqroman/app-icon.js b/apps/pooqroman/app-icon.js new file mode 100644 index 000000000..20a9c8b0a --- /dev/null +++ b/apps/pooqroman/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIAWiEAgIpKEwgrFgAaBgIcBAAwREC4oVBBoQoCAQoXJBogXqI653DC6SnEC9RHXX/6/kSgIAGU5wAICQhfGACAX/C/4AOXIIX/C/4X/C/4XUgEBF6wYHI6AYGL6MACIgXRCIISDR6QYEU6YYDX6gYCAAKxHDB4XTDAYXUL6oAgA==")) diff --git a/apps/pooqroman/app.js b/apps/pooqroman/app.js new file mode 100644 index 000000000..d25fcf1a8 --- /dev/null +++ b/apps/pooqroman/app.js @@ -0,0 +1,761 @@ +// pooqRoman +// +// Copyright (c) 2021 Stephen P Spackman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Notes: +// +// This only works for Bangle 2. + +////////////////////////////////////////////////////////////////////////////// +/* System integration */ + +const storage = require('Storage'); + +const settings = storage.readJSON("setting.json", true) || {}; + +const alarms = storage.readJSON('alarm.json', true) || []; + +/* + { on : true, + hr : 6.5, // hours + minutes/60 + msg : "Eat chocolate", + last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day! + rp : true, // repeat + as : false, // auto snooze + timer : 5, // OPTIONAL - if set, this is a timer and it's the time in minutes + } +*/ + +////////////////////////////////////////////////////////////////////////////// +/* Face-specific options */ + +class Options { + // Protocol: subclasses must have static id and defaults fields. + // Only fields named in the defaults will be saved. + constructor() { + this.id = this.constructor.id; + this.file = `${this.id}.json`; + this.backing = storage.readJSON(this.file, true) || {}; + this.defaults = this.constructor.defaults; + Object.keys(this.defaults).forEach(k => this.bless(k)); + } + + writeBack(delay) { + if (this.timeout) clearTimeout(this.timeout); + this.timeout = setTimeout( + () => { + this.timeout = null; + storage.writeJSON(this.file, this.backing); + }, + delay + ); + } + + bless(k) { + Object.defineProperty(this, k, { + get: () => this.backing[k] == null ? this.defaults[k] : this.backing[k], + set: v => { + this.backing[k] = v; + // Ten second writeback delay, since the user will roll values up and down. + this.writeBack(10000); + } + }); + } + + showMenu(m) { + if (m) { + for (const k in m) if ('init' in m[k]) m[k].value = m[k].init(); + m[''].selected = -1; // Workaround for self-selection bug. + } + E.showMenu(m); + } + + reset() { + this.backing = {}; + this.writeBack(0); + } + + interact() {this.showMenu(this.menu);} +} + +class RomanOptions extends Options { + constructor() { + super(); + this.menu = { + '': {title: '* face options *'}, + '< Back': _ => {this.showMenu(); this.emit('done');}, + Ticks: { + init: _ => this.resolution, + min: 0, max: 3, + onchange: x => this.resolution = x, + format: x => ['seconds', 'seconds (up)', 'minutes', 'hours'][x] + }, + 'Display': { + init: _ => this.o24h == null ? 0 : 1 + this.o24h, + min: 0, max: 2, + onchange: x => this.o24h = [null, 0, 1][x], + format: x => ['system', '12h', '24h'][x] + }, + 'Day of Week': { + init: _ => this.dow, + onchange: x => this.dow = x + }, + Calendar: { + init: _ => this.calendric, + min: 0, max: 2, + onchange: x => this.calendric = x, + format: x => ['none', 'day', 'date'][x] + }, + Defaults: _ => {this.reset();} + }; + } +} + +RomanOptions.id = 'pooqroman'; + +RomanOptions.defaults = { + resolution: 1, + dow: true, + calendric: 2, + o24h: !settings["12hour"], + bg: g.theme.bg, + fg: g.theme.fg, + barBg: g.theme.fg, + barFg: g.theme.bg, + hourFg: g.theme.fg, + minuteFg: g.theme.fg, + secondFg: g.theme.fg2, + rectFg: g.theme.fg, + hubFg: g.theme.fg, + alarmFg: '#f00', + timerFg: '#0f0', + active: g.theme.fg2, +}; + +////////////////////////////////////////////////////////////////////////////// +/* Assets (generated by resourcer.js, in this directory) */ + +const heatshrink = require('heatshrink'); +const dec = x => E.toString(heatshrink.decompress(atob(x))); +const romanPartsF = [ + dec( + 'wEBsEB3//7//9//+0AjUAguAg3AgYQJjfAgv+gH/8Fg/0gh/AgP4gf2h/j/+BCAP' + + 'wgFggEggEQgEMgEHwEDEIIyDuED3kD7+H9vn2k/hEPgMP4Xevd+j4QB7kA9kAmkA' + + 'hUGgOH8Hn3le4+GgH32PuvfGj+CCAMDgXD4dz+evt9DgcL7fXn87h8NCAMP+Ef/0' + + 'eg+egPugF2j0bCAPAh3wh88h8P/8BNwI' + ), 97, dec('gUDgUGgUJgYFBhsBhMJhgA=='), 17 +];const fontF = [ + dec( + 'AAUwAIM/4F/8HguHAmABBAoIJBBoIUBkEwsEw//wAIIdDBoUQBoIfC+HB+Hj2F/m' + + 'E+CIXAoHEsHMuHcmH8mHuuHH8GBGIUAwEBwEHwH/wH5+EBAIILCCAP8oH8EYXMmA' + + 'BB5wjCgYjCAYMP8E+uF8mHsCIWHCIgCBAIXw4fw54tBgBsBGgUAnKLC99w40wAII' + + 'FBBIINBCIM8gF+iHnmHDuHD8HnDYMAjizEMYJJBn+A+OAAYIHBBYKjDXYKvDYZYP' + + 'D40AAIYMBZYgkC4Hg4DnDuH/8H/BYIVCv/wnEAjwBCAoIJBEIYRFh0Ag8AgPAEYQ' + + 'RCJIJNBfYRXKnFAvlg9ihE8dwsfgkLFHMYgJF8DNCh+AUYWAA4ILBAAJGB/4PB+D' + + '9CgADCEoIPCJobbBB4IBBAoJdDEgXggvwhuwAIcH8EDRIh/BhkwAIMOuAPCMYQDB' + + 'A4ILBCIcGsECoAPLU4oPDH42ggeAB4XEg/mh1zhkzh03g/+h/4J4nwg0AhjbDRII' + + 'vCt/wAIIVFAoKTBCYIXBDIYHHEIYVFGJJxHSI8P/8H/6hLF44BBM4IABg8gh6NEh' + + 'vwgngBoITBv/Av7PBV4kAsArCfYIVBuEABYNwA4I3BD4cPL4UAM4IXBBYQfC4kP8' + + '0AucAmcAu8PXogA=' + ), 32, dec('gINMgUAhMHhIAGCQ0KAQIKBgwEBgcIBAQVEhIJBhAeIBQIADAoUDEQULBQcHg4FD' + + 'CII='), 16 +]; +const lockI = dec('iMSwMAgfwgf8geHgeB4PA8HguFwnH//9//+4gPf//v//3gE7//9//+8EHCAO///A'); +const batteryI = dec('h8SwMAgPggfAv/4//x//j//H/+P/8f/0//gOOA=='); +const chargeI = dec('h0MwIEBkEBwEMgFwgeAj/w/+AjkA8EDgEYgFAA=='); +const GPSI = dec('iUQwMAhEAgsAgUggFEgEKvEBn0Aj+AgfgglygsJosgxNGiNIgWJ4FBEoM4gA'); +const HRMI = dec('iMRwMAnken8fzfd7v+/3/v9/38/z+b5tiiM3/eP/+D/+AAIM/wEPwEDwEAAIIA=='); +const compassI = dec('iMRwMAgfgg/8g8ng0Q40ImcOjcHg+DwfB4Ph2Hw7FsolmkUxwEwuFwj/wEIMAA=='); + +////////////////////////////////////////////////////////////////////////////// +/* Squeezable strings */ + +class Formattable { + width(g) {return this.w != null ? this.w : (this.w = g.stringWidth(this.text));} + print(g, x, y) {g.drawString(this.text, x, y); return this.width();} +} + +class Fixed extends Formattable { + constructor(text) { + super(); + this.text = text; + } + squeeze() {return false;} +} + +class Squeezable extends Formattable { + constructor(named, index) { + super(); + this.named = named; + this.index = index; + this.end = index + named.forms; + } + squeeze() { + if (this.index >= this.end) return false; + this.index++; + this.w = null; + return true; + } + get text() {return this.named.table[this.index];} +} + +class Named { + constructor(forms, table) { + this.forms = forms; + this.table = table; + } + on(index) {return new Squeezable(this, this.forms * index);} +} + +////////////////////////////////////////////////////////////////////////////// +/* Face */ + +// Static geometry +const barW = 26, barH = g.getHeight(), barX = g.getWidth() - barW, barY = 0; +const faceW = g.getWidth() - barW, faceH = g.getHeight(); +const faceX = 0, faceY = 0, faceCX = faceW / 2, faceCY = faceH / 2; +const rectX = faceX + 35, rectY = faceY + 24, rectW = 80, rectH = 128; + +// Extended-Roman-numeral labels +const layout = E.toUint8Array( + 75, 23, // XII + 132, 24, // I + 132, 61, // II + 132, 97, // III + 132, 133, // IV + 132, 170, // V + 75, 171, // VI + 18, 170, // VII + 18, 133, // VIII + 18, 97, // IX + 18, 61, // X + 18, 24 // XI +); + +const numeral = (n, options) => [ + 'n', // 0 + 'abc', // I + 'abdc', // II + 'abddc', // III + 'abefg', // IV + 'hfg', // V + 'hfibc', // VI + 'hfibdc', // VII + 'hfibddc', // VIII + 'abjk', // IX + 'kjk', // X + 'kjbc', // XI + 'kjbdc', // XII + 'kjbddc', // XIII + 'kjbefg', // XIV + 'kjefg', // XV + 'labc', // XVI + 'labdc', // XVII + 'labddc', // XVIII + 'kjbjk', // XIX + 'kjjk', // XX + 'mabc', // XXI + 'mabdc', // XXII + 'mabddc', // XXIII +][options.o24h ? n % 24 : (n + 11) % 12 + 1]; + +const formatMonth = new Named(4, [ + 'January', 'Jan.', 'Jan', 'I', + 'February', 'Feb.', 'Feb', 'II', + 'March', 'Mar.', 'Mar', 'III', + 'April', 'Apr.', 'Apr', 'IV', + 'May', 'May', 'May', 'V', + 'June', 'June', 'Jun', 'VI', + 'July', 'July', 'Jul', 'VII', + 'August', 'Aug.', 'Aug', 'VIII', // VIII *is* narrower than Aug, our I is thin. + 'September', 'Sept.', 'Sep', 'IX', + 'October', 'Oct.', 'Oct', 'X', + 'November', 'Nov.', 'Nov', 'XI', + 'December', 'Dec.', 'Dec', 'XII' +]); +const formatDom = { + on: d => new Fixed(d.toString()) +}; +const formatDow = new Named(4, [ + 'Sunday', 'Sun.', 'Sun', 'Su', + 'Monday', 'Mon.', 'Mon', 'M', + 'Tuesday', 'Tues.', 'Tue', 'Tu', + 'Wednesday', 'Weds.', 'Wed', 'W', + 'Thursday', 'Thurs.', 'Thu', 'Th', + 'Friday', 'Fri.', 'Fri', 'F', + 'Saturday', 'Sat.', 'Sat', 'Sa' +]); + +const hceil = x => Math.ceil(x / 3600000) * 3600000; +const hfloor = x => Math.floor(x / 3600000) * 3600000; +const isString = x => typeof x == 'string'; +const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width; +const imageHeight = i => isString(i) ? i.charCodeAt(1) : i.height; + +const events = { + // Items are {time: number, wall: boolean, priority: number, + // past: bool, future: bool, precision: number, + // colour: colour, dramatic?: bool, event?: any} + fixed: [{time: Number.POSITIVE_INFINITY}], // indexed by ms absolute + wall: [{time: Number.POSITIVE_INFINITY}], // indexed by nominal ms + TZ ms + + clean: function(now, l) { + let o = now.getTimezoneOffset() * 60000; + let tf = now.getTime() + l, tw = tf - o; + // Discard stale events: + while (this.wall[0].time <= tw) this.wall.shift(); + while (this.fixed[0].time <= tf) this.fixed.shift(); + }, + + scan: function(now, from, to, f) { + result = Infinity; + let o = now.getTimezoneOffset() * 60000; + let t = now.getTime() - o; + let c, p, i, l = from - o, h = to - o; + for (i = 0; (c = this.wall[i]).time < l; i++) ; + for (; (c = this.wall[i]).time < h; i++) { + if ((p = c.time < t) ? c.past : c.future) + result = Math.min(result, f(c, new Date(c.time + o), p)); + } + l += o; h += o; t += o; + for (i = 0; (c = this.fixed[i]).time < l; i++) ; + for (; (c = this.fixed[i]).time < h; i++) { + if ((p = c.time < t) ? c.past : c.future) + result = Math.min(f(c, new Date(c.time), p)); + } + return result; + }, + + span: function(now, from, to, width) { + let o = now.getTimezoneOffset() * 60000; + let t = now.getTime() - o; + let lfence = [], rfence = []; + this.scan(now, from, to, (e, d, p) => { + if (p) { + for (let j = 0; j <= e.priority; j++) { + if (d < (lfence[e.priority] || t)) lfence[e.priority] = d; + } + } else { + for (let j = 0; j <= e.priority; j++) { + if (d > (rfence[e.priority] || t)) rfence[e.priority] = d; + } + } + }); + for (let j = 0; ; j += 0.5) { + if ((rfence[Math.ceil(j)] - lfence[Math.floor(j)] || 0) <= width) { + return [lfence[Math.floor(j)] || now, rfence[Math.ceil(j)] || now]; + } + } + }, + + insert: function(t, wall, e) { + let v = wall ? this.wall : this.fixed; + e.time = t = t - (wall ? t.getTimezoneOffset() * 60000 : 0); + v.splice(v.findIndex(x => x.time > t), 0, e); + }, + + loadFromSystem: function(options) { + alarms.forEach(x => { + if (x.on) { + const t = new Date(); + let h = x.hr; + let m = h % 1 * 60; + let s = m % 1 * 60; + let ms = s % 1 * 1000; + t.setHours(h - h % 1, m - m % 1, s - s % 1, ms); + // There's a race condition here, but I'm not sure what we can do about it. + if (t < Date.now() || x.last === t.getDate()) t.setDate(t.getDate() + 1); + this.insert(t, true, { + priority: 0, + past: false, // System alarms seem uninteresting if past? + future: true, + precision: x.timer ? 1000 : 60000, + colour: x.timer ? options.timerFg : options.alarmFg, + event: x + }); + } + }); + return this; + }, +}; + +////////////////////////////////////////////////////////////////////////////// +/* The main face logic */ + +class Sidebar { + constructor(g, x, y, w, h, options) { + this.g = g; + this.options = options; + this.x = x; + this.y = this.initY = y; + this.h = h; + this.rate = Infinity; + this.doLocked = Sidebar.status(_ => Bangle.isLocked(), lockI); + this.doHRM = Sidebar.status(_ => Bangle.isHRMOn(), HRMI); + this.doGPS = Sidebar.status(_ => Bangle.isGPSOn(), GPSI, Sidebar.gpsColour(options)); + } + reset(rate) {this.y = this.initY; this.rate = rate; return this;} + print(t) { + this.y += 4 + t.print( + this.g.setColor(this.options.barFg).setFontAlign(-1, 1, 1), + this.x + 3, this.y + 4 + ); + return this; + } + pad(n) {this.y += n; return this;} + free() {return this.h - this.y;} + static status(p, i, c) { + return function() { + if (p()) { + this.g.setColor(c ? c() : this.options.barFg) + .drawImage(i, this.x + 4, this.y += 4); + this.y += imageHeight(i); + } + return this; + }; + } + static gpsColour(o) { + const fix = Bangle.getGPSFix(); + return fix && fix.fix ? o.active : o.barFg; + } + doPower() { + const c = Bangle.isCharging(); + const b = E.getBattery(); + if (c || b < 50) { + let g = this.g, x = this.x, y = this.y, options = this.options; + g.setColor(options.barFg).drawImage(batteryI, x + 4, y + 4); + g.setColor(b <= 10 ? '#f00' : b <= 30 ? '#ff0' : '#0f0'); + const h = 13 * (100 - b) / 100; + g.fillRect(x + 8, y + 7 + h, x + 17, y + 20); + // Espruino disallows blank leading rows in icons, for some reason. + if (c) g.setColor(options.barBg).drawImage(chargeI, x + 4, y + 8); + this.y = y + imageHeight(batteryI) + 4; + } + return this; + } + doCompass() { + if (Bangle.isCompassOn()) { + const c = Bangle.getCompass(); + const a = c && this.rate <= 1000; + this.g.setColor(a ? this.options.active : this.options.barFg).drawImage( + compassI, + this.x + 4 + imageWidth(compassI) / 2, + this.y + 4 + imageHeight(compassI) / 2, + a ? {rotate: c.heading / 180 * Math.PI} : undefined + ); + this.y += 4 + imageHeight(compassI); + } + return this; + } +} + +class Roman { + constructor(g, events) { + this.g = g; + this.state = {}; + const options = this.options = new RomanOptions(); + this.events = events.loadFromSystem(this.options); + this.timescales = [1000, [1000, 60000], 60000, 3600000]; + this.sidebar = new Sidebar(g, barX, barY, barW, barH, options); + this.hours = Roman.hand(g, 3, 0.5, 12, _ => options.hourFg); + this.minutes = Roman.hand(g, 2, 0.9, 60, _ => options.minuteFg); + this.seconds = Roman.hand(g, 1, 0.9, 60, _ => options.secondFg); + } + + reset() {this.state = {}; this.g.clear(true);} + + doIcons(which) {this.state.iconsOk = null;} + + // Watch hands. These could be improved, graphically. + // If we restricted them to 60 positions, we could feasibly hand-draw them? + static hand(g, w, l, d, c) { + return p => { + g.setColor(c()); + p = ((12 * p / d) + 1) % 12; + let h = l * rectW / 2; + let v = l * rectH / 2; + let poly = + p <= 2 ? [faceCX + w, faceCY, faceCX - w, faceCY, + faceCX + h * (p - 1), faceCY - v, + faceCX + h * (p - 1) + 1, faceCY - v] + : p < 6 ? [faceCX + 1, faceCY + w, faceCX + 1, faceCY - w, + faceCX + h, faceCY + v / 2 * (p - 4), + faceCX + h, faceCY + v / 2 * (p - 4) + 1] + : p <= 8 ? [faceCX - w, faceCY + 1, faceCX + w, faceCY + 1, + faceCX - h * (p - 7), faceCY + v, + faceCX - h * (p - 7) - 1, faceCY + v] + : [faceCX, faceCY - w, faceCX, faceCY + w, + faceCX - h, faceCY - v / 2 * (p - 10), + faceCX - h, faceCY - v / 2 * (p - 10) - 1]; + g.fillPoly(poly); + }; + } + + static pos(p, r) { + let h = r * rectW / 2; + let v = r * rectH / 2; + p = (p + 1) % 12; + return p <= 2 ? [faceCX + h * (p - 1), faceCY - v] + : p < 6 ? [faceCX + h, faceCY + v / 2 * (p - 4)] + : p <= 8 ? [faceCX - h * (p - 7), faceCY + v] + : [faceCX - h, faceCY - v / 2 * (p - 10)]; + } + + alert(e, date, now, past) { + const g = this.g; + g.setColor(e.colour); + const dt = date - now; + if (e.precision < 60000 && dt >= 0 && e.future && dt <= 59000) { // Seconds away + const p = Roman.pos(date.getSeconds() / 5, 0.95); + g.drawLine(faceCX, faceCY, p[0], p[1]); + return 1000; + } else if (e.precision < 3600000 && dt >= 0 && e.future && dt <= 3540000) { // Minutes away + const p = Roman.pos(date.getMinutes() / 5 + date.getSeconds() / 300, 0.8); + g.drawLine(p[0] - 5, p[1], p[0] + 5, p[1]); + g.drawLine(p[0], p[1] - 5, p[0], p[1] + 5); + return dt < 119000 ? 1000 : 60000; // Turn on second hand two minutes up. + } else if (e.precision < 43200000 && dt >= 0 ? e.future : e.past) { // Hours away + const p = Roman.pos(date.getHours() + date.getMinutes() / 60, 0.6); + const poly = [p[0] - 4, p[1], p[0], p[1] - 4, p[0] + 4, p[1], p[0], p[1] + 4]; + if (date >= now) g.fillPoly(poly); + else g.drawPoly(poly, true); + return 3600000; + } + return Infinity; + } + + render(d, rate) { + const g = this.g; + const state = this.state; + const options = this.options; + const events = this.events; + events.clean(d, -39600000); // 11h + + // Sidebar: icons and date + if (d.getDate() !== state.date || !state.iconsOk) { + const sidebar = this.sidebar; + state.date = d.getDate(); + state.iconsOk = true; + g.setColor(options.barBg).fillRect(barX, barY, barX + barW, barY + barH); + + sidebar.reset(rate).doLocked().doPower().doGPS().doHRM().doCompass(); + g.setFontCustom.apply(g, fontF); + let formatters = []; + let month, dom, dow; + if (options.calendric > 1) { + formatters.push(month = formatMonth.on(d.getMonth())); + } + if (options.calendric > 0) { + formatters.push(dom = formatDom.on(d.getDate())); + } + if (options.dow) { + formatters.push(dow = formatDow.on(d.getDay())); + } + // Obnoxiously inefficient iterative method :( + let ava = sidebar.free() - 3, use, i = 0, j = 0; + while ((use = formatters.reduce((l, f) => l + f.width(g) + 4, 0)) > ava && + j < formatters.length) + for (j = 0; + !formatters[i++ % formatters.length].squeeze() && + j < formatters.length; + j++) ; + if (dow) sidebar.print(dow); + sidebar.pad(ava - use); + if (month) sidebar.print(month); + if (dom) sidebar.print(dom); + } + + // Hour labels and (purely aesthetic) box; clear inner face. + let keyHour = d.getHours() < 12 ? 1 : 13; + let alertSpan = events.span(d, hceil(d) - 39600000, hfloor(d) + 39600000, 39600000); + let l = alertSpan[0].getHours(), h = alertSpan[1].getHours(); + if ((l - keyHour + 24) % 24 >= 12 || (h - keyHour + 24) % 24 >= 12) keyHour = l; + if (keyHour !== state.keyHour) { + state.keyHour = keyHour; + g.setColor(options.bg) + .fillRect(faceX, faceY, faceX + faceW, faceY + faceH) + .setFontCustom.apply(g, romanPartsF) + .setFontAlign(0, 1) + .setColor(options.fg); + // In order to deal with timezone changes more logic will be required, + // since the labels may be in unusual locations (even offset when + // a non-integral zone is involved). The value of keyHour can be + // anything in [hr-12, hr] mod 24. + for (let h = keyHour; h < keyHour + 12; h++) { + g.drawString( + numeral(h % 24, options), + faceX + layout[h % 12 * 2], + faceY + layout[h % 12 * 2 + 1] + ); + } + g.setColor(options.rectFg) + .drawRect(rectX, rectY, rectX + rectW - 1, rectY + rectH - 1); + } else { + g.setColor(options.bg) + .fillRect(rectX + 1, rectY + 1, rectX + rectW - 2, rectY + rectH - 2) + .setColor(options.fg); + } + + // Alerts + let requestedRate = events.scan( + d, hfloor(alertSpan[0] + 0), hceil(alertSpan[1] + 0) + 1, + (e, t, p) => this.alert(e, t, d, p) + ); + if (rate > requestedRate) rate = requestedRate; + + // Hands + // Here we are using incremental hands for hours and minutes. + // If we quantised, we could use hand-crafted bitmaps, though. + this.hours(d.getHours() + d.getMinutes() / 60); + if (rate < 3600000) { + this.minutes(d.getMinutes() + d.getSeconds() / 60); + } + if (rate < 60000) this.seconds(d.getSeconds()); + g.setColor(options.hubFg).fillCircle(faceCX, faceCY, 3); + return requestedRate; + } +} + +////////////////////////////////////////////////////////////////////////////// +/* Master clock */ + +class Clock { + constructor(face) { + this.face = face; + this.timescales = face.timescales; + this.options = face.options; + this.rates = {}; + + this.options.on('done', () => this.start()); + + this.listeners = { + lcdPower: on => on ? this.active() : this.inactive(), + charging: () => {face.doIcons('charging'); this.active();}, + lock: () => {face.doIcons('locked'); this.active();}, + faceUp: up => {this.conservative = !up; this.active();}, + drag: e => { + if (this.t0) { + if (e.b) { + e.x > this.xN && (this.xN = e.x) || e.x > this.xX && (this.xX = e.x); + e.y > this.yN && (this.yN = e.y) || e.y > this.yX && (this.xY = e.y); + } else if (this.xX - this.xN < 20) { + if (e.y - this.e0.y < -50) { + this.options.resolution > 0 && this.options.resolution--; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + } else if (e.y - this.e0.y > 50) { + this.options.resolution < this.timescales.length - 1 && + this.options.resolution++; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + } else if (this.yX - this.yN < 20 && Date.now() - this.t0 > 500) { + this.stop(); + this.options.interact(); + } + this.t0 = null; + } + } else if (e.b) { + this.t0 = Date.now(); this.e0 = e; + this.xN = this.xX = e.x; this.yN = this.yX = e.y; + } + } + }; + } + + redraw(rate) { + const now = this.updated = new Date(); + if (this.refresh) this.face.reset(); + this.refresh = false; + rate = this.face.render(now, rate); + if (rate !== this.rates.face) { + this.rates.face = rate; + this.active(); + } + return this; + } + + inactive() { + this.timeout && clearTimeout(this.timeout); + this.exception && clearTimeout(this.exception); + this.interval && clearInterval(this.interval); + this.timeout = this.exception = this.interval = this.rate = null; + return this; + } + + active() { + const prev = this.rate; + const now = Date.now(); + let rate = Infinity; + for (const k in this.rates) { + let r = this.rates[k]; + r === +r || (r = r[+this.conservative]) + r < rate && (rate = r); + } + const delay = rate - now % rate + 1; + this.refresh = true; + + if (rate !== prev) { + this.inactive(); + this.redraw(rate); + if (rate < 31622400000) { // A year! + this.timeout = setTimeout( + () => { + this.inactive(); + this.interval = setInterval(() => this.redraw(rate), rate); + if (delay > 1000) this.redraw(rate); + this.rate = rate; + }, delay + ); + } + } else if (rate > 1000) { + if (!this.exception) this.exception = setTimeout(() => { + this.redraw(rate); + this.exception = null; + }, this.updated + 1000 - Date.now()); + } + return this; + } + + stop() { + this.inactive(); + for (const l in this.listeners) { + Bangle.removeListener(l, this.listeners[l]); + } + return this; + } + + start() { + this.inactive(); // Reset to known state. + this.conservative = false; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + for (const l in this.listeners) { + Bangle.on(l, this.listeners[l]); + } + Bangle.setUI('clock'); + return this; + } +} + +////////////////////////////////////////////////////////////////////////////// +/* Main */ + +const clock = new Clock(new Roman(g, events)).start(); diff --git a/apps/pooqroman/app.png b/apps/pooqroman/app.png new file mode 100644 index 000000000..bd27186e0 Binary files /dev/null and b/apps/pooqroman/app.png differ diff --git a/apps/pooqroman/resourcer.js b/apps/pooqroman/resourcer.js new file mode 100644 index 000000000..69365018e --- /dev/null +++ b/apps/pooqroman/resourcer.js @@ -0,0 +1,721 @@ +// pooqRoman resource maker +// +// Copyright (c) 2021 Stephen P Spackman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Notes: +// +////////////////////////////////////////////////////////////////////////////// +/* ==ASSETS== */ + +const heatshrink = require('heatshrink'); + +const enc = x => { + const d = btoa(require("heatshrink").compress(x)); + var r = "'" + d.substr(0, 64); + for (let i = 64; i < d.length; i += 64) r += "' +\n '" + d.substr(i, 64); + return r + "'"; +}; + +const prepBitmap = (name, data) => { + const image = Graphics.createImage(data); + const raw = String.fromCharCode(image.width, image.height, 0x81, 0) + image.buffer; + const x = ` +const ${name}I = dec(${enc(raw)}); +`; + return x; +}; + +const prepFont = (name, data) => { + const image = Graphics.createImage(data); + const lengths = Uint8Array(256); + const offsets = Uint16Array(256); + const adjustments = Uint16Array(256); + let min = Infinity, max = -Infinity; + const lines = data.split('\n'); + let m; + // This regexp is clearly suboptimal, but Espruino's regexp engine is really wonky + // and doesn't process nested parentheses or alternation correctly. + for (let i = 0; i < 5 && !(m = /^(<*)=([*\d]+)(=*)(>*)$/.exec(lines[i])); i++); + if (!m) throw new Error('Missing or incorrect header'); + const desc = m[1].length, body = 1 + m[2].length + m[3].length, asc = m[4].length; + const h = desc + body + asc; + let width = m[2] == '*' ? null : +m[2]; + let c = null, o = 0; + lines.forEach((line, l) => { + if (m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line)) { + const h = m[2] == '='; + if (m[1].length > desc || h && m[1].length != desc) + throw new Error('Invalid descender height at ' + l); + if (m[2].length + m[3].length + m[4].length != body) + throw new Error('Invalid body height at ' + l); + if (m[5].length > asc || h && m[5].length != asc) + throw new Error('Invalid ascender height at ' + l); + if (c != null) { + lengths[c] = l - o; + if (width !== null && width !== lengths[c]) + throw new Error( + `Character has width ${lengths[c]} != ${width} at ${offsets[c]}` + ); + c = null + } + if (!h) { + c = m[3].charCodeAt(0); + if (c < min) min = c; + if (c > max) max = c; + o = l + 1; + offsets[c] = l; + adjustments[c] = m[1].length + } + } + }); + const xoffs = Uint8Array(lines.length); + const ypos = Uint16Array(lines.length); + ypos.fill(0xffff); + const w0 = lengths[min]; + let widths = ''; + for (c = min, o = 0; c <= max; c++) { + for (i = 0, j = offsets[c]; i < lengths[c]; i++) { + xoffs[j] = asc + body + adjustments[c] - 1; + ypos[j++] = o++; + } + widths += String.fromCharCode(lengths[c]); + } + const raster = Graphics.createArrayBuffer(h, o, 1, {msb: true}); + const writer = Graphics.createCallback( + image.width, image.height, 1, + (x, y, col) => raster.setPixel(xoffs[y] - x, ypos[y], col) + ); + writer.drawImage(image); + if (width === null) width = `dec(${enc(widths)})`; + const x = `const ${name}F = [ + dec( + ${enc(raster.buffer)} + ), ${min}, ${width}, ${h} +];`; + return x; +}; + +res = ` +const heatshrink = require('heatshrink'); +const dec = x => E.toString(heatshrink.decompress(atob(x))); +`; + +res += prepFont('romanParts', ` +<=*============== +-a-------------- +x x +xx xx +-b-------------- +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +-c-------------- +xx xx +x x +-d-------------- +xx xx +xx xx +xx xx +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +-e-------------- +xx xx +x xxxx +<-f-------------- + xxxxxxxx + xxxxxxxxxxx + xxxxxxx xx + xxxxxx x +xxxxx + xxxxxx x + xxxxxxx xx + xxxxxxxxxxx + xxxxxxxx +-g-------------- + xxxx + xx + x +-h-------------- + x + xx + xxxx +-i-------------- +x xxxx +xx xx +-j-------------- +xx xx +xxx xxx +xxxx xxxx +xxxxxx xxxxxx +xx xxxx xxxx xx +x xxxxxx x + xxxx +x xxxxxx x +xx xxxx xxxx xx +xxxxxx xxxxxx +xxxx xxxx +xxx xxx +xx xx +-k-------------- +x x +<-l-------------- + xx x + xxxxxx xx + xxxx xxxx xxx + xxxx xx xxxx x +xxx xx + xxxx xx xxxx x + xxxx xxxx xxx + xxxxxx xx + xx x +-m-------------- +x xx x +xx xxxx xx +xxx xxxxxx xxx +x xxxx xx xxxx x + xx xx +x xxxx xx xxxx x +xxx xxxxxx xxx +xx xxxx xx +x xx x +-n-------------- + xxxxxxxx + xxxxxxxxxxxx + xxxx xxxx +xxxx xxxx +xxx xxx +xx xxxx xx +xx xxxx xx +xxx xxx +xxxx xxxx + xxxx xxxx + xxxxxxxxxxxx + xxxxxxxx +<=*============== +`); + +res += prepFont('font', ` +<<<<=*======>>>> +- ------ + +-.------ +xx +xx +-0------>>>> + xxxxxxxx + xxxxxxxxxx +xxx xxx +xx xx +xx xx +xxx xxx + xxxxxxxxxx + xxxxxxxx + +-1------>>>> +xx x +xx xx +xxxxxxxxxxxx +xxxxxxxxxxxx +xx +xx + +-2------>>>> +x x +xx xx +xxx xxx +xxxx xx +xxxxx xx +xx xxx xxx +xx xxxxxxx +xx xxxxx + +-3------>>>> + x xx + xx x xx +xxx xx xx +xx xxx xx +xx xxxxxx +xxx xxx xxx + xxxxxx xx + xxx x + +-4------>>>> + x + xx + xxxx + xxxxxxxxx +xxxxx xxxxx +xxxxx + xx + xx + +-5------>>>> + x xxxxxx + xx xxxxxx +xxx xx xx +xx xx xx +xx xx xx +xxx xxx xx + xxxxxx xx + xxxx + +-6------>>>> + xxxx + xxxxxxx +xxx xxxxx +xx xxxxx +xx xx xxx +xxx xxx xx + xxxxxx x + xxxx + +-7------>>>> + xx + xx +xxxx xx +xxxxxx xx + xxxx xx + xxxxxx + xxxx + x + +-8------>>>> + xxx xxx + xxxxxxxxxx +xxx xxxx xxx +xx xx xx +xx xx xx +xxx xxxx xxx + xxxxxxxxxx + xxx xxx + +-9------>>>> + xxxx +x xxxxxx +xx xxx xxx +xxx xx xx + xxxxx xx + xxxxx xxx + xxxxxxx + xxx + +-A------>>>> +xx +xxxxx + xxxxxxx + xxxxxxx + xx xxxx + xxxxxxx + xxxxxxx +xxxxx +xx + +-D------>>>> +xx xx +xxxxxxxxxxxx +xxxxxxxxxxxx +xx xx +xx xx +xxx xxx + xxxxxxxxxx + xxxxxxxx + +-F------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + xx xx + xx xx + xx xx + xx +-I------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + +-J------>>>> + xx + xxx xx +xxx xx +xx xx +xxx xx + xxxxxxxxxxx + xxxxxxxxxx + xx +-M------>>>> +xxxxxxxxxxxx +xxxxxxxxxxx + xxx + xxxx + xxxx + xxx +xxxxxxxxxxx +xxxxxxxxxxxx + +-N------>>>> +xxxxxxxxxxxx +xxxxxxxxxxx + xxx + xxx + xxx + xxx + xxxxxxxxxxx +xxxxxxxxxxxx + +-O------>>>> + xxxxxxxx + xxxxxxxxxx +xxx xxx +xx xx +xx xx +xxx xxx + xxxxxxxxxx + xxxxxxxx + +-S------>>>> + x xxx + xx xxxxx +xxx xx xxx +xx xx xx +xx xx xx +xxx xx xxx + xxxxx xx + xxx x + +-T------>>>> + xx + xx + xx +xxxxxxxxxxxx +xxxxxxxxxxxx + xx + xx + xx +-V------>>>> + xxx + xxxxxx + xxxxx +xxxxx + xxxxx + xxxxxx + xxx + +-W------>>>> + xxxx + xxxxxxxx +xxxxxxxx + xxxx + xxxx + xxxx +xxxxxxxx + xxxxxxxx + xxxx +-X------>>>> +xx xx +xxx xxx + xxx xxx + xxxx + xxxx + xxx xxx +xxx xxx +xx xx + +-a------ + xxx +xxxxx x +xx xx xx +xx xx xx +xx xx xx + xxxxxx +xxxxxx + +-b------>>>> +xxxxxxxxxxxx + xxxxxxxxxxx +xx xx +xx xx +xxx xxx + xxxxxx + xxxx + +-c------ + xxxx + xxxxxx +xxx xxx +xx xx +xx xx + xx xx + x x + +-d------>>>> + xxxx + xxxxxx +xxx xxx +xx xx +xx xx + xxxxxxxxxxx +xxxxxxxxxxxx + +-e------ + xxxx + xxxxxx +xx xx xx +xx xx xx +xx xx xx + x xxxx + xxx + +<<<<-g------ + x xxxx + xx xxxxxx +xx xxx xxx +xx xx xx +xxx xx xxx + xxxxxxxxxx + xxxxxxxxx + +-h------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + xx + xx + xxx +xxxxxxx +xxxxxx + +-i------>>>> +xxxxxxxx xx +xxxxxxxx xx + +-l------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + +-m------ +xxxxxxxx +xxxxxxx + xxx + xxx +xxxxxxx +xxxxxxx + xxx + xxx +xxxxxxx +xxxxxx + +-n------ +xxxxxxxx +xxxxxxx + xxx + xx + xxx +xxxxxxx +xxxxxx + +-o------ + xxxx + xxxxxx +xxx xxx +xx xx +xxx xxx + xxxxxx + xxxx + +<<<<-p------ +xxxxxxxxxxxx +xxxxxxxxxxx + xx xx + xx xx + xxx xxx + xxxxxx + xxxx + +-r------ +xxxxxxxx +xxxxxxx + xxx + xx + xx + xx + +-s------ + x xxx +xx xxxxx +xx xx xx +xx xx xx +xxxxx xx + xxx x + +-t------>>>> + xx + xxxxxxxxx + xxxxxxxxxx +xxx xx +xx xx +xx xx + xx + +-u------ + xxxxxx + xxxxxxx +xxx +xx +xxx + xxxxxxx +xxxxxxxx + +-v------ + xx + xxxx + xxxx +xxxx + xxxx + xxxx + xx + +<<<<-y------ + x xxxxxx + xx xxxxxxx +xx xxx +xx xx +xxx xxx + xxxxxxxxxxx + xxxxxxxxx + +<<<<=*======>>>> +`); + +res += prepBitmap('lock', ` + xxxxxx + xxxxxxxx + xxx xxx + xxx xxx + xxx xxx + xxx xxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx +`); + +res += prepBitmap('battery', ` + xxxx + xxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx +`); + +res += prepBitmap('charge', ` + x + xx + xx + xxx + xxx + xxxxxxxxx + xxxxxxxxx + xxx + xxx + xx + xx + x +`); + +res += prepBitmap('GPS', ` + x + x x + x x + x x + x x xxxx + x xxxxx + xxxxxx + xxxxx + x xxx x + x x x x x + x x x x x + x x xx x x + x x x x + x xxx x + x + xxx +`); + +res += prepBitmap('HRM', ` + xxxx xxxx + xxxxxx xxxxxx + xx xxxx xxx xxx +xxx xxxxxxxx xxxx +xxx xxxxxxxx xxxx +xxx xxxxxxxx xxxx +xx xxxxxxx xxxx +xx xx xxxx xx x + xx x x x + xx xxxxxxxx xxx + xxxxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxx + xxxxxxx + xxxxx + xxx + x +`); + +res += prepBitmap('compass', ` + xxxxx + xxxxxxxxx + xxx x xxx + xx x xx + xx x xx + xx xxx xx +xx xxx xx +xx xxx xx +xx xxx xx +xx xx xx xx +xx xx xx xx + xx x x xx + xx x x xx + xx xx + xxx xxx + xxxxxxxxx + xxxxx +`); + +print(res); diff --git a/apps/poweroff/ChangeLog b/apps/poweroff/ChangeLog new file mode 100644 index 000000000..1a3bc1757 --- /dev/null +++ b/apps/poweroff/ChangeLog @@ -0,0 +1 @@ +0.01: New app! diff --git a/apps/poweroff/README.md b/apps/poweroff/README.md new file mode 100644 index 000000000..d9d7a8dbc --- /dev/null +++ b/apps/poweroff/README.md @@ -0,0 +1,13 @@ +# Poweroff + +Simple app to power off your Bangle.js + +## Usage + +Start the app to shutdown your Bangle.js watch after a short delay. + +## Creator +Marco ([myxor](https://github.com/myxor)) + +## Icon +Icon taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 diff --git a/apps/poweroff/app-icon.js b/apps/poweroff/app-icon.js new file mode 100644 index 000000000..81a2527b5 --- /dev/null +++ b/apps/poweroff/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIolgfAAqkCAoNAAoMHAoPgAoMPwfB+AFBj/D4f4AoM/AoP8AoQRBAoV/DoP+AoN+AoN+AoP8AoM/Ao/4AoMfAsBQCAo5QDAo5KCAQV/AQJZCn+AgIUD4EDAoUf+EPFgUP///RIUHAoKVCgYFBVAYFBWYc/EQQAvA")) diff --git a/apps/poweroff/app.js b/apps/poweroff/app.js new file mode 100644 index 000000000..303e78d03 --- /dev/null +++ b/apps/poweroff/app.js @@ -0,0 +1,13 @@ +g.clear(); + +g.setFont("6x8",2).setFontAlign(0,0); + var x = g.getWidth()/2; + var y = g.getHeight()/2 + 10; + g.drawString("Powering off...", x, y); + +setTimeout(function() { + if (Bangle.softOff) Bangle.softOff(); else Bangle.off(); +}, 1000); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/poweroff/app.png b/apps/poweroff/app.png new file mode 100644 index 000000000..aa186ab20 Binary files /dev/null and b/apps/poweroff/app.png differ diff --git a/apps/pparrot/bangle1-party-parrot-screenshot.png b/apps/pparrot/bangle1-party-parrot-screenshot.png new file mode 100644 index 000000000..2965f42cf Binary files /dev/null and b/apps/pparrot/bangle1-party-parrot-screenshot.png differ diff --git a/apps/qalarm/ChangeLog b/apps/qalarm/ChangeLog index 135e69d23..fb6c751bb 100644 --- a/apps/qalarm/ChangeLog +++ b/apps/qalarm/ChangeLog @@ -1,2 +1,5 @@ 0.01: First version! -0.02: Fixed alarms not working and localised days of week. \ No newline at end of file +0.02: Fixed alarms not working and localised days of week. +0.03: Fix unfreed memory, and clearInterval that disabled all clocks at midnight + Fix app icon + Change menu order so 'back' is at the top diff --git a/apps/qalarm/app-icon.js b/apps/qalarm/app-icon.js index 1a014b796..12d2c103f 100644 --- a/apps/qalarm/app-icon.js +++ b/apps/qalarm/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("/wA/AH4A/AH4AF0WiF1wwtF73GB53MAAgkY4wABFqIxPEhQuXGB4vUFxYwMEpBpGBwouNGAwfFF5I1KF6ZQHGAwNLFx4wHF/4v/F/4v/AoYGDF6gaFF5AwHL7QuMBJQvWEpwvxBQ4uRGBAkJT4wuWGBIuIRjKRNF8wwXFy4wWFzIwU53NFzPN5wuR5/PGK4tBDYSNQ5wVCCwIzBAAQoIAAQWGSJ5HFDYYAQIYTCRKRIeBAAYmDAAZsJMCQAbeCAybFiQ0XFTQAIzgAGFcYvz0QAGF84wGF1AwFF1QA/AH4A/ADQ=")) +require("heatshrink").decompress(atob("mEw4UA///gH+93+oH9Jf8AgfABZMP+ALRmADCitUAgUMAQP8AQMBqtVoAFBn4CBDwUFBYNQFAQLEioLBEgQLBgfwE4IKBAAI3BBYXAE4ILE/gJBAIM8HQQ8CngL/n4LFKYR3BhgLFNYSDCBYqPFBZKzBUwSoDWYTLBUwSoDZYQABBQa0DBZCoBAAY6EcojhEHgoACkoLFrALD1WVBQdW1QLDtQMDBQOpHQmqAAg8DIwQKEJAg6FMApfLDIoJFAAX//4KIBbE/aAIAIh7oBAH4A==")) diff --git a/apps/qalarm/app.js b/apps/qalarm/app.js index 64f601bf6..ad071adf0 100644 --- a/apps/qalarm/app.js +++ b/apps/qalarm/app.js @@ -41,6 +41,7 @@ function getCurrentTime() { function showMainMenu() { const menu = { "": { title: "Alarms" }, + "< Back" : () => load(), "New Alarm": () => showEditAlarmMenu(-1), "New Timer": () => showEditTimerMenu(-1), }; @@ -54,9 +55,7 @@ function showMainMenu() { else showEditAlarmMenu(idx); }; }); - menu["< Back"] = () => { - load(); - }; + menu if (WIDGETS["qalarm"]) WIDGETS["qalarm"].reload(); return E.showMenu(menu); @@ -86,6 +85,7 @@ function showEditAlarmMenu(alarmIndex, alarm) { const menu = { "": { title: alarm.msg ? alarm.msg : "Alarms" }, + "< Back" : showMainMenu, Hours: { value: hrs, onchange: function (v) { @@ -162,7 +162,6 @@ function showEditAlarmMenu(alarmIndex, alarm) { showMainMenu(); }; } - menu["< Back"] = showMainMenu; return E.showMenu(menu); } @@ -206,6 +205,7 @@ function showEditTimerMenu(timerIndex) { const menu = { "": { title: "Timer" }, + "< Back" : showMainMenu, Hours: { value: hrs, onchange: function (v) { @@ -264,7 +264,7 @@ function showEditTimerMenu(timerIndex) { showMainMenu(); }; } - menu["< Back"] = showMainMenu; + return E.showMenu(menu); } diff --git a/apps/qalarm/boot.js b/apps/qalarm/boot.js index 6713ad9e1..5e9560ee2 100644 --- a/apps/qalarm/boot.js +++ b/apps/qalarm/boot.js @@ -1 +1 @@ -eval(require("Storage").read("qalarmcheck.js")); +(function() { eval(require("Storage").read("qalarmcheck.js")); })() diff --git a/apps/qalarm/qalarmcheck.js b/apps/qalarm/qalarmcheck.js index 9a3f10d5e..8dac43800 100644 --- a/apps/qalarm/qalarmcheck.js +++ b/apps/qalarm/qalarmcheck.js @@ -4,7 +4,10 @@ print("Checking for alarms..."); -clearInterval(); +if (Bangle.QALARM) { + clearInterval(Bangle.QALARM); + Bangle.QALARM = undefined; +} function getCurrentTime() { let time = new Date(); @@ -29,13 +32,13 @@ let nextAlarms = (require("Storage").readJSON("qalarm.json", 1) || []) .sort((a, b) => a.t - b.t); if (nextAlarms[0]) { - setTimeout(() => { + Bangle.QALARM = setTimeout(() => { eval(require("Storage").read("qalarmcheck.js")); load("qalarm.js"); }, nextAlarms[0].t - t); } else { // No alarms found: will re-check at midnight - setTimeout(() => { + Bangle.QALARM = setTimeout(() => { eval(require("Storage").read("qalarmcheck.js")); }, 86400000 - t); } diff --git a/apps/qmsched/ChangeLog b/apps/qmsched/ChangeLog index 8bae1dba0..0b8d67e76 100644 --- a/apps/qmsched/ChangeLog +++ b/apps/qmsched/ChangeLog @@ -1,3 +1,4 @@ 0.01: First version 0.02: Add widget - +0.03: Bangle.js 2 support +0.04: Move Quiet Mode LCD options from global settings to this app diff --git a/apps/qmsched/README.md b/apps/qmsched/README.md index 033014789..535ae56e4 100644 --- a/apps/qmsched/README.md +++ b/apps/qmsched/README.md @@ -1,9 +1,14 @@ # Quiet Mode Schedule and Widget -Automatically turn Quiet Mode on or off at set times, and display a widget when enabled. +Automatically turn Quiet Mode on or off at set times, and display a widget when Quiet Mode is active. -### Edit Schedule: -![Main menu](screenshot_main.png) ![Edit Schedule menu](screenshot_edit.png) +| Bangle.js 1 | Bangle.js 2 | +|:---------------------------------------------:|:---------------------------------------------:| +| (widget: Silent mode) | (widget: Alarms mode) | +| ![Main menu](screenshot_b1_main.png) | ![Main menu](screenshot_b2_main.png) | +| ![Edit Schedule menu](screenshot_b1_edit.png) | ![Edit Schedule menu](screenshot_b2_edit.png) | +| ![LCD Options menu](screenshot_b1_lcd.png) | ![LCD Options menu](screenshot_b2_lcd.png) | -### Widget: -![Widget, quiet mode: silent](screenshot_widget_silent.png) ![Widget, quiet mode: alarms](screenshot_widget_alarms.png) +### LCD Settings: + +If set, these override the default LCD settings while Quiet Mode is active. \ No newline at end of file diff --git a/apps/qmsched/app.js b/apps/qmsched/app.js index 105e09ea6..7be3339fb 100644 --- a/apps/qmsched/app.js +++ b/apps/qmsched/app.js @@ -2,27 +2,74 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); const modeNames = ["Off", "Alarms", "Silent"]; -let scheds = require("Storage").readJSON("qmsched.json", 1); -/*scheds = [ - { hr : 6.5, // hours + minutes/60 - mode : 1, // quiet mode (0/1/2) - } -];*/ -if (!scheds) { - // set default schedule on first load of app - scheds = [ - {"hr": 8, "mode": 0}, - {"hr": 22, "mode": 1}, - ]; - require("Storage").writeJSON("qmsched.json", scheds); + +// load global brightness setting +let bSettings = require('Storage').readJSON('setting.json',true)||{}; +let current = 0|bSettings.quiet; +delete bSettings; // we don't need any other global settings + + + + + + +/** + * Save settings to qmsched.json + */ +function save() { + require('Storage').writeJSON('qmsched.json', settings); } -if (scheds.length && scheds.some(s => "last" in s)) { - // cleanup: remove "last" values (used by old versions) - scheds = scheds.map(s => { - delete s.last; - return s; - }); - require("Storage").writeJSON("qmsched.json", scheds); +function get(key, def) { + return (key in settings) ? settings[key] : def; +} +function set(key, val) { + settings[key] = val; save(); + scheds = settings.scheds; options = settings.options; // update references +} +function unset(key) { + delete settings[key]; save(); +} + +let settings, + scheds, options; // references for convenience +/** + * Load settings file, check if we need to migrate old setting formats to new + */ +function loadSettings() { + settings = require('Storage').readJSON("qmsched.json", true) || {}; + + if (Array.isArray(settings)) { + // migrate old file (plain array of schedules, qmOptions stored in global settings file) + require("Storage").erase("qmsched.json"); // need to erase old file, or Things Break, somehow... + let bOptions = require('Storage').readJSON('setting.json',true)||{}; + settings = { + options: bOptions.qmOptions || {}, + scheds: settings, + }; + // store new format + save(); + // and clean up qmOptions from global settings file + delete bOptions.qmOptions; + require('Storage').writeJSON('setting.json',bOptions); + } + // apply defaults + settings = Object.assign({ + options: {}, // Bangle options to override during quiet mode, default = none + scheds: [ + // default schedule: + {"hr": 8, "mode": 0}, + {"hr": 22, "mode": 1}, + ], + }, settings); + scheds = settings.scheds; options = settings.options; + + if (scheds.length && scheds.some(s => "last" in s)) { + // cleanup: remove "last" values (used by older versions) + set('scheds', scheds.map(s => { + delete s.last; + return s; + })); + } } function formatTime(t) { @@ -32,30 +79,35 @@ function formatTime(t) { } function showMainMenu() { - const menu = { + let _m, menu = { "": {"title": "Quiet Mode"}, - "Current Mode": { - value: (require("Storage").readJSON("setting.json", 1) || {}).quiet|0, - format: v => modeNames[v], - onchange: function(v) { - if (v<0) {v = 2;} - if (v>2) {v = 0;} - require("qmsched").setMode(v); - this.value = v; - }, + "< Exit": () => load() + }; + // "Current Mode""Silent" won't fit on Bangle.js 2 + menu["Current"+((process.env.HWVERSION===2) ? "" : " Mode")] = { + value: current, + format: v => modeNames[v], + onchange: function(v) { + if (v<0) {v = 2;} + if (v>2) {v = 0;} + require("qmsched").setMode(v); + current = v; + this.value = v; }, }; scheds.sort((a, b) => (a.hr-b.hr)); scheds.forEach((sched, idx) => { - const name = modeNames[sched.mode]; - const txt = formatTime(sched.hr)+" ".repeat(14-name.length)+name; - menu[txt] = function() { - showEditMenu(idx); + menu[formatTime(sched.hr)] = { + format: () => modeNames[sched.mode], // abuse format to right-align text + onchange: function() { + _m.draw = ()=> {}; // prevent redraw of main menu over edit menu + showEditMenu(idx); + } }; }); menu["Add Schedule"] = () => showEditMenu(-1); - menu["< Back"] = () => {load();}; - return E.showMenu(menu); + menu["LCD Settings"] = () => showOptionsMenu(); + _m = E.showMenu(menu); } function showEditMenu(index) { @@ -70,6 +122,7 @@ function showEditMenu(index) { } const menu = { "": {"title": (isNew ? "Add" : "Edit")+" Schedule"}, + "< Cancel": () => showMainMenu(), "Hours": { value: hrs, onchange: function(v) { @@ -111,18 +164,88 @@ function showEditMenu(index) { } else { scheds[index] = getSched(); } - require("Storage").writeJSON("qmsched.json", scheds); + save(); showMainMenu(); }; if (!isNew) { menu["> Delete"] = function() { scheds.splice(index, 1); - require("Storage").writeJSON("qmsched.json", scheds); + save(); showMainMenu(); }; } - menu["< Cancel"] = showMainMenu; return E.showMenu(menu); } +function showOptionsMenu() { + const disabledFormat = v => v ? "Off" : "-"; + function toggle(option) { + // we disable wakeOn* events by setting them to `false` in options + // not disabled = not present in options at all + if (option in options) { + delete options[option]; + } else { + options[option] = false; + } + save(); + } + let resetTimeout; + const oMenu = { + "": {"title": "LCD Settings"}, + "< Back": () => showMainMenu(), + "LCD Brightness": { + value: get("brightness", 0), + min: 0, // 0 = use default + max: 1, + step: 0.1, + format: v => (v>0.05) ? v : "-", + onchange: v => { + if (v>0.05) { // prevent v=0.000000000000001 bugs + set("brightness", v); + Bangle.setLCDBrightness(v); // show result, even if not quiet right now + // restore brightness after half a second + if (resetTimeout) clearTimeout(resetTimeout); + resetTimeout = setTimeout(() => { + resetTimeout = undefined; + require("qmsched").setMode(current); + }, 500); + } else { + unset("brightness"); + require("qmsched").setMode(current); + } + }, + }, + "LCD Timeout": { + value: get("timeout", 0), + min: 0, // 0 = use default (no constant on for quiet mode) + max: 60, + step: 5, + format: v => v>1 ? v : "-", + onchange: v => { + if (v>1) set("timeout", v); + else unset("timeout"); + }, + }, + // we disable wakeOn* events by overwriting them as false in options + // not disabled = not present in options at all + "Wake on FaceUp": { + value: "wakeOnFaceUp" in options, + format: disabledFormat, + onchange: () => {toggle("wakeOnFaceUp");}, + }, + "Wake on Touch": { + value: "wakeOnTouch" in options, + format: disabledFormat, + onchange: () => {toggle("wakeOnTouch");}, + }, + "Wake on Twist": { + value: "wakeOnTwist" in options, + format: disabledFormat, + onchange: () => {toggle("wakeOnTwist");}, + }, + }; + return E.showMenu(oMenu); +} + +loadSettings(); showMainMenu(); diff --git a/apps/qmsched/boot.js b/apps/qmsched/boot.js index 2712cab30..c3bc49b58 100644 --- a/apps/qmsched/boot.js +++ b/apps/qmsched/boot.js @@ -1,7 +1,13 @@ // apply Quiet Mode schedules (function qm() { - let scheds = require("Storage").readJSON("qmsched.json", 1) || []; - if (!scheds.length) { return;} + let bSettings = require('Storage').readJSON('setting.json',true)||{}; + const curr = 0|bSettings.quiet; + delete bSettings; + if (curr) require("qmsched").applyOptions(curr); // no need to re-apply default options + + let settings = require('Storage').readJSON('qmsched.json',true)||{}; + let scheds = settings.scheds||[]; + if (!scheds.length) {return;} const now = new Date(), hr = now.getHours()+(now.getMinutes()/60)+(now.getSeconds()/3600); // current (decimal) hour scheds.sort((a, b) => a.hr-b.hr); diff --git a/apps/qmsched/lib.js b/apps/qmsched/lib.js index a3d36ed34..9b307769a 100644 --- a/apps/qmsched/lib.js +++ b/apps/qmsched/lib.js @@ -1,18 +1,23 @@ +/** + * Apply LCD options for given mode + * @param {int} mode Quiet Mode + */ +exports.applyOptions = function(mode) { + const s = require("Storage").readJSON(mode ? "qmsched.json" : "setting.json", 1) || {}; + const get = (k, d) => k in s ? s[k] : d; + Bangle.setOptions(get("options", {})); + Bangle.setLCDBrightness(get("brightness", 1)); + Bangle.setLCDTimeout(get("timeout", 10)); +}; /** * Set new Quiet Mode and apply Bangle options * @param {int} mode Quiet Mode */ exports.setMode = function(mode) { - let s = require("Storage").readJSON("setting.json", 1) || {}; - s.quiet = mode; - require("Storage").writeJSON("setting.json", s); - if (s.options) Bangle.setOptions(s.options); - if (mode && s.qmOptions) Bangle.setOptions(s.qmOptions); - if (mode && s.qmBrightness) { - if (s.qmBrightness!=1) Bangle.setLCDBrightness(s.qmBrightness); - } else { - if (s.brightness && s.brightness!=1) Bangle.setLCDBrightness(s.brightness); - } - if (mode && s.qmTimeout) Bangle.setLCDTimeout(s.qmTimeout); - if (typeof (WIDGETS)!=="undefined" && "qmsched" in WIDGETS) {WIDGETS["qmsched"].draw();} -}; \ No newline at end of file + require("Storage").writeJSON("setting.json", Object.assign( + require("Storage").readJSON("setting.json", 1) || {}, + {quiet:mode} + )); + exports.applyOptions(mode); + if (WIDGETS && "qmsched" in WIDGETS) WIDGETS["qmsched"].draw(); +}; diff --git a/apps/qmsched/screenshot_b1_edit.png b/apps/qmsched/screenshot_b1_edit.png new file mode 100644 index 000000000..ec82e92e6 Binary files /dev/null and b/apps/qmsched/screenshot_b1_edit.png differ diff --git a/apps/qmsched/screenshot_b1_lcd.png b/apps/qmsched/screenshot_b1_lcd.png new file mode 100644 index 000000000..16f9356b8 Binary files /dev/null and b/apps/qmsched/screenshot_b1_lcd.png differ diff --git a/apps/qmsched/screenshot_b1_main.png b/apps/qmsched/screenshot_b1_main.png new file mode 100644 index 000000000..803ca69d5 Binary files /dev/null and b/apps/qmsched/screenshot_b1_main.png differ diff --git a/apps/qmsched/screenshot_b2_edit.png b/apps/qmsched/screenshot_b2_edit.png new file mode 100644 index 000000000..d26ff02cb Binary files /dev/null and b/apps/qmsched/screenshot_b2_edit.png differ diff --git a/apps/qmsched/screenshot_b2_lcd.png b/apps/qmsched/screenshot_b2_lcd.png new file mode 100644 index 000000000..3f06488c3 Binary files /dev/null and b/apps/qmsched/screenshot_b2_lcd.png differ diff --git a/apps/qmsched/screenshot_b2_main.png b/apps/qmsched/screenshot_b2_main.png new file mode 100644 index 000000000..f6d22a8b8 Binary files /dev/null and b/apps/qmsched/screenshot_b2_main.png differ diff --git a/apps/qmsched/screenshot_edit.png b/apps/qmsched/screenshot_edit.png deleted file mode 100644 index 88b7fcad4..000000000 Binary files a/apps/qmsched/screenshot_edit.png and /dev/null differ diff --git a/apps/qmsched/screenshot_main.png b/apps/qmsched/screenshot_main.png deleted file mode 100644 index 634abd633..000000000 Binary files a/apps/qmsched/screenshot_main.png and /dev/null differ diff --git a/apps/qmsched/screenshot_widget_alarms.png b/apps/qmsched/screenshot_widget_alarms.png deleted file mode 100644 index 52dbe2464..000000000 Binary files a/apps/qmsched/screenshot_widget_alarms.png and /dev/null differ diff --git a/apps/qmsched/screenshot_widget_silent.png b/apps/qmsched/screenshot_widget_silent.png deleted file mode 100644 index 38b133650..000000000 Binary files a/apps/qmsched/screenshot_widget_silent.png and /dev/null differ diff --git a/apps/qmsched/widget.js b/apps/qmsched/widget.js index c602288ad..8a8333ba5 100644 --- a/apps/qmsched/widget.js +++ b/apps/qmsched/widget.js @@ -16,9 +16,9 @@ WIDGETS["qmsched"] = { } let x = this.x, y = this.y; g.clearRect(x, y, x+23, y+23); - // quiet mode: draw dim red one-way-street sign + // quiet mode: draw red one-way-street sign (dim red on Bangle.js 1) x = this.x+11;y = this.y+11; // center of widget - g.setColor(0.8, 0, 0).fillCircle(x, y, 8); + g.setColor(process.env.HWVERSION===2 ? 1 : 0.8, 0, 0).fillCircle(x, y, 8); g.setColor(g.theme.bg).fillRect(x-6, y-2, x+6, y+2); if (mode>1) {return;} // no alarms // alarms still on: draw alarm icon in bottom-right corner diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index 2ea6e9fa8..40240de64 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -2,3 +2,4 @@ 0.02: Use 'recorder.log..' rather than 'record.log..' Fix interface.html 0.03: Fix theme and maps/graphing if no GPS +0.04: Multiple bugfixes diff --git a/apps/recorder/app.js b/apps/recorder/app.js index d29959e25..fcd8d6031 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -304,10 +304,10 @@ function plotTrack(info) { g.fillCircle(ox,oy,5); if (info.qOSTM) g.setColor("#000"); else g.setColor(g.theme.fg); - 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); @@ -360,6 +360,10 @@ function plotGraph(info, style) { var t,dx,dy,d,lt = c[timeIdx]; while(l!==undefined) { ++nl;c=l.split(","); + l = f.readLine(f); + if (c[latIdx] == "") { + continue; + } t = c[timeIdx]; i = Math.round(80*(t - strt)/dur); p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]}); @@ -372,7 +376,6 @@ function plotGraph(info, style) { } lp = p; lt = t; - l = f.readLine(f); } } else throw new Error("Unknown type "+style); var min=100000,max=-100000; @@ -396,13 +399,15 @@ function plotGraph(info, style) { height: g.getHeight()-(24+8), axes : true, gridy : grid, - gridx : 50, + gridx : infn.length / 3, title: title, + miny: min, + maxy: max, 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.filename, info); }, global.BTN3||BTN1); diff --git a/apps/rpgdice/bangle1-rpg-dice-screenshot.png b/apps/rpgdice/bangle1-rpg-dice-screenshot.png new file mode 100644 index 000000000..bca526b33 Binary files /dev/null and b/apps/rpgdice/bangle1-rpg-dice-screenshot.png differ diff --git a/apps/schoolCalendar/CalenderLogo.png b/apps/schoolCalendar/CalenderLogo.png new file mode 100644 index 000000000..49d198a15 Binary files /dev/null and b/apps/schoolCalendar/CalenderLogo.png differ diff --git a/apps/schoolCalendar/ChangeLog b/apps/schoolCalendar/ChangeLog new file mode 100644 index 000000000..85a7cc698 --- /dev/null +++ b/apps/schoolCalendar/ChangeLog @@ -0,0 +1 @@ +0.01: App is created with gradient background. diff --git a/apps/schoolCalendar/README.md b/apps/schoolCalendar/README.md new file mode 100644 index 000000000..fd95ddf59 --- /dev/null +++ b/apps/schoolCalendar/README.md @@ -0,0 +1,27 @@ +# School Calendar + +School Calendar is a calendar that you can see your upcoming classes or schedule. + +## Usage: +Enter your calendar events on the customizer then upload. (all day events are not supported yet) + +Once uploaded on the watch when in the table mode you can use BTN1 and BTN3 to scroll up and down on the list. (The red rectangle indicates your current position on the table and your yellow rectangle indicates your current schedule item or your next schedule item.) + +If you press BTN2 it will go into detail mode, and you can see additional information about your schedule item. Also, in this mode you can scroll up and down with BTN1 and BTN3 to move around in the table. To exit detail mode press BTN2 again. + +## Screenshots: +![screenshot (5)](https://user-images.githubusercontent.com/89286474/142801592-485aa0b0-c417-44c8-8097-befa81d2599c.png) +![screenshot (6)](https://user-images.githubusercontent.com/89286474/142801595-47f73c63-501a-4221-baba-84dd97b65bf9.png) + +## Updates Coming Soon: +- [ ] Notifications +- [ ] All Day Events +- [ ] Improved Rendering Screen +- [ ] Better Graphics +- [ ] Scrolling Table +- [ ] Bangle.js V2 Compatibility +- [ ] Full Calendar (Calendar that does not have repeating weekly events.) + +## Creator +Ronin0000 + diff --git a/apps/schoolCalendar/app-icon.js b/apps/schoolCalendar/app-icon.js new file mode 100644 index 000000000..f41623fc7 --- /dev/null +++ b/apps/schoolCalendar/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwyBC/AH4A/AH4A/AH4A/AH4A/AH4A80s0AIIh/L/5f/EP4ATscsAIo9DBY4BVEJZf/L/5fRznzAIJfdEJZfpymyAJmSBpwPLBZRfqIYYBwL9OMuIBzL9VRAMRTDCJhfymBpkL+GEmABzL9UQAJelinOrPWzQDBymSCpe96+c6YnNL9N794tBAYoFD5152u21t1AoP332MuIPDVIJxB88c///AoODD4gFBLoYJBL9YBT888M4IHDNYPvvoDDznzD5pfq54BT737L4QHCVYQFCL4XTD5pfpueuAKOtqpRBxlRB5JfDEJpfqxwBIG4OOwkw/4AC+++xlxC5ZfBymyDoYlHAIJf0AImEiBbB0s0MIOtuoTJL4glML9NrtoBTLoJTBBpJfCyQfNL9VNAKZfB88cBpJfED5hfslgzEAoT3BxlxBYd795RB2uWC4tjAoQNBC4olFCIZfpFoIBJe4JJB+++AYe964XLL4YPLAIJfqhgBNueuAIJnBCp4BPL9Na9YBBsQBCAoY3BA4YBRC4INPL9oBS5YXWDoxfqJIIByL9NS1QBzL9WKAIgzBAooHFAMBfpMJABqLtYA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ALA")) diff --git a/apps/schoolCalendar/custom.html b/apps/schoolCalendar/custom.html new file mode 100644 index 000000000..b007d16e3 --- /dev/null +++ b/apps/schoolCalendar/custom.html @@ -0,0 +1,416 @@ + + + + + + + +
+

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.

+
+ + + + + +
+ + diff --git a/apps/schoolCalendar/fullcalendar/interaction/LICENSE.txt b/apps/schoolCalendar/fullcalendar/interaction/LICENSE.txt new file mode 100644 index 000000000..84924a980 --- /dev/null +++ b/apps/schoolCalendar/fullcalendar/interaction/LICENSE.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2021 Adam Shaw + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/apps/schoolCalendar/fullcalendar/interaction/README.md b/apps/schoolCalendar/fullcalendar/interaction/README.md new file mode 100644 index 000000000..6d5821d71 --- /dev/null +++ b/apps/schoolCalendar/fullcalendar/interaction/README.md @@ -0,0 +1,8 @@ + +# FullCalendar Interaction Plugin + +Provides functionality for event drag-n-drop, resizing, dateClick, and selectable actions + +[View the docs »](https://fullcalendar.io/docs/editable) + +This package was created from the [FullCalendar monorepo »](https://github.com/fullcalendar/fullcalendar) diff --git a/apps/schoolCalendar/fullcalendar/interaction/package.json b/apps/schoolCalendar/fullcalendar/interaction/package.json new file mode 100644 index 000000000..d382635a8 --- /dev/null +++ b/apps/schoolCalendar/fullcalendar/interaction/package.json @@ -0,0 +1,32 @@ +{ + "name": "@fullcalendar/interaction", + "version": "5.9.0", + "title": "FullCalendar Interaction Plugin", + "description": "Provides functionality for event drag-n-drop, resizing, dateClick, and selectable actions", + "docs": "https://fullcalendar.io/docs/editable", + "dependencies": { + "@fullcalendar/common": "workspace:~5.9.0", + "tslib": "^2.1.0" + }, + "main": "main.cjs.js", + "module": "main.js", + "types": "main.d.ts", + "jsdelivr": "main.global.min.js", + "browserGlobal": "FullCalendarInteraction", + "homepage": "https://fullcalendar.io/", + "bugs": "https://fullcalendar.io/reporting-bugs", + "repository": { + "type": "git", + "url": "https://github.com/fullcalendar/fullcalendar.git", + "homepage": "https://github.com/fullcalendar/fullcalendar" + }, + "license": "MIT", + "author": { + "name": "Adam Shaw", + "email": "arshaw@arshaw.com", + "url": "http://arshaw.com/" + }, + "devDependencies": { + "@fullcalendar/core-preact": "workspace:*" + } +} diff --git a/apps/schoolCalendar/fullcalendar/interaction/tsconfig.json b/apps/schoolCalendar/fullcalendar/interaction/tsconfig.json new file mode 100644 index 000000000..6172b1a45 --- /dev/null +++ b/apps/schoolCalendar/fullcalendar/interaction/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "rootDir": "src", + "outDir": "tsc" + }, + "include": [ + "src/**/*" + ], + "references": [ + { "path": "../common" } + ] +} diff --git a/apps/schoolCalendar/fullcalendar/locales-all.js b/apps/schoolCalendar/fullcalendar/locales-all.js new file mode 100644 index 000000000..f8be47ef2 --- /dev/null +++ b/apps/schoolCalendar/fullcalendar/locales-all.js @@ -0,0 +1,1622 @@ +[].push.apply(FullCalendar.globalLocales, function () { + 'use strict'; + + var l0 = { + code: 'af', + week: { + dow: 1, // Maandag is die eerste dag van die week. + doy: 4, // Die week wat die 4de Januarie bevat is die eerste week van die jaar. + }, + buttonText: { + prev: 'Vorige', + next: 'Volgende', + today: 'Vandag', + year: 'Jaar', + month: 'Maand', + week: 'Week', + day: 'Dag', + list: 'Agenda', + }, + allDayText: 'Heeldag', + moreLinkText: 'Addisionele', + noEventsText: 'Daar is geen gebeurtenisse nie', + }; + + var l1 = { + code: 'ar-dz', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 4, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l2 = { + code: 'ar-kw', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l3 = { + code: 'ar-ly', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l4 = { + code: 'ar-ma', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l5 = { + code: 'ar-sa', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l6 = { + code: 'ar-tn', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l7 = { + code: 'ar', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', + }; + + var l8 = { + code: 'az', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Əvvəl', + next: 'Sonra', + today: 'Bu Gün', + month: 'Ay', + week: 'Həftə', + day: 'Gün', + list: 'Gündəm', + }, + weekText: 'Həftə', + allDayText: 'Bütün Gün', + moreLinkText: function(n) { + return '+ daha çox ' + n + }, + noEventsText: 'Göstərmək üçün hadisə yoxdur', + }; + + var l9 = { + code: 'bg', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'назад', + next: 'напред', + today: 'днес', + month: 'Месец', + week: 'Седмица', + day: 'Ден', + list: 'График', + }, + allDayText: 'Цял ден', + moreLinkText: function(n) { + return '+още ' + n + }, + noEventsText: 'Няма събития за показване', + }; + + var l10 = { + code: 'bn', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'পেছনে', + next: 'সামনে', + today: 'আজ', + month: 'মাস', + week: 'সপ্তাহ', + day: 'দিন', + list: 'তালিকা', + }, + weekText: 'সপ্তাহ', + allDayText: 'সারাদিন', + moreLinkText: function(n) { + return '+অন্যান্য ' + n + }, + noEventsText: 'কোনো ইভেন্ট নেই', + }; + + var l11 = { + code: 'bs', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prošli', + next: 'Sljedeći', + today: 'Danas', + month: 'Mjesec', + week: 'Sedmica', + day: 'Dan', + list: 'Raspored', + }, + weekText: 'Sed', + allDayText: 'Cijeli dan', + moreLinkText: function(n) { + return '+ još ' + n + }, + noEventsText: 'Nema događaja za prikazivanje', + }; + + var l12 = { + code: 'ca', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Anterior', + next: 'Següent', + today: 'Avui', + month: 'Mes', + week: 'Setmana', + day: 'Dia', + list: 'Agenda', + }, + weekText: 'Set', + allDayText: 'Tot el dia', + moreLinkText: 'més', + noEventsText: 'No hi ha esdeveniments per mostrar', + }; + + var l13 = { + code: 'cs', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Dříve', + next: 'Později', + today: 'Nyní', + month: 'Měsíc', + week: 'Týden', + day: 'Den', + list: 'Agenda', + }, + weekText: 'Týd', + allDayText: 'Celý den', + moreLinkText: function(n) { + return '+další: ' + n + }, + noEventsText: 'Žádné akce k zobrazení', + }; + + var l14 = { + code: 'cy', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Blaenorol', + next: 'Nesaf', + today: 'Heddiw', + year: 'Blwyddyn', + month: 'Mis', + week: 'Wythnos', + day: 'Dydd', + list: 'Rhestr', + }, + weekText: 'Wythnos', + allDayText: 'Trwy\'r dydd', + moreLinkText: 'Mwy', + noEventsText: 'Dim digwyddiadau', + }; + + var l15 = { + code: 'da', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Forrige', + next: 'Næste', + today: 'I dag', + month: 'Måned', + week: 'Uge', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Uge', + allDayText: 'Hele dagen', + moreLinkText: 'flere', + noEventsText: 'Ingen arrangementer at vise', + }; + + var l16 = { + code: 'de-at', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zurück', + next: 'Vor', + today: 'Heute', + year: 'Jahr', + month: 'Monat', + week: 'Woche', + day: 'Tag', + list: 'Terminübersicht', + }, + weekText: 'KW', + allDayText: 'Ganztägig', + moreLinkText: function(n) { + return '+ weitere ' + n + }, + noEventsText: 'Keine Ereignisse anzuzeigen', + }; + + var l17 = { + code: 'de', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zurück', + next: 'Vor', + today: 'Heute', + year: 'Jahr', + month: 'Monat', + week: 'Woche', + day: 'Tag', + list: 'Terminübersicht', + }, + weekText: 'KW', + allDayText: 'Ganztägig', + moreLinkText: function(n) { + return '+ weitere ' + n + }, + noEventsText: 'Keine Ereignisse anzuzeigen', + }; + + var l18 = { + code: 'el', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4st is the first week of the year. + }, + buttonText: { + prev: 'Προηγούμενος', + next: 'Επόμενος', + today: 'Σήμερα', + month: 'Μήνας', + week: 'Εβδομάδα', + day: 'Ημέρα', + list: 'Ατζέντα', + }, + weekText: 'Εβδ', + allDayText: 'Ολοήμερο', + moreLinkText: 'περισσότερα', + noEventsText: 'Δεν υπάρχουν γεγονότα προς εμφάνιση', + }; + + var l19 = { + code: 'en-au', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + }; + + var l20 = { + code: 'en-gb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + }; + + var l21 = { + code: 'en-nz', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + }; + + var l22 = { + code: 'eo', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Antaŭa', + next: 'Sekva', + today: 'Hodiaŭ', + month: 'Monato', + week: 'Semajno', + day: 'Tago', + list: 'Tagordo', + }, + weekText: 'Sm', + allDayText: 'Tuta tago', + moreLinkText: 'pli', + noEventsText: 'Neniuj eventoj por montri', + }; + + var l23 = { + code: 'es', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Sig', + today: 'Hoy', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Todo el día', + moreLinkText: 'más', + noEventsText: 'No hay eventos para mostrar', + }; + + var l24 = { + code: 'es', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Sig', + today: 'Hoy', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Todo el día', + moreLinkText: 'más', + noEventsText: 'No hay eventos para mostrar', + }; + + var l25 = { + code: 'et', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Eelnev', + next: 'Järgnev', + today: 'Täna', + month: 'Kuu', + week: 'Nädal', + day: 'Päev', + list: 'Päevakord', + }, + weekText: 'näd', + allDayText: 'Kogu päev', + moreLinkText: function(n) { + return '+ veel ' + n + }, + noEventsText: 'Kuvamiseks puuduvad sündmused', + }; + + var l26 = { + code: 'eu', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Aur', + next: 'Hur', + today: 'Gaur', + month: 'Hilabetea', + week: 'Astea', + day: 'Eguna', + list: 'Agenda', + }, + weekText: 'As', + allDayText: 'Egun osoa', + moreLinkText: 'gehiago', + noEventsText: 'Ez dago ekitaldirik erakusteko', + }; + + var l27 = { + code: 'fa', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'قبلی', + next: 'بعدی', + today: 'امروز', + month: 'ماه', + week: 'هفته', + day: 'روز', + list: 'برنامه', + }, + weekText: 'هف', + allDayText: 'تمام روز', + moreLinkText: function(n) { + return 'بیش از ' + n + }, + noEventsText: 'هیچ رویدادی به نمایش', + }; + + var l28 = { + code: 'fi', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Edellinen', + next: 'Seuraava', + today: 'Tänään', + month: 'Kuukausi', + week: 'Viikko', + day: 'Päivä', + list: 'Tapahtumat', + }, + weekText: 'Vk', + allDayText: 'Koko päivä', + moreLinkText: 'lisää', + noEventsText: 'Ei näytettäviä tapahtumia', + }; + + var l29 = { + code: 'fr', + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: "Aujourd'hui", + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Mon planning', + }, + weekText: 'Sem.', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun événement à afficher', + }; + + var l30 = { + code: 'fr-ch', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: 'Courant', + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Mon planning', + }, + weekText: 'Sm', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun événement à afficher', + }; + + var l31 = { + code: 'fr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: "Aujourd'hui", + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Planning', + }, + weekText: 'Sem.', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun événement à afficher', + }; + + var l32 = { + code: 'gl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Seg', + today: 'Hoxe', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Axenda', + }, + weekText: 'Sm', + allDayText: 'Todo o día', + moreLinkText: 'máis', + noEventsText: 'Non hai eventos para amosar', + }; + + var l33 = { + code: 'he', + direction: 'rtl', + buttonText: { + prev: 'הקודם', + next: 'הבא', + today: 'היום', + month: 'חודש', + week: 'שבוע', + day: 'יום', + list: 'סדר יום', + }, + allDayText: 'כל היום', + moreLinkText: 'אחר', + noEventsText: 'אין אירועים להצגה', + weekText: 'שבוע', + }; + + var l34 = { + code: 'hi', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'पिछला', + next: 'अगला', + today: 'आज', + month: 'महीना', + week: 'सप्ताह', + day: 'दिन', + list: 'कार्यसूची', + }, + weekText: 'हफ्ता', + allDayText: 'सभी दिन', + moreLinkText: function(n) { + return '+अधिक ' + n + }, + noEventsText: 'कोई घटनाओं को प्रदर्शित करने के लिए', + }; + + var l35 = { + code: 'hr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prijašnji', + next: 'Sljedeći', + today: 'Danas', + month: 'Mjesec', + week: 'Tjedan', + day: 'Dan', + list: 'Raspored', + }, + weekText: 'Tje', + allDayText: 'Cijeli dan', + moreLinkText: function(n) { + return '+ još ' + n + }, + noEventsText: 'Nema događaja za prikaz', + }; + + var l36 = { + code: 'hu', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'vissza', + next: 'előre', + today: 'ma', + month: 'Hónap', + week: 'Hét', + day: 'Nap', + list: 'Lista', + }, + weekText: 'Hét', + allDayText: 'Egész nap', + moreLinkText: 'további', + noEventsText: 'Nincs megjeleníthető esemény', + }; + + var l37 = { + code: 'hy-am', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Նախորդ', + next: 'Հաջորդ', + today: 'Այսօր', + month: 'Ամիս', + week: 'Շաբաթ', + day: 'Օր', + list: 'Օրվա ցուցակ', + }, + weekText: 'Շաբ', + allDayText: 'Ամբողջ օր', + moreLinkText: function(n) { + return '+ ևս ' + n + }, + noEventsText: 'Բացակայում է իրադարձությունը ցուցադրելու', + }; + + var l38 = { + code: 'id', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'mundur', + next: 'maju', + today: 'hari ini', + month: 'Bulan', + week: 'Minggu', + day: 'Hari', + list: 'Agenda', + }, + weekText: 'Mg', + allDayText: 'Sehari penuh', + moreLinkText: 'lebih', + noEventsText: 'Tidak ada acara untuk ditampilkan', + }; + + var l39 = { + code: 'is', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Fyrri', + next: 'Næsti', + today: 'Í dag', + month: 'Mánuður', + week: 'Vika', + day: 'Dagur', + list: 'Dagskrá', + }, + weekText: 'Vika', + allDayText: 'Allan daginn', + moreLinkText: 'meira', + noEventsText: 'Engir viðburðir til að sýna', + }; + + var l40 = { + code: 'it', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Prec', + next: 'Succ', + today: 'Oggi', + month: 'Mese', + week: 'Settimana', + day: 'Giorno', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Tutto il giorno', + moreLinkText: function(n) { + return '+altri ' + n + }, + noEventsText: 'Non ci sono eventi da visualizzare', + }; + + var l41 = { + code: 'ja', + buttonText: { + prev: '前', + next: '次', + today: '今日', + month: '月', + week: '週', + day: '日', + list: '予定リスト', + }, + weekText: '週', + allDayText: '終日', + moreLinkText: function(n) { + return '他 ' + n + ' 件' + }, + noEventsText: '表示する予定はありません', + }; + + var l42 = { + code: 'ka', + week: { + dow: 1, + doy: 7, + }, + buttonText: { + prev: 'წინა', + next: 'შემდეგი', + today: 'დღეს', + month: 'თვე', + week: 'კვირა', + day: 'დღე', + list: 'დღის წესრიგი', + }, + weekText: 'კვ', + allDayText: 'მთელი დღე', + moreLinkText: function(n) { + return '+ კიდევ ' + n + }, + noEventsText: 'ღონისძიებები არ არის', + }; + + var l43 = { + code: 'kk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Алдыңғы', + next: 'Келесі', + today: 'Бүгін', + month: 'Ай', + week: 'Апта', + day: 'Күн', + list: 'Күн тәртібі', + }, + weekText: 'Не', + allDayText: 'Күні бойы', + moreLinkText: function(n) { + return '+ тағы ' + n + }, + noEventsText: 'Көрсету үшін оқиғалар жоқ', + }; + + var l44 = { + code: 'km', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'មុន', + next: 'បន្ទាប់', + today: 'ថ្ងៃនេះ', + year: 'ឆ្នាំ', + month: 'ខែ', + week: 'សប្តាហ៍', + day: 'ថ្ងៃ', + list: 'បញ្ជី', + }, + weekText: 'សប្តាហ៍', + allDayText: 'ពេញមួយថ្ងៃ', + moreLinkText: 'ច្រើនទៀត', + noEventsText: 'គ្មានព្រឹត្តិការណ៍ត្រូវបង្ហាញ', + }; + + var l45 = { + code: 'ko', + buttonText: { + prev: '이전달', + next: '다음달', + today: '오늘', + month: '월', + week: '주', + day: '일', + list: '일정목록', + }, + weekText: '주', + allDayText: '종일', + moreLinkText: '개', + noEventsText: '일정이 없습니다', + }; + + var l46 = { + code: 'ku', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'پێشتر', + next: 'دواتر', + today: 'ئەمڕو', + month: 'مانگ', + week: 'هەفتە', + day: 'ڕۆژ', + list: 'بەرنامە', + }, + weekText: 'هەفتە', + allDayText: 'هەموو ڕۆژەکە', + moreLinkText: 'زیاتر', + noEventsText: 'هیچ ڕووداوێك نیە', + }; + + var l47 = { + code: 'lb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zréck', + next: 'Weider', + today: 'Haut', + month: 'Mount', + week: 'Woch', + day: 'Dag', + list: 'Terminiwwersiicht', + }, + weekText: 'W', + allDayText: 'Ganzen Dag', + moreLinkText: 'méi', + noEventsText: 'Nee Evenementer ze affichéieren', + }; + + var l48 = { + code: 'lt', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Atgal', + next: 'Pirmyn', + today: 'Šiandien', + month: 'Mėnuo', + week: 'Savaitė', + day: 'Diena', + list: 'Darbotvarkė', + }, + weekText: 'SAV', + allDayText: 'Visą dieną', + moreLinkText: 'daugiau', + noEventsText: 'Nėra įvykių rodyti', + }; + + var l49 = { + code: 'lv', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Iepr.', + next: 'Nāk.', + today: 'Šodien', + month: 'Mēnesis', + week: 'Nedēļa', + day: 'Diena', + list: 'Dienas kārtība', + }, + weekText: 'Ned.', + allDayText: 'Visu dienu', + moreLinkText: function(n) { + return '+vēl ' + n + }, + noEventsText: 'Nav notikumu', + }; + + var l50 = { + code: 'mk', + buttonText: { + prev: 'претходно', + next: 'следно', + today: 'Денес', + month: 'Месец', + week: 'Недела', + day: 'Ден', + list: 'График', + }, + weekText: 'Сед', + allDayText: 'Цел ден', + moreLinkText: function(n) { + return '+повеќе ' + n + }, + noEventsText: 'Нема настани за прикажување', + }; + + var l51 = { + code: 'ms', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Sebelum', + next: 'Selepas', + today: 'hari ini', + month: 'Bulan', + week: 'Minggu', + day: 'Hari', + list: 'Agenda', + }, + weekText: 'Mg', + allDayText: 'Sepanjang hari', + moreLinkText: function(n) { + return 'masih ada ' + n + ' acara' + }, + noEventsText: 'Tiada peristiwa untuk dipaparkan', + }; + + var l52 = { + code: 'nb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Forrige', + next: 'Neste', + today: 'I dag', + month: 'Måned', + week: 'Uke', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Uke', + allDayText: 'Hele dagen', + moreLinkText: 'til', + noEventsText: 'Ingen hendelser å vise', + }; + + var l53 = { + code: 'ne', // code for nepal + week: { + dow: 7, // Sunday is the first day of the week. + doy: 1, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'अघिल्लो', + next: 'अर्को', + today: 'आज', + month: 'महिना', + week: 'हप्ता', + day: 'दिन', + list: 'सूची', + }, + weekText: 'हप्ता', + allDayText: 'दिनभरि', + moreLinkText: 'थप लिंक', + noEventsText: 'देखाउनको लागि कुनै घटनाहरू छैनन्', + }; + + var l54 = { + code: 'nl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Vorige', + next: 'Volgende', + today: 'Vandaag', + year: 'Jaar', + month: 'Maand', + week: 'Week', + day: 'Dag', + list: 'Agenda', + }, + allDayText: 'Hele dag', + moreLinkText: 'extra', + noEventsText: 'Geen evenementen om te laten zien', + }; + + var l55 = { + code: 'nn', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Førre', + next: 'Neste', + today: 'I dag', + month: 'Månad', + week: 'Veke', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Veke', + allDayText: 'Heile dagen', + moreLinkText: 'til', + noEventsText: 'Ingen hendelser å vise', + }; + + var l56 = { + code: 'pl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Poprzedni', + next: 'Następny', + today: 'Dziś', + month: 'Miesiąc', + week: 'Tydzień', + day: 'Dzień', + list: 'Plan dnia', + }, + weekText: 'Tydz', + allDayText: 'Cały dzień', + moreLinkText: 'więcej', + noEventsText: 'Brak wydarzeń do wyświetlenia', + }; + + var l57 = { + code: 'pt-br', + buttonText: { + prev: 'Anterior', + next: 'Próximo', + today: 'Hoje', + month: 'Mês', + week: 'Semana', + day: 'Dia', + list: 'Lista', + }, + weekText: 'Sm', + allDayText: 'dia inteiro', + moreLinkText: function(n) { + return 'mais +' + n + }, + noEventsText: 'Não há eventos para mostrar', + }; + + var l58 = { + code: 'pt', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Anterior', + next: 'Seguinte', + today: 'Hoje', + month: 'Mês', + week: 'Semana', + day: 'Dia', + list: 'Agenda', + }, + weekText: 'Sem', + allDayText: 'Todo o dia', + moreLinkText: 'mais', + noEventsText: 'Não há eventos para mostrar', + }; + + var l59 = { + code: 'ro', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'precedentă', + next: 'următoare', + today: 'Azi', + month: 'Lună', + week: 'Săptămână', + day: 'Zi', + list: 'Agendă', + }, + weekText: 'Săpt', + allDayText: 'Toată ziua', + moreLinkText: function(n) { + return '+alte ' + n + }, + noEventsText: 'Nu există evenimente de afișat', + }; + + var l60 = { + code: 'ru', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Пред', + next: 'След', + today: 'Сегодня', + month: 'Месяц', + week: 'Неделя', + day: 'День', + list: 'Повестка дня', + }, + weekText: 'Нед', + allDayText: 'Весь день', + moreLinkText: function(n) { + return '+ ещё ' + n + }, + noEventsText: 'Нет событий для отображения', + }; + + var l61 = { + code: 'sk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Predchádzajúci', + next: 'Nasledujúci', + today: 'Dnes', + month: 'Mesiac', + week: 'Týždeň', + day: 'Deň', + list: 'Rozvrh', + }, + weekText: 'Ty', + allDayText: 'Celý deň', + moreLinkText: function(n) { + return '+ďalšie: ' + n + }, + noEventsText: 'Žiadne akcie na zobrazenie', + }; + + var l62 = { + code: 'sl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prejšnji', + next: 'Naslednji', + today: 'Trenutni', + month: 'Mesec', + week: 'Teden', + day: 'Dan', + list: 'Dnevni red', + }, + weekText: 'Teden', + allDayText: 'Ves dan', + moreLinkText: 'več', + noEventsText: 'Ni dogodkov za prikaz', + }; + + var l63 = { + code: 'sm', + buttonText: { + prev: 'Talu ai', + next: 'Mulimuli atu', + today: 'Aso nei', + month: 'Masina', + week: 'Vaiaso', + day: 'Aso', + list: 'Faasologa', + }, + weekText: 'Vaiaso', + allDayText: 'Aso atoa', + moreLinkText: 'sili atu', + noEventsText: 'Leai ni mea na tutupu', + }; + + var l64 = { + code: 'sq', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'mbrapa', + next: 'Përpara', + today: 'sot', + month: 'Muaj', + week: 'Javë', + day: 'Ditë', + list: 'Listë', + }, + weekText: 'Ja', + allDayText: 'Gjithë ditën', + moreLinkText: function(n) { + return '+më tepër ' + n + }, + noEventsText: 'Nuk ka evente për të shfaqur', + }; + + var l65 = { + code: 'sr-cyrl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Претходна', + next: 'следећи', + today: 'Данас', + month: 'Месец', + week: 'Недеља', + day: 'Дан', + list: 'Планер', + }, + weekText: 'Сед', + allDayText: 'Цео дан', + moreLinkText: function(n) { + return '+ још ' + n + }, + noEventsText: 'Нема догађаја за приказ', + }; + + var l66 = { + code: 'sr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prethodna', + next: 'Sledeći', + today: 'Danas', + month: 'Mеsеc', + week: 'Nеdеlja', + day: 'Dan', + list: 'Planеr', + }, + weekText: 'Sed', + allDayText: 'Cеo dan', + moreLinkText: function(n) { + return '+ još ' + n + }, + noEventsText: 'Nеma događaja za prikaz', + }; + + var l67 = { + code: 'sv', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Förra', + next: 'Nästa', + today: 'Idag', + month: 'Månad', + week: 'Vecka', + day: 'Dag', + list: 'Program', + }, + weekText: 'v.', + allDayText: 'Heldag', + moreLinkText: 'till', + noEventsText: 'Inga händelser att visa', + }; + + var l68 = { + code: 'ta-in', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'முந்தைய', + next: 'அடுத்தது', + today: 'இன்று', + month: 'மாதம்', + week: 'வாரம்', + day: 'நாள்', + list: 'தினசரி அட்டவணை', + }, + weekText: 'வாரம்', + allDayText: 'நாள் முழுவதும்', + moreLinkText: function(n) { + return '+ மேலும் ' + n + }, + noEventsText: 'காண்பிக்க நிகழ்வுகள் இல்லை', + }; + + var l69 = { + code: 'th', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'ก่อนหน้า', + next: 'ถัดไป', + prevYear: 'ปีก่อนหน้า', + nextYear: 'ปีถัดไป', + year: 'ปี', + today: 'วันนี้', + month: 'เดือน', + week: 'สัปดาห์', + day: 'วัน', + list: 'กำหนดการ', + }, + weekText: 'สัปดาห์', + allDayText: 'ตลอดวัน', + moreLinkText: 'เพิ่มเติม', + noEventsText: 'ไม่มีกิจกรรมที่จะแสดง', + }; + + var l70 = { + code: 'tr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'geri', + next: 'ileri', + today: 'bugün', + month: 'Ay', + week: 'Hafta', + day: 'Gün', + list: 'Ajanda', + }, + weekText: 'Hf', + allDayText: 'Tüm gün', + moreLinkText: 'daha fazla', + noEventsText: 'Gösterilecek etkinlik yok', + }; + + var l71 = { + code: 'ug', + buttonText: { + month: 'ئاي', + week: 'ھەپتە', + day: 'كۈن', + list: 'كۈنتەرتىپ', + }, + allDayText: 'پۈتۈن كۈن', + }; + + var l72 = { + code: 'uk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Попередній', + next: 'далі', + today: 'Сьогодні', + month: 'Місяць', + week: 'Тиждень', + day: 'День', + list: 'Порядок денний', + }, + weekText: 'Тиж', + allDayText: 'Увесь день', + moreLinkText: function(n) { + return '+ще ' + n + '...' + }, + noEventsText: 'Немає подій для відображення', + }; + + var l73 = { + code: 'uz', + buttonText: { + month: 'Oy', + week: 'Xafta', + day: 'Kun', + list: 'Kun tartibi', + }, + allDayText: "Kun bo'yi", + moreLinkText: function(n) { + return '+ yana ' + n + }, + noEventsText: "Ko'rsatish uchun voqealar yo'q", + }; + + var l74 = { + code: 'vi', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Trước', + next: 'Tiếp', + today: 'Hôm nay', + month: 'Tháng', + week: 'Tuần', + day: 'Ngày', + list: 'Lịch biểu', + }, + weekText: 'Tu', + allDayText: 'Cả ngày', + moreLinkText: function(n) { + return '+ thêm ' + n + }, + noEventsText: 'Không có sự kiện để hiển thị', + }; + + var l75 = { + code: 'zh-cn', + week: { + // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: '上月', + next: '下月', + today: '今天', + month: '月', + week: '周', + day: '日', + list: '日程', + }, + weekText: '周', + allDayText: '全天', + moreLinkText: function(n) { + return '另外 ' + n + ' 个' + }, + noEventsText: '没有事件显示', + }; + + var l76 = { + code: 'zh-tw', + buttonText: { + prev: '上月', + next: '下月', + today: '今天', + month: '月', + week: '週', + day: '天', + list: '活動列表', + }, + weekText: '周', + allDayText: '整天', + moreLinkText: '顯示更多', + noEventsText: '没有任何活動', + }; + + /* eslint max-len: off */ + + var localesAll = [ + l0, l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, l26, l27, l28, l29, l30, l31, l32, l33, l34, l35, l36, l37, l38, l39, l40, l41, l42, l43, l44, l45, l46, l47, l48, l49, l50, l51, l52, l53, l54, l55, l56, l57, l58, l59, l60, l61, l62, l63, l64, l65, l66, l67, l68, l69, l70, l71, l72, l73, l74, l75, l76, + ]; + + return localesAll; + +}()); diff --git a/apps/schoolCalendar/fullcalendar/main.css b/apps/schoolCalendar/fullcalendar/main.css new file mode 100644 index 000000000..957887b56 --- /dev/null +++ b/apps/schoolCalendar/fullcalendar/main.css @@ -0,0 +1,1446 @@ + +/* classes attached to */ +/* TODO: make fc-event selector work when calender in shadow DOM */ +.fc-not-allowed, +.fc-not-allowed .fc-event { /* override events' custom cursors */ + cursor: not-allowed; +} + +/* TODO: not attached to body. attached to specific els. move */ +.fc-unselectable { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +.fc { + /* layout of immediate children */ + display: flex; + flex-direction: column; + + font-size: 1em +} +.fc, + .fc *, + .fc *:before, + .fc *:after { + box-sizing: border-box; + } +.fc table { + border-collapse: collapse; + border-spacing: 0; + font-size: 1em; /* normalize cross-browser */ + } +.fc th { + text-align: center; + } +.fc th, + .fc td { + vertical-align: top; + padding: 0; + } +.fc a[data-navlink] { + cursor: pointer; + } +.fc a[data-navlink]:hover { + text-decoration: underline; + } +.fc-direction-ltr { + direction: ltr; + text-align: left; +} +.fc-direction-rtl { + direction: rtl; + text-align: right; +} +.fc-theme-standard td, + .fc-theme-standard th { + border: 1px solid #ddd; + border: 1px solid var(--fc-border-color, #ddd); + } +/* for FF, which doesn't expand a 100% div within a table cell. use absolute positioning */ +/* inner-wrappers are responsible for being absolute */ +/* TODO: best place for this? */ +.fc-liquid-hack td, + .fc-liquid-hack th { + position: relative; + } + +@font-face { + font-family: 'fcicons'; + src: url("data:application/x-font-ttf;charset=utf-8;base64,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") format('truetype'); + font-weight: normal; + font-style: normal; +} + +.fc-icon { + /* added for fc */ + display: inline-block; + width: 1em; + height: 1em; + text-align: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'fcicons' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.fc-icon-chevron-left:before { + content: "\e900"; +} + +.fc-icon-chevron-right:before { + content: "\e901"; +} + +.fc-icon-chevrons-left:before { + content: "\e902"; +} + +.fc-icon-chevrons-right:before { + content: "\e903"; +} + +.fc-icon-minus-square:before { + content: "\e904"; +} + +.fc-icon-plus-square:before { + content: "\e905"; +} + +.fc-icon-x:before { + content: "\e906"; +} +/* +Lots taken from Flatly (MIT): https://bootswatch.com/4/flatly/bootstrap.css + +These styles only apply when the standard-theme is activated. +When it's NOT activated, the fc-button classes won't even be in the DOM. +*/ +.fc { + + /* reset */ + +} +.fc .fc-button { + border-radius: 0; + overflow: visible; + text-transform: none; + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; + } +.fc .fc-button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; + } +.fc .fc-button { + -webkit-appearance: button; + } +.fc .fc-button:not(:disabled) { + cursor: pointer; + } +.fc .fc-button::-moz-focus-inner { + padding: 0; + border-style: none; + } +.fc { + + /* theme */ + +} +.fc .fc-button { + display: inline-block; + font-weight: 400; + text-align: center; + vertical-align: middle; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.4em 0.65em; + font-size: 1em; + line-height: 1.5; + border-radius: 0.25em; + } +.fc .fc-button:hover { + text-decoration: none; + } +.fc .fc-button:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(44, 62, 80, 0.25); + } +.fc .fc-button:disabled { + opacity: 0.65; + } +.fc { + + /* "primary" coloring */ + +} +.fc .fc-button-primary { + color: #fff; + color: var(--fc-button-text-color, #fff); + background-color: #2C3E50; + background-color: var(--fc-button-bg-color, #2C3E50); + border-color: #2C3E50; + border-color: var(--fc-button-border-color, #2C3E50); + } +.fc .fc-button-primary:hover { + color: #fff; + color: var(--fc-button-text-color, #fff); + background-color: #1e2b37; + background-color: var(--fc-button-hover-bg-color, #1e2b37); + border-color: #1a252f; + border-color: var(--fc-button-hover-border-color, #1a252f); + } +.fc .fc-button-primary:disabled { /* not DRY */ + color: #fff; + color: var(--fc-button-text-color, #fff); + background-color: #2C3E50; + background-color: var(--fc-button-bg-color, #2C3E50); + border-color: #2C3E50; + border-color: var(--fc-button-border-color, #2C3E50); /* overrides :hover */ + } +.fc .fc-button-primary:focus { + box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5); + } +.fc .fc-button-primary:not(:disabled):active, + .fc .fc-button-primary:not(:disabled).fc-button-active { + color: #fff; + color: var(--fc-button-text-color, #fff); + background-color: #1a252f; + background-color: var(--fc-button-active-bg-color, #1a252f); + border-color: #151e27; + border-color: var(--fc-button-active-border-color, #151e27); + } +.fc .fc-button-primary:not(:disabled):active:focus, + .fc .fc-button-primary:not(:disabled).fc-button-active:focus { + box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5); + } +.fc { + + /* icons within buttons */ + +} +.fc .fc-button .fc-icon { + vertical-align: middle; + font-size: 1.5em; /* bump up the size (but don't make it bigger than line-height of button, which is 1.5em also) */ + } +.fc .fc-button-group { + position: relative; + display: inline-flex; + vertical-align: middle; + } +.fc .fc-button-group > .fc-button { + position: relative; + flex: 1 1 auto; + } +.fc .fc-button-group > .fc-button:hover { + z-index: 1; + } +.fc .fc-button-group > .fc-button:focus, + .fc .fc-button-group > .fc-button:active, + .fc .fc-button-group > .fc-button.fc-button-active { + z-index: 1; + } +.fc-direction-ltr .fc-button-group > .fc-button:not(:first-child) { + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +.fc-direction-ltr .fc-button-group > .fc-button:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } +.fc-direction-rtl .fc-button-group > .fc-button:not(:first-child) { + margin-right: -1px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } +.fc-direction-rtl .fc-button-group > .fc-button:not(:last-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +.fc .fc-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + } +.fc .fc-toolbar.fc-header-toolbar { + margin-bottom: 1.5em; + } +.fc .fc-toolbar.fc-footer-toolbar { + margin-top: 1.5em; + } +.fc .fc-toolbar-title { + font-size: 1.75em; + margin: 0; + } +.fc-direction-ltr .fc-toolbar > * > :not(:first-child) { + margin-left: .75em; /* space between */ + } +.fc-direction-rtl .fc-toolbar > * > :not(:first-child) { + margin-right: .75em; /* space between */ + } +.fc-direction-rtl .fc-toolbar-ltr { /* when the toolbar-chunk positioning system is explicitly left-to-right */ + flex-direction: row-reverse; + } +.fc .fc-scroller { + -webkit-overflow-scrolling: touch; + position: relative; /* for abs-positioned elements within */ + } +.fc .fc-scroller-liquid { + height: 100%; + } +.fc .fc-scroller-liquid-absolute { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + } +.fc .fc-scroller-harness { + position: relative; + overflow: hidden; + direction: ltr; + /* hack for chrome computing the scroller's right/left wrong for rtl. undone below... */ + /* TODO: demonstrate in codepen */ + } +.fc .fc-scroller-harness-liquid { + height: 100%; + } +.fc-direction-rtl .fc-scroller-harness > .fc-scroller { /* undo above hack */ + direction: rtl; + } +.fc-theme-standard .fc-scrollgrid { + border: 1px solid #ddd; + border: 1px solid var(--fc-border-color, #ddd); /* bootstrap does this. match */ + } +.fc .fc-scrollgrid, + .fc .fc-scrollgrid table { /* all tables (self included) */ + width: 100%; /* because tables don't normally do this */ + table-layout: fixed; + } +.fc .fc-scrollgrid table { /* inner tables */ + border-top-style: hidden; + border-left-style: hidden; + border-right-style: hidden; + } +.fc .fc-scrollgrid { + + border-collapse: separate; + border-right-width: 0; + border-bottom-width: 0; + + } +.fc .fc-scrollgrid-liquid { + height: 100%; + } +.fc .fc-scrollgrid-section { /* a */ + height: 1px /* better than 0, for firefox */ + + } +.fc .fc-scrollgrid-section > td { + height: 1px; /* needs a height so inner div within grow. better than 0, for firefox */ + } +.fc .fc-scrollgrid-section table { + height: 1px; + /* for most browsers, if a height isn't set on the table, can't do liquid-height within cells */ + /* serves as a min-height. harmless */ + } +.fc .fc-scrollgrid-section-liquid > td { + height: 100%; /* better than `auto`, for firefox */ + } +.fc .fc-scrollgrid-section > * { + border-top-width: 0; + border-left-width: 0; + } +.fc .fc-scrollgrid-section-header > *, + .fc .fc-scrollgrid-section-footer > * { + border-bottom-width: 0; + } +.fc .fc-scrollgrid-section-body table, + .fc .fc-scrollgrid-section-footer table { + border-bottom-style: hidden; /* head keeps its bottom border tho */ + } +.fc { + + /* stickiness */ + +} +.fc .fc-scrollgrid-section-sticky > * { + background: #fff; + background: var(--fc-page-bg-color, #fff); + position: sticky; + z-index: 3; /* TODO: var */ + /* TODO: box-shadow when sticking */ + } +.fc .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky > * { + top: 0; /* because border-sharing causes a gap at the top */ + /* TODO: give safari -1. has bug */ + } +.fc .fc-scrollgrid-section-footer.fc-scrollgrid-section-sticky > * { + bottom: 0; /* known bug: bottom-stickiness doesn't work in safari */ + } +.fc .fc-scrollgrid-sticky-shim { /* for horizontal scrollbar */ + height: 1px; /* needs height to create scrollbars */ + margin-bottom: -1px; + } +.fc-sticky { /* no .fc wrap because used as child of body */ + position: sticky; +} +.fc .fc-view-harness { + flex-grow: 1; /* because this harness is WITHIN the .fc's flexbox */ + position: relative; + } +.fc { + + /* when the harness controls the height, make the view liquid */ + +} +.fc .fc-view-harness-active > .fc-view { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } +.fc .fc-col-header-cell-cushion { + display: inline-block; /* x-browser for when sticky (when multi-tier header) */ + padding: 2px 4px; + } +.fc .fc-bg-event, + .fc .fc-non-business, + .fc .fc-highlight { + /* will always have a harness with position:relative/absolute, so absolutely expand */ + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } +.fc .fc-non-business { + background: rgba(215, 215, 215, 0.3); + background: var(--fc-non-business-color, rgba(215, 215, 215, 0.3)); + } +.fc .fc-bg-event { + background: rgb(143, 223, 130); + background: var(--fc-bg-event-color, rgb(143, 223, 130)); + opacity: 0.3; + opacity: var(--fc-bg-event-opacity, 0.3) + } +.fc .fc-bg-event .fc-event-title { + margin: .5em; + font-size: .85em; + font-size: var(--fc-small-font-size, .85em); + font-style: italic; + } +.fc .fc-highlight { + background: rgba(188, 232, 241, 0.3); + background: var(--fc-highlight-color, rgba(188, 232, 241, 0.3)); + } +.fc .fc-cell-shaded, + .fc .fc-day-disabled { + background: rgba(208, 208, 208, 0.3); + background: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); + } +/* link resets */ +/* ---------------------------------------------------------------------------------------------------- */ +a.fc-event, +a.fc-event:hover { + text-decoration: none; +} +/* cursor */ +.fc-event[href], +.fc-event.fc-event-draggable { + cursor: pointer; +} +/* event text content */ +/* ---------------------------------------------------------------------------------------------------- */ +.fc-event .fc-event-main { + position: relative; + z-index: 2; + } +/* dragging */ +/* ---------------------------------------------------------------------------------------------------- */ +.fc-event-dragging:not(.fc-event-selected) { /* MOUSE */ + opacity: 0.75; + } +.fc-event-dragging.fc-event-selected { /* TOUCH */ + box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3); + } +/* resizing */ +/* ---------------------------------------------------------------------------------------------------- */ +/* (subclasses should hone positioning for touch and non-touch) */ +.fc-event .fc-event-resizer { + display: none; + position: absolute; + z-index: 4; + } +.fc-event:hover, /* MOUSE */ +.fc-event-selected { /* TOUCH */ + +} +.fc-event:hover .fc-event-resizer, .fc-event-selected .fc-event-resizer { + display: block; + } +.fc-event-selected .fc-event-resizer { + border-radius: 4px; + border-radius: calc(var(--fc-event-resizer-dot-total-width, 8px) / 2); + border-width: 1px; + border-width: var(--fc-event-resizer-dot-border-width, 1px); + width: 8px; + width: var(--fc-event-resizer-dot-total-width, 8px); + height: 8px; + height: var(--fc-event-resizer-dot-total-width, 8px); + border-style: solid; + border-color: inherit; + background: #fff; + background: var(--fc-page-bg-color, #fff) + + /* expand hit area */ + + } +.fc-event-selected .fc-event-resizer:before { + content: ''; + position: absolute; + top: -20px; + left: -20px; + right: -20px; + bottom: -20px; + } +/* selecting (always TOUCH) */ +/* ---------------------------------------------------------------------------------------------------- */ +.fc-event-selected { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) + + /* expand hit area (subclasses should expand) */ + +} +.fc-event-selected:before { + content: ""; + position: absolute; + z-index: 3; + top: 0; + left: 0; + right: 0; + bottom: 0; + } +.fc-event-selected { + + /* dimmer effect */ + +} +.fc-event-selected:after { + content: ""; + background: rgba(0, 0, 0, 0.25); + background: var(--fc-event-selected-overlay-color, rgba(0, 0, 0, 0.25)); + position: absolute; + z-index: 1; + + /* assume there's a border on all sides. overcome it. */ + /* sometimes there's NOT a border, in which case the dimmer will go over */ + /* an adjacent border, which looks fine. */ + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + } +/* +A HORIZONTAL event +*/ +.fc-h-event { /* allowed to be top-level */ + display: block; + border: 1px solid #3788d8; + border: 1px solid var(--fc-event-border-color, #3788d8); + background-color: #3788d8; + background-color: var(--fc-event-bg-color, #3788d8) + +} +.fc-h-event .fc-event-main { + color: #fff; + color: var(--fc-event-text-color, #fff); + } +.fc-h-event .fc-event-main-frame { + display: flex; /* for make fc-event-title-container expand */ + } +.fc-h-event .fc-event-time { + max-width: 100%; /* clip overflow on this element */ + overflow: hidden; + } +.fc-h-event .fc-event-title-container { /* serves as a container for the sticky cushion */ + flex-grow: 1; + flex-shrink: 1; + min-width: 0; /* important for allowing to shrink all the way */ + } +.fc-h-event .fc-event-title { + display: inline-block; /* need this to be sticky cross-browser */ + vertical-align: top; /* for not messing up line-height */ + left: 0; /* for sticky */ + right: 0; /* for sticky */ + max-width: 100%; /* clip overflow on this element */ + overflow: hidden; + } +.fc-h-event.fc-event-selected:before { + /* expand hit area */ + top: -10px; + bottom: -10px; + } +/* adjust border and border-radius (if there is any) for non-start/end */ +.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-start), +.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-end) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left-width: 0; +} +.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-end), +.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-start) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right-width: 0; +} +/* resizers */ +.fc-h-event:not(.fc-event-selected) .fc-event-resizer { + top: 0; + bottom: 0; + width: 8px; + width: var(--fc-event-resizer-thickness, 8px); +} +.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start, +.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end { + cursor: w-resize; + left: -4px; + left: calc(var(--fc-event-resizer-thickness, 8px) / -2); +} +.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end, +.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start { + cursor: e-resize; + right: -4px; + right: calc(var(--fc-event-resizer-thickness, 8px) / -2); +} +/* resizers for TOUCH */ +.fc-h-event.fc-event-selected .fc-event-resizer { + top: 50%; + margin-top: -4px; + margin-top: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); +} +.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-start, +.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end { + left: -4px; + left: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); +} +.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-end, +.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start { + right: -4px; + right: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); +} +.fc .fc-popover { + position: absolute; + z-index: 9999; + box-shadow: 0 2px 6px rgba(0,0,0,.15); + } +.fc .fc-popover-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 3px 4px; + } +.fc .fc-popover-title { + margin: 0 2px; + } +.fc .fc-popover-close { + cursor: pointer; + opacity: 0.65; + font-size: 1.1em; + } +.fc-theme-standard .fc-popover { + border: 1px solid #ddd; + border: 1px solid var(--fc-border-color, #ddd); + background: #fff; + background: var(--fc-page-bg-color, #fff); + } +.fc-theme-standard .fc-popover-header { + background: rgba(208, 208, 208, 0.3); + background: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); + } + + +:root { + --fc-daygrid-event-dot-width: 8px; +} +/* help things clear margins of inner content */ +.fc-daygrid-day-frame, +.fc-daygrid-day-events, +.fc-daygrid-event-harness { /* for event top/bottom margins */ +} +.fc-daygrid-day-frame:before, .fc-daygrid-day-events:before, .fc-daygrid-event-harness:before { + content: ""; + clear: both; + display: table; } +.fc-daygrid-day-frame:after, .fc-daygrid-day-events:after, .fc-daygrid-event-harness:after { + content: ""; + clear: both; + display: table; } +.fc .fc-daygrid-body { /* a
that wraps the table */ + position: relative; + z-index: 1; /* container inner z-index's because s can't do it */ + } +.fc .fc-daygrid-day.fc-day-today { + background-color: rgba(255, 220, 40, 0.15); + background-color: var(--fc-today-bg-color, rgba(255, 220, 40, 0.15)); + } +.fc .fc-daygrid-day-frame { + position: relative; + min-height: 100%; /* seems to work better than `height` because sets height after rows/cells naturally do it */ + } +.fc { + + /* cell top */ + +} +.fc .fc-daygrid-day-top { + display: flex; + flex-direction: row-reverse; + } +.fc .fc-day-other .fc-daygrid-day-top { + opacity: 0.3; + } +.fc { + + /* day number (within cell top) */ + +} +.fc .fc-daygrid-day-number { + position: relative; + z-index: 4; + padding: 4px; + } +.fc { + + /* event container */ + +} +.fc .fc-daygrid-day-events { + margin-top: 1px; /* needs to be margin, not padding, so that available cell height can be computed */ + } +.fc { + + /* positioning for balanced vs natural */ + +} +.fc .fc-daygrid-body-balanced .fc-daygrid-day-events { + position: absolute; + left: 0; + right: 0; + } +.fc .fc-daygrid-body-unbalanced .fc-daygrid-day-events { + position: relative; /* for containing abs positioned event harnesses */ + min-height: 2em; /* in addition to being a min-height during natural height, equalizes the heights a little bit */ + } +.fc .fc-daygrid-body-natural { /* can coexist with -unbalanced */ + } +.fc .fc-daygrid-body-natural .fc-daygrid-day-events { + margin-bottom: 1em; + } +.fc { + + /* event harness */ + +} +.fc .fc-daygrid-event-harness { + position: relative; + } +.fc .fc-daygrid-event-harness-abs { + position: absolute; + top: 0; /* fallback coords for when cannot yet be computed */ + left: 0; /* */ + right: 0; /* */ + } +.fc .fc-daygrid-bg-harness { + position: absolute; + top: 0; + bottom: 0; + } +.fc { + + /* bg content */ + +} +.fc .fc-daygrid-day-bg .fc-non-business { z-index: 1 } +.fc .fc-daygrid-day-bg .fc-bg-event { z-index: 2 } +.fc .fc-daygrid-day-bg .fc-highlight { z-index: 3 } +.fc { + + /* events */ + +} +.fc .fc-daygrid-event { + z-index: 6; + margin-top: 1px; + } +.fc .fc-daygrid-event.fc-event-mirror { + z-index: 7; + } +.fc { + + /* cell bottom (within day-events) */ + +} +.fc .fc-daygrid-day-bottom { + font-size: .85em; + padding: 2px 3px 0 + } +.fc .fc-daygrid-day-bottom:before { + content: ""; + clear: both; + display: table; } +.fc .fc-daygrid-more-link { + position: relative; + z-index: 4; + cursor: pointer; + } +.fc { + + /* week number (within frame) */ + +} +.fc .fc-daygrid-week-number { + position: absolute; + z-index: 5; + top: 0; + padding: 2px; + min-width: 1.5em; + text-align: center; + background-color: rgba(208, 208, 208, 0.3); + background-color: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); + color: #808080; + color: var(--fc-neutral-text-color, #808080); + } +.fc { + + /* popover */ + +} +.fc .fc-more-popover .fc-popover-body { + min-width: 220px; + padding: 10px; + } +.fc-direction-ltr .fc-daygrid-event.fc-event-start, +.fc-direction-rtl .fc-daygrid-event.fc-event-end { + margin-left: 2px; +} +.fc-direction-ltr .fc-daygrid-event.fc-event-end, +.fc-direction-rtl .fc-daygrid-event.fc-event-start { + margin-right: 2px; +} +.fc-direction-ltr .fc-daygrid-week-number { + left: 0; + border-radius: 0 0 3px 0; + } +.fc-direction-rtl .fc-daygrid-week-number { + right: 0; + border-radius: 0 0 0 3px; + } +.fc-liquid-hack .fc-daygrid-day-frame { + position: static; /* will cause inner absolute stuff to expand to */ + } +.fc-daygrid-event { /* make root-level, because will be dragged-and-dropped outside of a component root */ + position: relative; /* for z-indexes assigned later */ + white-space: nowrap; + border-radius: 3px; /* dot event needs this to when selected */ + font-size: .85em; + font-size: var(--fc-small-font-size, .85em); +} +/* --- the rectangle ("block") style of event --- */ +.fc-daygrid-block-event .fc-event-time { + font-weight: bold; + } +.fc-daygrid-block-event .fc-event-time, + .fc-daygrid-block-event .fc-event-title { + padding: 1px; + } +/* --- the dot style of event --- */ +.fc-daygrid-dot-event { + display: flex; + align-items: center; + padding: 2px 0 + +} +.fc-daygrid-dot-event .fc-event-title { + flex-grow: 1; + flex-shrink: 1; + min-width: 0; /* important for allowing to shrink all the way */ + overflow: hidden; + font-weight: bold; + } +.fc-daygrid-dot-event:hover, + .fc-daygrid-dot-event.fc-event-mirror { + background: rgba(0, 0, 0, 0.1); + } +.fc-daygrid-dot-event.fc-event-selected:before { + /* expand hit area */ + top: -10px; + bottom: -10px; + } +.fc-daygrid-event-dot { /* the actual dot */ + margin: 0 4px; + box-sizing: content-box; + width: 0; + height: 0; + border: 4px solid #3788d8; + border: calc(var(--fc-daygrid-event-dot-width, 8px) / 2) solid var(--fc-event-border-color, #3788d8); + border-radius: 4px; + border-radius: calc(var(--fc-daygrid-event-dot-width, 8px) / 2); +} +/* --- spacing between time and title --- */ +.fc-direction-ltr .fc-daygrid-event .fc-event-time { + margin-right: 3px; + } +.fc-direction-rtl .fc-daygrid-event .fc-event-time { + margin-left: 3px; + } + + +/* +A VERTICAL event +*/ + +.fc-v-event { /* allowed to be top-level */ + display: block; + border: 1px solid #3788d8; + border: 1px solid var(--fc-event-border-color, #3788d8); + background-color: #3788d8; + background-color: var(--fc-event-bg-color, #3788d8) + +} + +.fc-v-event .fc-event-main { + color: #fff; + color: var(--fc-event-text-color, #fff); + height: 100%; + } + +.fc-v-event .fc-event-main-frame { + height: 100%; + display: flex; + flex-direction: column; + } + +.fc-v-event .fc-event-time { + flex-grow: 0; + flex-shrink: 0; + max-height: 100%; + overflow: hidden; + } + +.fc-v-event .fc-event-title-container { /* a container for the sticky cushion */ + flex-grow: 1; + flex-shrink: 1; + min-height: 0; /* important for allowing to shrink all the way */ + } + +.fc-v-event .fc-event-title { /* will have fc-sticky on it */ + top: 0; + bottom: 0; + max-height: 100%; /* clip overflow */ + overflow: hidden; + } + +.fc-v-event:not(.fc-event-start) { + border-top-width: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + +.fc-v-event:not(.fc-event-end) { + border-bottom-width: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + +.fc-v-event.fc-event-selected:before { + /* expand hit area */ + left: -10px; + right: -10px; + } + +.fc-v-event { + + /* resizer (mouse AND touch) */ + +} + +.fc-v-event .fc-event-resizer-start { + cursor: n-resize; + } + +.fc-v-event .fc-event-resizer-end { + cursor: s-resize; + } + +.fc-v-event { + + /* resizer for MOUSE */ + +} + +.fc-v-event:not(.fc-event-selected) .fc-event-resizer { + height: 8px; + height: var(--fc-event-resizer-thickness, 8px); + left: 0; + right: 0; + } + +.fc-v-event:not(.fc-event-selected) .fc-event-resizer-start { + top: -4px; + top: calc(var(--fc-event-resizer-thickness, 8px) / -2); + } + +.fc-v-event:not(.fc-event-selected) .fc-event-resizer-end { + bottom: -4px; + bottom: calc(var(--fc-event-resizer-thickness, 8px) / -2); + } + +.fc-v-event { + + /* resizer for TOUCH (when event is "selected") */ + +} + +.fc-v-event.fc-event-selected .fc-event-resizer { + left: 50%; + margin-left: -4px; + margin-left: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); + } + +.fc-v-event.fc-event-selected .fc-event-resizer-start { + top: -4px; + top: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); + } + +.fc-v-event.fc-event-selected .fc-event-resizer-end { + bottom: -4px; + bottom: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); + } +.fc .fc-timegrid .fc-daygrid-body { /* the all-day daygrid within the timegrid view */ + z-index: 2; /* put above the timegrid-body so that more-popover is above everything. TODO: better solution */ + } +.fc .fc-timegrid-divider { + padding: 0 0 2px; /* browsers get confused when you set height. use padding instead */ + } +.fc .fc-timegrid-body { + position: relative; + z-index: 1; /* scope the z-indexes of slots and cols */ + min-height: 100%; /* fill height always, even when slat table doesn't grow */ + } +.fc .fc-timegrid-axis-chunk { /* for advanced ScrollGrid */ + position: relative /* offset parent for now-indicator-container */ + + } +.fc .fc-timegrid-axis-chunk > table { + position: relative; + z-index: 1; /* above the now-indicator-container */ + } +.fc .fc-timegrid-slots { + position: relative; + z-index: 1; + } +.fc .fc-timegrid-slot { /* a */ + height: 1.5em; + border-bottom: 0 /* each cell owns its top border */ + } +.fc .fc-timegrid-slot:empty:before { + content: '\00a0'; /* make sure there's at least an empty space to create height for height syncing */ + } +.fc .fc-timegrid-slot-minor { + border-top-style: dotted; + } +.fc .fc-timegrid-slot-label-cushion { + display: inline-block; + white-space: nowrap; + } +.fc .fc-timegrid-slot-label { + vertical-align: middle; /* vertical align the slots */ + } +.fc { + + + /* slots AND axis cells (top-left corner of view including the "all-day" text) */ + +} +.fc .fc-timegrid-axis-cushion, + .fc .fc-timegrid-slot-label-cushion { + padding: 0 4px; + } +.fc { + + + /* axis cells (top-left corner of view including the "all-day" text) */ + /* vertical align is more complicated, uses flexbox */ + +} +.fc .fc-timegrid-axis-frame-liquid { + height: 100%; /* will need liquid-hack in FF */ + } +.fc .fc-timegrid-axis-frame { + overflow: hidden; + display: flex; + align-items: center; /* vertical align */ + justify-content: flex-end; /* horizontal align. matches text-align below */ + } +.fc .fc-timegrid-axis-cushion { + max-width: 60px; /* limits the width of the "all-day" text */ + flex-shrink: 0; /* allows text to expand how it normally would, regardless of constrained width */ + } +.fc-direction-ltr .fc-timegrid-slot-label-frame { + text-align: right; + } +.fc-direction-rtl .fc-timegrid-slot-label-frame { + text-align: left; + } +.fc-liquid-hack .fc-timegrid-axis-frame-liquid { + height: auto; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } +.fc .fc-timegrid-col.fc-day-today { + background-color: rgba(255, 220, 40, 0.15); + background-color: var(--fc-today-bg-color, rgba(255, 220, 40, 0.15)); + } +.fc .fc-timegrid-col-frame { + min-height: 100%; /* liquid-hack is below */ + position: relative; + } +.fc-media-screen.fc-liquid-hack .fc-timegrid-col-frame { + height: auto; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } +.fc-media-screen .fc-timegrid-cols { + position: absolute; /* no z-index. children will decide and go above slots */ + top: 0; + left: 0; + right: 0; + bottom: 0 + } +.fc-media-screen .fc-timegrid-cols > table { + height: 100%; + } +.fc-media-screen .fc-timegrid-col-bg, + .fc-media-screen .fc-timegrid-col-events, + .fc-media-screen .fc-timegrid-now-indicator-container { + position: absolute; + top: 0; + left: 0; + right: 0; + } +.fc { + + /* bg */ + +} +.fc .fc-timegrid-col-bg { + z-index: 2; /* TODO: kill */ + } +.fc .fc-timegrid-col-bg .fc-non-business { z-index: 1 } +.fc .fc-timegrid-col-bg .fc-bg-event { z-index: 2 } +.fc .fc-timegrid-col-bg .fc-highlight { z-index: 3 } +.fc .fc-timegrid-bg-harness { + position: absolute; /* top/bottom will be set by JS */ + left: 0; + right: 0; + } +.fc { + + /* fg events */ + /* (the mirror segs are put into a separate container with same classname, */ + /* and they must be after the normal seg container to appear at a higher z-index) */ + +} +.fc .fc-timegrid-col-events { + z-index: 3; + /* child event segs have z-indexes that are scoped within this div */ + } +.fc { + + /* now indicator */ + +} +.fc .fc-timegrid-now-indicator-container { + bottom: 0; + overflow: hidden; /* don't let overflow of lines/arrows cause unnecessary scrolling */ + /* z-index is set on the individual elements */ + } +.fc-direction-ltr .fc-timegrid-col-events { + margin: 0 2.5% 0 2px; + } +.fc-direction-rtl .fc-timegrid-col-events { + margin: 0 2px 0 2.5%; + } +.fc-timegrid-event-harness { + position: absolute /* top/left/right/bottom will all be set by JS */ +} +.fc-timegrid-event-harness > .fc-timegrid-event { + position: absolute; /* absolute WITHIN the harness */ + top: 0; /* for when not yet positioned */ + bottom: 0; /* " */ + left: 0; + right: 0; + } +.fc-timegrid-event-harness-inset .fc-timegrid-event, +.fc-timegrid-event.fc-event-mirror, +.fc-timegrid-more-link { + box-shadow: 0px 0px 0px 1px #fff; + box-shadow: 0px 0px 0px 1px var(--fc-page-bg-color, #fff); +} +.fc-timegrid-event, +.fc-timegrid-more-link { /* events need to be root */ + font-size: .85em; + font-size: var(--fc-small-font-size, .85em); + border-radius: 3px; +} +.fc-timegrid-event { /* events need to be root */ + margin-bottom: 1px /* give some space from bottom */ +} +.fc-timegrid-event .fc-event-main { + padding: 1px 1px 0; + } +.fc-timegrid-event .fc-event-time { + white-space: nowrap; + font-size: .85em; + font-size: var(--fc-small-font-size, .85em); + margin-bottom: 1px; + } +.fc-timegrid-event-short .fc-event-main-frame { + flex-direction: row; + overflow: hidden; + } +.fc-timegrid-event-short .fc-event-time:after { + content: '\00a0-\00a0'; /* dash surrounded by non-breaking spaces */ + } +.fc-timegrid-event-short .fc-event-title { + font-size: .85em; + font-size: var(--fc-small-font-size, .85em) + } +.fc-timegrid-more-link { /* does NOT inherit from fc-timegrid-event */ + position: absolute; + z-index: 9999; /* hack */ + color: inherit; + color: var(--fc-more-link-text-color, inherit); + background: #d0d0d0; + background: var(--fc-more-link-bg-color, #d0d0d0); + cursor: pointer; + margin-bottom: 1px; /* match space below fc-timegrid-event */ +} +.fc-timegrid-more-link-inner { /* has fc-sticky */ + padding: 3px 2px; + top: 0; +} +.fc-direction-ltr .fc-timegrid-more-link { + right: 0; + } +.fc-direction-rtl .fc-timegrid-more-link { + left: 0; + } +.fc { + + /* line */ + +} +.fc .fc-timegrid-now-indicator-line { + position: absolute; + z-index: 4; + left: 0; + right: 0; + border-style: solid; + border-color: red; + border-color: var(--fc-now-indicator-color, red); + border-width: 1px 0 0; + } +.fc { + + /* arrow */ + +} +.fc .fc-timegrid-now-indicator-arrow { + position: absolute; + z-index: 4; + margin-top: -5px; /* vertically center on top coordinate */ + border-style: solid; + border-color: red; + border-color: var(--fc-now-indicator-color, red); + } +.fc-direction-ltr .fc-timegrid-now-indicator-arrow { + left: 0; + + /* triangle pointing right. TODO: mixin */ + border-width: 5px 0 5px 6px; + border-top-color: transparent; + border-bottom-color: transparent; + } +.fc-direction-rtl .fc-timegrid-now-indicator-arrow { + right: 0; + + /* triangle pointing left. TODO: mixin */ + border-width: 5px 6px 5px 0; + border-top-color: transparent; + border-bottom-color: transparent; + } + + +:root { + --fc-list-event-dot-width: 10px; + --fc-list-event-hover-bg-color: #f5f5f5; +} +.fc-theme-standard .fc-list { + border: 1px solid #ddd; + border: 1px solid var(--fc-border-color, #ddd); + } +.fc { + + /* message when no events */ + +} +.fc .fc-list-empty { + background-color: rgba(208, 208, 208, 0.3); + background-color: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); + height: 100%; + display: flex; + justify-content: center; + align-items: center; /* vertically aligns fc-list-empty-inner */ + } +.fc .fc-list-empty-cushion { + margin: 5em 0; + } +.fc { + + /* table within the scroller */ + /* ---------------------------------------------------------------------------------------------------- */ + +} +.fc .fc-list-table { + width: 100%; + border-style: hidden; /* kill outer border on theme */ + } +.fc .fc-list-table tr > * { + border-left: 0; + border-right: 0; + } +.fc .fc-list-sticky .fc-list-day > * { /* the cells */ + position: sticky; + top: 0; + background: #fff; + background: var(--fc-page-bg-color, #fff); /* for when headers are styled to be transparent and sticky */ + } +.fc .fc-list-table th { + padding: 0; /* uses an inner-wrapper instead... */ + } +.fc .fc-list-table td, + .fc .fc-list-day-cushion { + padding: 8px 14px; + } +.fc { + + + /* date heading rows */ + /* ---------------------------------------------------------------------------------------------------- */ + +} +.fc .fc-list-day-cushion:after { + content: ""; + clear: both; + display: table; /* clear floating */ + } +.fc-theme-standard .fc-list-day-cushion { + background-color: rgba(208, 208, 208, 0.3); + background-color: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); + } +.fc-direction-ltr .fc-list-day-text, +.fc-direction-rtl .fc-list-day-side-text { + float: left; +} +.fc-direction-ltr .fc-list-day-side-text, +.fc-direction-rtl .fc-list-day-text { + float: right; +} +/* make the dot closer to the event title */ +.fc-direction-ltr .fc-list-table .fc-list-event-graphic { padding-right: 0 } +.fc-direction-rtl .fc-list-table .fc-list-event-graphic { padding-left: 0 } +.fc .fc-list-event.fc-event-forced-url { + cursor: pointer; /* whole row will seem clickable */ + } +.fc .fc-list-event:hover td { + background-color: #f5f5f5; + background-color: var(--fc-list-event-hover-bg-color, #f5f5f5); + } +.fc { + + /* shrink certain cols */ + +} +.fc .fc-list-event-graphic, + .fc .fc-list-event-time { + white-space: nowrap; + width: 1px; + } +.fc .fc-list-event-dot { + display: inline-block; + box-sizing: content-box; + width: 0; + height: 0; + border: 5px solid #3788d8; + border: calc(var(--fc-list-event-dot-width, 10px) / 2) solid var(--fc-event-border-color, #3788d8); + border-radius: 5px; + border-radius: calc(var(--fc-list-event-dot-width, 10px) / 2); + } +.fc { + + /* reset styling */ + +} +.fc .fc-list-event-title a { + color: inherit; + text-decoration: none; + } +.fc { + + /* underline link when hovering over any part of row */ + +} +.fc .fc-list-event.fc-event-forced-url:hover a { + text-decoration: underline; + } + + + + .fc-theme-bootstrap a:not([href]) { + color: inherit; /* natural color for navlinks */ + } + diff --git a/apps/schoolCalendar/fullcalendar/main.js b/apps/schoolCalendar/fullcalendar/main.js new file mode 100644 index 000000000..54bf45d3f --- /dev/null +++ b/apps/schoolCalendar/fullcalendar/main.js @@ -0,0 +1,14738 @@ +/*! +FullCalendar v5.9.0 +Docs & License: https://fullcalendar.io/ +(c) 2021 Adam Shaw +*/ +var FullCalendar = (function (exports) { + 'use strict'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + /* global Reflect, Promise */ + + var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + + function __extends(d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + } + + var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + + function __spreadArray(to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || from); + } + + var n,u,i$1,t,o,r$1={},f$1=[],e$1=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function c$1(n,l){for(var u in l)n[u]=l[u];return n}function s(n){var l=n.parentNode;l&&l.removeChild(n);}function a$1(n,l,u){var i,t,o,r=arguments,f={};for(o in l)"key"==o?i=l[o]:"ref"==o?t=l[o]:f[o]=l[o];if(arguments.length>3)for(u=[u],o=3;o0?v$1(k.type,k.props,k.key,null,k.__v):k)){if(k.__=u,k.__b=u.__b+1,null===(_=A[h])||_&&k.key==_.key&&k.type===_.type)A[h]=void 0;else for(p=0;p3;)e.pop()();if(e[1]>>1,1),t.i.removeChild(n);}}),N(a$1(T,{context:t.context},n.__v),t.l)):t.l&&t.componentWillUnmount();}function I(n,t){return a$1(j,{__v:n,i:t})}(F.prototype=new p).__e=function(n){var t=this,e=U(t.__v),r=t.o.get(n);return r[0]++,function(u){var o=function(){t.props.revealOrder?(r.push(u),M(t,n,r)):u();};e?e(o):o();}},F.prototype.render=function(n){this.u=null,this.o=new Map;var t=w$1(n.children);n.revealOrder&&"b"===n.revealOrder[0]&&t.reverse();for(var e=t.length;e--;)this.o.set(t[e],this.u=[1,0,this.u]);return n.children},F.prototype.componentDidUpdate=F.prototype.componentDidMount=function(){var n=this;this.o.forEach(function(t,e){M(n,e,t);});};var W="undefined"!=typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103,P=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,V=function(n){return ("undefined"!=typeof Symbol&&"symbol"==typeof Symbol()?/fil|che|rad/i:/fil|che|ra/i).test(n)};p.prototype.isReactComponent={},["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(n){Object.defineProperty(p.prototype,n,{configurable:!0,get:function(){return this["UNSAFE_"+n]},set:function(t){Object.defineProperty(this,n,{configurable:!0,writable:!0,value:t});}});});var H=n.event;function Z(){}function Y(){return this.cancelBubble}function $(){return this.defaultPrevented}n.event=function(n){return H&&(n=H(n)),n.persist=Z,n.isPropagationStopped=Y,n.isDefaultPrevented=$,n.nativeEvent=n};var G={configurable:!0,get:function(){return this.class}},J=n.vnode;n.vnode=function(n){var t=n.type,e=n.props,r=e;if("string"==typeof t){for(var u in r={},e){var o=e[u];"value"===u&&"defaultValue"in e&&null==o||("defaultValue"===u&&"value"in e&&null==e.value?u="value":"download"===u&&!0===o?o="":/ondoubleclick/i.test(u)?u="ondblclick":/^onchange(textarea|input)/i.test(u+t)&&!V(e.type)?u="oninput":/^on(Ani|Tra|Tou|BeforeInp)/.test(u)?u=u.toLowerCase():P.test(u)?u=u.replace(/[A-Z0-9]/,"-$&").toLowerCase():null===o&&(o=void 0),r[u]=o);}"select"==t&&r.multiple&&Array.isArray(r.value)&&(r.value=w$1(e.children).forEach(function(n){n.props.selected=-1!=r.value.indexOf(n.props.value);})),"select"==t&&null!=r.defaultValue&&(r.value=w$1(e.children).forEach(function(n){n.props.selected=r.multiple?-1!=r.defaultValue.indexOf(n.props.value):r.defaultValue==n.props.value;})),n.props=r;}t&&e.class!=e.className&&(G.enumerable="className"in e,null!=e.className&&(r.class=e.className),Object.defineProperty(r,"className",G)),n.$$typeof=W,J&&J(n);};var K=n.__r;n.__r=function(n){K&&K(n);};"object"==typeof performance&&"function"==typeof performance.now?performance.now.bind(performance):function(){return Date.now()}; + + var globalObj = typeof globalThis !== 'undefined' ? globalThis : window; // // TODO: streamline when killing IE11 support + if (globalObj.FullCalendarVDom) { + console.warn('FullCalendar VDOM already loaded'); + } + else { + globalObj.FullCalendarVDom = { + Component: p, + createElement: a$1, + render: N, + createRef: h, + Fragment: y, + createContext: createContext$1, + createPortal: I, + flushToDom: flushToDom$1, + unmountComponentAtNode: unmountComponentAtNode$1, + }; + } + // HACKS... + // TODO: lock version + // TODO: link gh issues + function flushToDom$1() { + var oldDebounceRendering = n.debounceRendering; // orig + var callbackQ = []; + function execCallbackSync(callback) { + callbackQ.push(callback); + } + n.debounceRendering = execCallbackSync; + N(a$1(FakeComponent, {}), document.createElement('div')); + while (callbackQ.length) { + callbackQ.shift()(); + } + n.debounceRendering = oldDebounceRendering; + } + var FakeComponent = /** @class */ (function (_super) { + __extends(FakeComponent, _super); + function FakeComponent() { + return _super !== null && _super.apply(this, arguments) || this; + } + FakeComponent.prototype.render = function () { return a$1('div', {}); }; + FakeComponent.prototype.componentDidMount = function () { this.setState({}); }; + return FakeComponent; + }(p)); + function createContext$1(defaultValue) { + var ContextType = q(defaultValue); + var origProvider = ContextType.Provider; + ContextType.Provider = function () { + var _this = this; + var isNew = !this.getChildContext; + var children = origProvider.apply(this, arguments); // eslint-disable-line prefer-rest-params + if (isNew) { + var subs_1 = []; + this.shouldComponentUpdate = function (_props) { + if (_this.props.value !== _props.value) { + subs_1.forEach(function (c) { + c.context = _props.value; + c.forceUpdate(); + }); + } + }; + this.sub = function (c) { + subs_1.push(c); + var old = c.componentWillUnmount; + c.componentWillUnmount = function () { + subs_1.splice(subs_1.indexOf(c), 1); + old && old.call(c); + }; + }; + } + return children; + }; + return ContextType; + } + function unmountComponentAtNode$1(node) { + N(null, node); + } + + // no public types yet. when there are, export from: + // import {} from './api-type-deps' + var EventSourceApi = /** @class */ (function () { + function EventSourceApi(context, internalEventSource) { + this.context = context; + this.internalEventSource = internalEventSource; + } + EventSourceApi.prototype.remove = function () { + this.context.dispatch({ + type: 'REMOVE_EVENT_SOURCE', + sourceId: this.internalEventSource.sourceId, + }); + }; + EventSourceApi.prototype.refetch = function () { + this.context.dispatch({ + type: 'FETCH_EVENT_SOURCES', + sourceIds: [this.internalEventSource.sourceId], + isRefetch: true, + }); + }; + Object.defineProperty(EventSourceApi.prototype, "id", { + get: function () { + return this.internalEventSource.publicId; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventSourceApi.prototype, "url", { + get: function () { + return this.internalEventSource.meta.url; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventSourceApi.prototype, "format", { + get: function () { + return this.internalEventSource.meta.format; // TODO: bad. not guaranteed + }, + enumerable: false, + configurable: true + }); + return EventSourceApi; + }()); + + function removeElement(el) { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + } + // Querying + // ---------------------------------------------------------------------------------------------------------------- + function elementClosest(el, selector) { + if (el.closest) { + return el.closest(selector); + // really bad fallback for IE + // from https://developer.mozilla.org/en-US/docs/Web/API/Element/closest + } + if (!document.documentElement.contains(el)) { + return null; + } + do { + if (elementMatches(el, selector)) { + return el; + } + el = (el.parentElement || el.parentNode); + } while (el !== null && el.nodeType === 1); + return null; + } + function elementMatches(el, selector) { + var method = el.matches || el.matchesSelector || el.msMatchesSelector; + return method.call(el, selector); + } + // accepts multiple subject els + // returns a real array. good for methods like forEach + // TODO: accept the document + function findElements(container, selector) { + var containers = container instanceof HTMLElement ? [container] : container; + var allMatches = []; + for (var i = 0; i < containers.length; i += 1) { + var matches = containers[i].querySelectorAll(selector); + for (var j = 0; j < matches.length; j += 1) { + allMatches.push(matches[j]); + } + } + return allMatches; + } + // accepts multiple subject els + // only queries direct child elements // TODO: rename to findDirectChildren! + function findDirectChildren(parent, selector) { + var parents = parent instanceof HTMLElement ? [parent] : parent; + var allMatches = []; + for (var i = 0; i < parents.length; i += 1) { + var childNodes = parents[i].children; // only ever elements + for (var j = 0; j < childNodes.length; j += 1) { + var childNode = childNodes[j]; + if (!selector || elementMatches(childNode, selector)) { + allMatches.push(childNode); + } + } + } + return allMatches; + } + // Style + // ---------------------------------------------------------------------------------------------------------------- + var PIXEL_PROP_RE = /(top|left|right|bottom|width|height)$/i; + function applyStyle(el, props) { + for (var propName in props) { + applyStyleProp(el, propName, props[propName]); + } + } + function applyStyleProp(el, name, val) { + if (val == null) { + el.style[name] = ''; + } + else if (typeof val === 'number' && PIXEL_PROP_RE.test(name)) { + el.style[name] = val + "px"; + } + else { + el.style[name] = val; + } + } + // Event Handling + // ---------------------------------------------------------------------------------------------------------------- + // if intercepting bubbled events at the document/window/body level, + // and want to see originating element (the 'target'), use this util instead + // of `ev.target` because it goes within web-component boundaries. + function getEventTargetViaRoot(ev) { + var _a, _b; + return (_b = (_a = ev.composedPath) === null || _a === void 0 ? void 0 : _a.call(ev)[0]) !== null && _b !== void 0 ? _b : ev.target; + } + // Shadow DOM consuderations + // ---------------------------------------------------------------------------------------------------------------- + function getElRoot(el) { + return el.getRootNode ? el.getRootNode() : document; + } + + // Stops a mouse/touch event from doing it's native browser action + function preventDefault(ev) { + ev.preventDefault(); + } + // Event Delegation + // ---------------------------------------------------------------------------------------------------------------- + function buildDelegationHandler(selector, handler) { + return function (ev) { + var matchedChild = elementClosest(ev.target, selector); + if (matchedChild) { + handler.call(matchedChild, ev, matchedChild); + } + }; + } + function listenBySelector(container, eventType, selector, handler) { + var attachedHandler = buildDelegationHandler(selector, handler); + container.addEventListener(eventType, attachedHandler); + return function () { + container.removeEventListener(eventType, attachedHandler); + }; + } + function listenToHoverBySelector(container, selector, onMouseEnter, onMouseLeave) { + var currentMatchedChild; + return listenBySelector(container, 'mouseover', selector, function (mouseOverEv, matchedChild) { + if (matchedChild !== currentMatchedChild) { + currentMatchedChild = matchedChild; + onMouseEnter(mouseOverEv, matchedChild); + var realOnMouseLeave_1 = function (mouseLeaveEv) { + currentMatchedChild = null; + onMouseLeave(mouseLeaveEv, matchedChild); + matchedChild.removeEventListener('mouseleave', realOnMouseLeave_1); + }; + // listen to the next mouseleave, and then unattach + matchedChild.addEventListener('mouseleave', realOnMouseLeave_1); + } + }); + } + // Animation + // ---------------------------------------------------------------------------------------------------------------- + var transitionEventNames = [ + 'webkitTransitionEnd', + 'otransitionend', + 'oTransitionEnd', + 'msTransitionEnd', + 'transitionend', + ]; + // triggered only when the next single subsequent transition finishes + function whenTransitionDone(el, callback) { + var realCallback = function (ev) { + callback(ev); + transitionEventNames.forEach(function (eventName) { + el.removeEventListener(eventName, realCallback); + }); + }; + transitionEventNames.forEach(function (eventName) { + el.addEventListener(eventName, realCallback); // cross-browser way to determine when the transition finishes + }); + } + + var guidNumber = 0; + function guid() { + guidNumber += 1; + return String(guidNumber); + } + /* FullCalendar-specific DOM Utilities + ----------------------------------------------------------------------------------------------------------------------*/ + // Make the mouse cursor express that an event is not allowed in the current area + function disableCursor() { + document.body.classList.add('fc-not-allowed'); + } + // Returns the mouse cursor to its original look + function enableCursor() { + document.body.classList.remove('fc-not-allowed'); + } + /* Selection + ----------------------------------------------------------------------------------------------------------------------*/ + function preventSelection(el) { + el.classList.add('fc-unselectable'); + el.addEventListener('selectstart', preventDefault); + } + function allowSelection(el) { + el.classList.remove('fc-unselectable'); + el.removeEventListener('selectstart', preventDefault); + } + /* Context Menu + ----------------------------------------------------------------------------------------------------------------------*/ + function preventContextMenu(el) { + el.addEventListener('contextmenu', preventDefault); + } + function allowContextMenu(el) { + el.removeEventListener('contextmenu', preventDefault); + } + function parseFieldSpecs(input) { + var specs = []; + var tokens = []; + var i; + var token; + if (typeof input === 'string') { + tokens = input.split(/\s*,\s*/); + } + else if (typeof input === 'function') { + tokens = [input]; + } + else if (Array.isArray(input)) { + tokens = input; + } + for (i = 0; i < tokens.length; i += 1) { + token = tokens[i]; + if (typeof token === 'string') { + specs.push(token.charAt(0) === '-' ? + { field: token.substring(1), order: -1 } : + { field: token, order: 1 }); + } + else if (typeof token === 'function') { + specs.push({ func: token }); + } + } + return specs; + } + function compareByFieldSpecs(obj0, obj1, fieldSpecs) { + var i; + var cmp; + for (i = 0; i < fieldSpecs.length; i += 1) { + cmp = compareByFieldSpec(obj0, obj1, fieldSpecs[i]); + if (cmp) { + return cmp; + } + } + return 0; + } + function compareByFieldSpec(obj0, obj1, fieldSpec) { + if (fieldSpec.func) { + return fieldSpec.func(obj0, obj1); + } + return flexibleCompare(obj0[fieldSpec.field], obj1[fieldSpec.field]) + * (fieldSpec.order || 1); + } + function flexibleCompare(a, b) { + if (!a && !b) { + return 0; + } + if (b == null) { + return -1; + } + if (a == null) { + return 1; + } + if (typeof a === 'string' || typeof b === 'string') { + return String(a).localeCompare(String(b)); + } + return a - b; + } + /* String Utilities + ----------------------------------------------------------------------------------------------------------------------*/ + function padStart(val, len) { + var s = String(val); + return '000'.substr(0, len - s.length) + s; + } + /* Number Utilities + ----------------------------------------------------------------------------------------------------------------------*/ + function compareNumbers(a, b) { + return a - b; + } + function isInt(n) { + return n % 1 === 0; + } + /* FC-specific DOM dimension stuff + ----------------------------------------------------------------------------------------------------------------------*/ + function computeSmallestCellWidth(cellEl) { + var allWidthEl = cellEl.querySelector('.fc-scrollgrid-shrink-frame'); + var contentWidthEl = cellEl.querySelector('.fc-scrollgrid-shrink-cushion'); + if (!allWidthEl) { + throw new Error('needs fc-scrollgrid-shrink-frame className'); // TODO: use const + } + if (!contentWidthEl) { + throw new Error('needs fc-scrollgrid-shrink-cushion className'); + } + return cellEl.getBoundingClientRect().width - allWidthEl.getBoundingClientRect().width + // the cell padding+border + contentWidthEl.getBoundingClientRect().width; + } + + var DAY_IDS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + // Adding + function addWeeks(m, n) { + var a = dateToUtcArray(m); + a[2] += n * 7; + return arrayToUtcDate(a); + } + function addDays(m, n) { + var a = dateToUtcArray(m); + a[2] += n; + return arrayToUtcDate(a); + } + function addMs(m, n) { + var a = dateToUtcArray(m); + a[6] += n; + return arrayToUtcDate(a); + } + // Diffing (all return floats) + // TODO: why not use ranges? + function diffWeeks(m0, m1) { + return diffDays(m0, m1) / 7; + } + function diffDays(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60 * 24); + } + function diffHours(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60); + } + function diffMinutes(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / (1000 * 60); + } + function diffSeconds(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / 1000; + } + function diffDayAndTime(m0, m1) { + var m0day = startOfDay(m0); + var m1day = startOfDay(m1); + return { + years: 0, + months: 0, + days: Math.round(diffDays(m0day, m1day)), + milliseconds: (m1.valueOf() - m1day.valueOf()) - (m0.valueOf() - m0day.valueOf()), + }; + } + // Diffing Whole Units + function diffWholeWeeks(m0, m1) { + var d = diffWholeDays(m0, m1); + if (d !== null && d % 7 === 0) { + return d / 7; + } + return null; + } + function diffWholeDays(m0, m1) { + if (timeAsMs(m0) === timeAsMs(m1)) { + return Math.round(diffDays(m0, m1)); + } + return null; + } + // Start-Of + function startOfDay(m) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + ]); + } + function startOfHour(m) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + m.getUTCHours(), + ]); + } + function startOfMinute(m) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + m.getUTCHours(), + m.getUTCMinutes(), + ]); + } + function startOfSecond(m) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + m.getUTCHours(), + m.getUTCMinutes(), + m.getUTCSeconds(), + ]); + } + // Week Computation + function weekOfYear(marker, dow, doy) { + var y = marker.getUTCFullYear(); + var w = weekOfGivenYear(marker, y, dow, doy); + if (w < 1) { + return weekOfGivenYear(marker, y - 1, dow, doy); + } + var nextW = weekOfGivenYear(marker, y + 1, dow, doy); + if (nextW >= 1) { + return Math.min(w, nextW); + } + return w; + } + function weekOfGivenYear(marker, year, dow, doy) { + var firstWeekStart = arrayToUtcDate([year, 0, 1 + firstWeekOffset(year, dow, doy)]); + var dayStart = startOfDay(marker); + var days = Math.round(diffDays(firstWeekStart, dayStart)); + return Math.floor(days / 7) + 1; // zero-indexed + } + // start-of-first-week - start-of-year + function firstWeekOffset(year, dow, doy) { + // first-week day -- which january is always in the first week (4 for iso, 1 for other) + var fwd = 7 + dow - doy; + // first-week day local weekday -- which local weekday is fwd + var fwdlw = (7 + arrayToUtcDate([year, 0, fwd]).getUTCDay() - dow) % 7; + return -fwdlw + fwd - 1; + } + // Array Conversion + function dateToLocalArray(date) { + return [ + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + date.getMilliseconds(), + ]; + } + function arrayToLocalDate(a) { + return new Date(a[0], a[1] || 0, a[2] == null ? 1 : a[2], // day of month + a[3] || 0, a[4] || 0, a[5] || 0); + } + function dateToUtcArray(date) { + return [ + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds(), + ]; + } + function arrayToUtcDate(a) { + // according to web standards (and Safari), a month index is required. + // massage if only given a year. + if (a.length === 1) { + a = a.concat([0]); + } + return new Date(Date.UTC.apply(Date, a)); + } + // Other Utils + function isValidDate(m) { + return !isNaN(m.valueOf()); + } + function timeAsMs(m) { + return m.getUTCHours() * 1000 * 60 * 60 + + m.getUTCMinutes() * 1000 * 60 + + m.getUTCSeconds() * 1000 + + m.getUTCMilliseconds(); + } + + function createEventInstance(defId, range, forcedStartTzo, forcedEndTzo) { + return { + instanceId: guid(), + defId: defId, + range: range, + forcedStartTzo: forcedStartTzo == null ? null : forcedStartTzo, + forcedEndTzo: forcedEndTzo == null ? null : forcedEndTzo, + }; + } + + var hasOwnProperty = Object.prototype.hasOwnProperty; + // Merges an array of objects into a single object. + // The second argument allows for an array of property names who's object values will be merged together. + function mergeProps(propObjs, complexPropsMap) { + var dest = {}; + if (complexPropsMap) { + for (var name_1 in complexPropsMap) { + var complexObjs = []; + // collect the trailing object values, stopping when a non-object is discovered + for (var i = propObjs.length - 1; i >= 0; i -= 1) { + var val = propObjs[i][name_1]; + if (typeof val === 'object' && val) { // non-null object + complexObjs.unshift(val); + } + else if (val !== undefined) { + dest[name_1] = val; // if there were no objects, this value will be used + break; + } + } + // if the trailing values were objects, use the merged value + if (complexObjs.length) { + dest[name_1] = mergeProps(complexObjs); + } + } + } + // copy values into the destination, going from last to first + for (var i = propObjs.length - 1; i >= 0; i -= 1) { + var props = propObjs[i]; + for (var name_2 in props) { + if (!(name_2 in dest)) { // if already assigned by previous props or complex props, don't reassign + dest[name_2] = props[name_2]; + } + } + } + return dest; + } + function filterHash(hash, func) { + var filtered = {}; + for (var key in hash) { + if (func(hash[key], key)) { + filtered[key] = hash[key]; + } + } + return filtered; + } + function mapHash(hash, func) { + var newHash = {}; + for (var key in hash) { + newHash[key] = func(hash[key], key); + } + return newHash; + } + function arrayToHash(a) { + var hash = {}; + for (var _i = 0, a_1 = a; _i < a_1.length; _i++) { + var item = a_1[_i]; + hash[item] = true; + } + return hash; + } + function buildHashFromArray(a, func) { + var hash = {}; + for (var i = 0; i < a.length; i += 1) { + var tuple = func(a[i], i); + hash[tuple[0]] = tuple[1]; + } + return hash; + } + function hashValuesToArray(obj) { + var a = []; + for (var key in obj) { + a.push(obj[key]); + } + return a; + } + function isPropsEqual(obj0, obj1) { + if (obj0 === obj1) { + return true; + } + for (var key in obj0) { + if (hasOwnProperty.call(obj0, key)) { + if (!(key in obj1)) { + return false; + } + } + } + for (var key in obj1) { + if (hasOwnProperty.call(obj1, key)) { + if (obj0[key] !== obj1[key]) { + return false; + } + } + } + return true; + } + function getUnequalProps(obj0, obj1) { + var keys = []; + for (var key in obj0) { + if (hasOwnProperty.call(obj0, key)) { + if (!(key in obj1)) { + keys.push(key); + } + } + } + for (var key in obj1) { + if (hasOwnProperty.call(obj1, key)) { + if (obj0[key] !== obj1[key]) { + keys.push(key); + } + } + } + return keys; + } + function compareObjs(oldProps, newProps, equalityFuncs) { + if (equalityFuncs === void 0) { equalityFuncs = {}; } + if (oldProps === newProps) { + return true; + } + for (var key in newProps) { + if (key in oldProps && isObjValsEqual(oldProps[key], newProps[key], equalityFuncs[key])) ; + else { + return false; + } + } + // check for props that were omitted in the new + for (var key in oldProps) { + if (!(key in newProps)) { + return false; + } + } + return true; + } + /* + assumed "true" equality for handler names like "onReceiveSomething" + */ + function isObjValsEqual(val0, val1, comparator) { + if (val0 === val1 || comparator === true) { + return true; + } + if (comparator) { + return comparator(val0, val1); + } + return false; + } + function collectFromHash(hash, startIndex, endIndex, step) { + if (startIndex === void 0) { startIndex = 0; } + if (step === void 0) { step = 1; } + var res = []; + if (endIndex == null) { + endIndex = Object.keys(hash).length; + } + for (var i = startIndex; i < endIndex; i += step) { + var val = hash[i]; + if (val !== undefined) { // will disregard undefined for sparse arrays + res.push(val); + } + } + return res; + } + + function parseRecurring(refined, defaultAllDay, dateEnv, recurringTypes) { + for (var i = 0; i < recurringTypes.length; i += 1) { + var parsed = recurringTypes[i].parse(refined, dateEnv); + if (parsed) { + var allDay = refined.allDay; + if (allDay == null) { + allDay = defaultAllDay; + if (allDay == null) { + allDay = parsed.allDayGuess; + if (allDay == null) { + allDay = false; + } + } + } + return { + allDay: allDay, + duration: parsed.duration, + typeData: parsed.typeData, + typeId: i, + }; + } + } + return null; + } + function expandRecurring(eventStore, framingRange, context) { + var dateEnv = context.dateEnv, pluginHooks = context.pluginHooks, options = context.options; + var defs = eventStore.defs, instances = eventStore.instances; + // remove existing recurring instances + // TODO: bad. always expand events as a second step + instances = filterHash(instances, function (instance) { return !defs[instance.defId].recurringDef; }); + for (var defId in defs) { + var def = defs[defId]; + if (def.recurringDef) { + var duration = def.recurringDef.duration; + if (!duration) { + duration = def.allDay ? + options.defaultAllDayEventDuration : + options.defaultTimedEventDuration; + } + var starts = expandRecurringRanges(def, duration, framingRange, dateEnv, pluginHooks.recurringTypes); + for (var _i = 0, starts_1 = starts; _i < starts_1.length; _i++) { + var start = starts_1[_i]; + var instance = createEventInstance(defId, { + start: start, + end: dateEnv.add(start, duration), + }); + instances[instance.instanceId] = instance; + } + } + } + return { defs: defs, instances: instances }; + } + /* + Event MUST have a recurringDef + */ + function expandRecurringRanges(eventDef, duration, framingRange, dateEnv, recurringTypes) { + var typeDef = recurringTypes[eventDef.recurringDef.typeId]; + var markers = typeDef.expand(eventDef.recurringDef.typeData, { + start: dateEnv.subtract(framingRange.start, duration), + end: framingRange.end, + }, dateEnv); + // the recurrence plugins don't guarantee that all-day events are start-of-day, so we have to + if (eventDef.allDay) { + markers = markers.map(startOfDay); + } + return markers; + } + + var INTERNAL_UNITS = ['years', 'months', 'days', 'milliseconds']; + var PARSE_RE = /^(-?)(?:(\d+)\.)?(\d+):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/; + // Parsing and Creation + function createDuration(input, unit) { + var _a; + if (typeof input === 'string') { + return parseString(input); + } + if (typeof input === 'object' && input) { // non-null object + return parseObject(input); + } + if (typeof input === 'number') { + return parseObject((_a = {}, _a[unit || 'milliseconds'] = input, _a)); + } + return null; + } + function parseString(s) { + var m = PARSE_RE.exec(s); + if (m) { + var sign = m[1] ? -1 : 1; + return { + years: 0, + months: 0, + days: sign * (m[2] ? parseInt(m[2], 10) : 0), + milliseconds: sign * ((m[3] ? parseInt(m[3], 10) : 0) * 60 * 60 * 1000 + // hours + (m[4] ? parseInt(m[4], 10) : 0) * 60 * 1000 + // minutes + (m[5] ? parseInt(m[5], 10) : 0) * 1000 + // seconds + (m[6] ? parseInt(m[6], 10) : 0) // ms + ), + }; + } + return null; + } + function parseObject(obj) { + var duration = { + years: obj.years || obj.year || 0, + months: obj.months || obj.month || 0, + days: obj.days || obj.day || 0, + milliseconds: (obj.hours || obj.hour || 0) * 60 * 60 * 1000 + // hours + (obj.minutes || obj.minute || 0) * 60 * 1000 + // minutes + (obj.seconds || obj.second || 0) * 1000 + // seconds + (obj.milliseconds || obj.millisecond || obj.ms || 0), // ms + }; + var weeks = obj.weeks || obj.week; + if (weeks) { + duration.days += weeks * 7; + duration.specifiedWeeks = true; + } + return duration; + } + // Equality + function durationsEqual(d0, d1) { + return d0.years === d1.years && + d0.months === d1.months && + d0.days === d1.days && + d0.milliseconds === d1.milliseconds; + } + function asCleanDays(dur) { + if (!dur.years && !dur.months && !dur.milliseconds) { + return dur.days; + } + return 0; + } + // Simple Math + function addDurations(d0, d1) { + return { + years: d0.years + d1.years, + months: d0.months + d1.months, + days: d0.days + d1.days, + milliseconds: d0.milliseconds + d1.milliseconds, + }; + } + function subtractDurations(d1, d0) { + return { + years: d1.years - d0.years, + months: d1.months - d0.months, + days: d1.days - d0.days, + milliseconds: d1.milliseconds - d0.milliseconds, + }; + } + function multiplyDuration(d, n) { + return { + years: d.years * n, + months: d.months * n, + days: d.days * n, + milliseconds: d.milliseconds * n, + }; + } + // Conversions + // "Rough" because they are based on average-case Gregorian months/years + function asRoughYears(dur) { + return asRoughDays(dur) / 365; + } + function asRoughMonths(dur) { + return asRoughDays(dur) / 30; + } + function asRoughDays(dur) { + return asRoughMs(dur) / 864e5; + } + function asRoughMinutes(dur) { + return asRoughMs(dur) / (1000 * 60); + } + function asRoughSeconds(dur) { + return asRoughMs(dur) / 1000; + } + function asRoughMs(dur) { + return dur.years * (365 * 864e5) + + dur.months * (30 * 864e5) + + dur.days * 864e5 + + dur.milliseconds; + } + // Advanced Math + function wholeDivideDurations(numerator, denominator) { + var res = null; + for (var i = 0; i < INTERNAL_UNITS.length; i += 1) { + var unit = INTERNAL_UNITS[i]; + if (denominator[unit]) { + var localRes = numerator[unit] / denominator[unit]; + if (!isInt(localRes) || (res !== null && res !== localRes)) { + return null; + } + res = localRes; + } + else if (numerator[unit]) { + // needs to divide by something but can't! + return null; + } + } + return res; + } + function greatestDurationDenominator(dur) { + var ms = dur.milliseconds; + if (ms) { + if (ms % 1000 !== 0) { + return { unit: 'millisecond', value: ms }; + } + if (ms % (1000 * 60) !== 0) { + return { unit: 'second', value: ms / 1000 }; + } + if (ms % (1000 * 60 * 60) !== 0) { + return { unit: 'minute', value: ms / (1000 * 60) }; + } + if (ms) { + return { unit: 'hour', value: ms / (1000 * 60 * 60) }; + } + } + if (dur.days) { + if (dur.specifiedWeeks && dur.days % 7 === 0) { + return { unit: 'week', value: dur.days / 7 }; + } + return { unit: 'day', value: dur.days }; + } + if (dur.months) { + return { unit: 'month', value: dur.months }; + } + if (dur.years) { + return { unit: 'year', value: dur.years }; + } + return { unit: 'millisecond', value: 0 }; + } + + // timeZoneOffset is in minutes + function buildIsoString(marker, timeZoneOffset, stripZeroTime) { + if (stripZeroTime === void 0) { stripZeroTime = false; } + var s = marker.toISOString(); + s = s.replace('.000', ''); + if (stripZeroTime) { + s = s.replace('T00:00:00Z', ''); + } + if (s.length > 10) { // time part wasn't stripped, can add timezone info + if (timeZoneOffset == null) { + s = s.replace('Z', ''); + } + else if (timeZoneOffset !== 0) { + s = s.replace('Z', formatTimeZoneOffset(timeZoneOffset, true)); + } + // otherwise, its UTC-0 and we want to keep the Z + } + return s; + } + // formats the date, but with no time part + // TODO: somehow merge with buildIsoString and stripZeroTime + // TODO: rename. omit "string" + function formatDayString(marker) { + return marker.toISOString().replace(/T.*$/, ''); + } + // TODO: use Date::toISOString and use everything after the T? + function formatIsoTimeString(marker) { + return padStart(marker.getUTCHours(), 2) + ':' + + padStart(marker.getUTCMinutes(), 2) + ':' + + padStart(marker.getUTCSeconds(), 2); + } + function formatTimeZoneOffset(minutes, doIso) { + if (doIso === void 0) { doIso = false; } + var sign = minutes < 0 ? '-' : '+'; + var abs = Math.abs(minutes); + var hours = Math.floor(abs / 60); + var mins = Math.round(abs % 60); + if (doIso) { + return sign + padStart(hours, 2) + ":" + padStart(mins, 2); + } + return "GMT" + sign + hours + (mins ? ":" + padStart(mins, 2) : ''); + } + + // TODO: new util arrayify? + function removeExact(array, exactVal) { + var removeCnt = 0; + var i = 0; + while (i < array.length) { + if (array[i] === exactVal) { + array.splice(i, 1); + removeCnt += 1; + } + else { + i += 1; + } + } + return removeCnt; + } + function isArraysEqual(a0, a1, equalityFunc) { + if (a0 === a1) { + return true; + } + var len = a0.length; + var i; + if (len !== a1.length) { // not array? or not same length? + return false; + } + for (i = 0; i < len; i += 1) { + if (!(equalityFunc ? equalityFunc(a0[i], a1[i]) : a0[i] === a1[i])) { + return false; + } + } + return true; + } + + function memoize(workerFunc, resEquality, teardownFunc) { + var currentArgs; + var currentRes; + return function () { + var newArgs = []; + for (var _i = 0; _i < arguments.length; _i++) { + newArgs[_i] = arguments[_i]; + } + if (!currentArgs) { + currentRes = workerFunc.apply(this, newArgs); + } + else if (!isArraysEqual(currentArgs, newArgs)) { + if (teardownFunc) { + teardownFunc(currentRes); + } + var res = workerFunc.apply(this, newArgs); + if (!resEquality || !resEquality(res, currentRes)) { + currentRes = res; + } + } + currentArgs = newArgs; + return currentRes; + }; + } + function memoizeObjArg(workerFunc, resEquality, teardownFunc) { + var _this = this; + var currentArg; + var currentRes; + return function (newArg) { + if (!currentArg) { + currentRes = workerFunc.call(_this, newArg); + } + else if (!isPropsEqual(currentArg, newArg)) { + if (teardownFunc) { + teardownFunc(currentRes); + } + var res = workerFunc.call(_this, newArg); + if (!resEquality || !resEquality(res, currentRes)) { + currentRes = res; + } + } + currentArg = newArg; + return currentRes; + }; + } + function memoizeArraylike(// used at all? + workerFunc, resEquality, teardownFunc) { + var _this = this; + var currentArgSets = []; + var currentResults = []; + return function (newArgSets) { + var currentLen = currentArgSets.length; + var newLen = newArgSets.length; + var i = 0; + for (; i < currentLen; i += 1) { + if (!newArgSets[i]) { // one of the old sets no longer exists + if (teardownFunc) { + teardownFunc(currentResults[i]); + } + } + else if (!isArraysEqual(currentArgSets[i], newArgSets[i])) { + if (teardownFunc) { + teardownFunc(currentResults[i]); + } + var res = workerFunc.apply(_this, newArgSets[i]); + if (!resEquality || !resEquality(res, currentResults[i])) { + currentResults[i] = res; + } + } + } + for (; i < newLen; i += 1) { + currentResults[i] = workerFunc.apply(_this, newArgSets[i]); + } + currentArgSets = newArgSets; + currentResults.splice(newLen); // remove excess + return currentResults; + }; + } + function memoizeHashlike(// used? + workerFunc, resEquality, teardownFunc) { + var _this = this; + var currentArgHash = {}; + var currentResHash = {}; + return function (newArgHash) { + var newResHash = {}; + for (var key in newArgHash) { + if (!currentResHash[key]) { + newResHash[key] = workerFunc.apply(_this, newArgHash[key]); + } + else if (!isArraysEqual(currentArgHash[key], newArgHash[key])) { + if (teardownFunc) { + teardownFunc(currentResHash[key]); + } + var res = workerFunc.apply(_this, newArgHash[key]); + newResHash[key] = (resEquality && resEquality(res, currentResHash[key])) + ? currentResHash[key] + : res; + } + else { + newResHash[key] = currentResHash[key]; + } + } + currentArgHash = newArgHash; + currentResHash = newResHash; + return newResHash; + }; + } + + var EXTENDED_SETTINGS_AND_SEVERITIES = { + week: 3, + separator: 0, + omitZeroMinute: 0, + meridiem: 0, + omitCommas: 0, + }; + var STANDARD_DATE_PROP_SEVERITIES = { + timeZoneName: 7, + era: 6, + year: 5, + month: 4, + day: 2, + weekday: 2, + hour: 1, + minute: 1, + second: 1, + }; + var MERIDIEM_RE = /\s*([ap])\.?m\.?/i; // eats up leading spaces too + var COMMA_RE = /,/g; // we need re for globalness + var MULTI_SPACE_RE = /\s+/g; + var LTR_RE = /\u200e/g; // control character + var UTC_RE = /UTC|GMT/; + var NativeFormatter = /** @class */ (function () { + function NativeFormatter(formatSettings) { + var standardDateProps = {}; + var extendedSettings = {}; + var severity = 0; + for (var name_1 in formatSettings) { + if (name_1 in EXTENDED_SETTINGS_AND_SEVERITIES) { + extendedSettings[name_1] = formatSettings[name_1]; + severity = Math.max(EXTENDED_SETTINGS_AND_SEVERITIES[name_1], severity); + } + else { + standardDateProps[name_1] = formatSettings[name_1]; + if (name_1 in STANDARD_DATE_PROP_SEVERITIES) { // TODO: what about hour12? no severity + severity = Math.max(STANDARD_DATE_PROP_SEVERITIES[name_1], severity); + } + } + } + this.standardDateProps = standardDateProps; + this.extendedSettings = extendedSettings; + this.severity = severity; + this.buildFormattingFunc = memoize(buildFormattingFunc); + } + NativeFormatter.prototype.format = function (date, context) { + return this.buildFormattingFunc(this.standardDateProps, this.extendedSettings, context)(date); + }; + NativeFormatter.prototype.formatRange = function (start, end, context, betterDefaultSeparator) { + var _a = this, standardDateProps = _a.standardDateProps, extendedSettings = _a.extendedSettings; + var diffSeverity = computeMarkerDiffSeverity(start.marker, end.marker, context.calendarSystem); + if (!diffSeverity) { + return this.format(start, context); + } + var biggestUnitForPartial = diffSeverity; + if (biggestUnitForPartial > 1 && // the two dates are different in a way that's larger scale than time + (standardDateProps.year === 'numeric' || standardDateProps.year === '2-digit') && + (standardDateProps.month === 'numeric' || standardDateProps.month === '2-digit') && + (standardDateProps.day === 'numeric' || standardDateProps.day === '2-digit')) { + biggestUnitForPartial = 1; // make it look like the dates are only different in terms of time + } + var full0 = this.format(start, context); + var full1 = this.format(end, context); + if (full0 === full1) { + return full0; + } + var partialDateProps = computePartialFormattingOptions(standardDateProps, biggestUnitForPartial); + var partialFormattingFunc = buildFormattingFunc(partialDateProps, extendedSettings, context); + var partial0 = partialFormattingFunc(start); + var partial1 = partialFormattingFunc(end); + var insertion = findCommonInsertion(full0, partial0, full1, partial1); + var separator = extendedSettings.separator || betterDefaultSeparator || context.defaultSeparator || ''; + if (insertion) { + return insertion.before + partial0 + separator + partial1 + insertion.after; + } + return full0 + separator + full1; + }; + NativeFormatter.prototype.getLargestUnit = function () { + switch (this.severity) { + case 7: + case 6: + case 5: + return 'year'; + case 4: + return 'month'; + case 3: + return 'week'; + case 2: + return 'day'; + default: + return 'time'; // really? + } + }; + return NativeFormatter; + }()); + function buildFormattingFunc(standardDateProps, extendedSettings, context) { + var standardDatePropCnt = Object.keys(standardDateProps).length; + if (standardDatePropCnt === 1 && standardDateProps.timeZoneName === 'short') { + return function (date) { return (formatTimeZoneOffset(date.timeZoneOffset)); }; + } + if (standardDatePropCnt === 0 && extendedSettings.week) { + return function (date) { return (formatWeekNumber(context.computeWeekNumber(date.marker), context.weekText, context.locale, extendedSettings.week)); }; + } + return buildNativeFormattingFunc(standardDateProps, extendedSettings, context); + } + function buildNativeFormattingFunc(standardDateProps, extendedSettings, context) { + standardDateProps = __assign({}, standardDateProps); // copy + extendedSettings = __assign({}, extendedSettings); // copy + sanitizeSettings(standardDateProps, extendedSettings); + standardDateProps.timeZone = 'UTC'; // we leverage the only guaranteed timeZone for our UTC markers + var normalFormat = new Intl.DateTimeFormat(context.locale.codes, standardDateProps); + var zeroFormat; // needed? + if (extendedSettings.omitZeroMinute) { + var zeroProps = __assign({}, standardDateProps); + delete zeroProps.minute; // seconds and ms were already considered in sanitizeSettings + zeroFormat = new Intl.DateTimeFormat(context.locale.codes, zeroProps); + } + return function (date) { + var marker = date.marker; + var format; + if (zeroFormat && !marker.getUTCMinutes()) { + format = zeroFormat; + } + else { + format = normalFormat; + } + var s = format.format(marker); + return postProcess(s, date, standardDateProps, extendedSettings, context); + }; + } + function sanitizeSettings(standardDateProps, extendedSettings) { + // deal with a browser inconsistency where formatting the timezone + // requires that the hour/minute be present. + if (standardDateProps.timeZoneName) { + if (!standardDateProps.hour) { + standardDateProps.hour = '2-digit'; + } + if (!standardDateProps.minute) { + standardDateProps.minute = '2-digit'; + } + } + // only support short timezone names + if (standardDateProps.timeZoneName === 'long') { + standardDateProps.timeZoneName = 'short'; + } + // if requesting to display seconds, MUST display minutes + if (extendedSettings.omitZeroMinute && (standardDateProps.second || standardDateProps.millisecond)) { + delete extendedSettings.omitZeroMinute; + } + } + function postProcess(s, date, standardDateProps, extendedSettings, context) { + s = s.replace(LTR_RE, ''); // remove left-to-right control chars. do first. good for other regexes + if (standardDateProps.timeZoneName === 'short') { + s = injectTzoStr(s, (context.timeZone === 'UTC' || date.timeZoneOffset == null) ? + 'UTC' : // important to normalize for IE, which does "GMT" + formatTimeZoneOffset(date.timeZoneOffset)); + } + if (extendedSettings.omitCommas) { + s = s.replace(COMMA_RE, '').trim(); + } + if (extendedSettings.omitZeroMinute) { + s = s.replace(':00', ''); // zeroFormat doesn't always achieve this + } + // ^ do anything that might create adjacent spaces before this point, + // because MERIDIEM_RE likes to eat up loading spaces + if (extendedSettings.meridiem === false) { + s = s.replace(MERIDIEM_RE, '').trim(); + } + else if (extendedSettings.meridiem === 'narrow') { // a/p + s = s.replace(MERIDIEM_RE, function (m0, m1) { return m1.toLocaleLowerCase(); }); + } + else if (extendedSettings.meridiem === 'short') { // am/pm + s = s.replace(MERIDIEM_RE, function (m0, m1) { return m1.toLocaleLowerCase() + "m"; }); + } + else if (extendedSettings.meridiem === 'lowercase') { // other meridiem transformers already converted to lowercase + s = s.replace(MERIDIEM_RE, function (m0) { return m0.toLocaleLowerCase(); }); + } + s = s.replace(MULTI_SPACE_RE, ' '); + s = s.trim(); + return s; + } + function injectTzoStr(s, tzoStr) { + var replaced = false; + s = s.replace(UTC_RE, function () { + replaced = true; + return tzoStr; + }); + // IE11 doesn't include UTC/GMT in the original string, so append to end + if (!replaced) { + s += " " + tzoStr; + } + return s; + } + function formatWeekNumber(num, weekText, locale, display) { + var parts = []; + if (display === 'narrow') { + parts.push(weekText); + } + else if (display === 'short') { + parts.push(weekText, ' '); + } + // otherwise, considered 'numeric' + parts.push(locale.simpleNumberFormat.format(num)); + if (locale.options.direction === 'rtl') { // TODO: use control characters instead? + parts.reverse(); + } + return parts.join(''); + } + // Range Formatting Utils + // 0 = exactly the same + // 1 = different by time + // and bigger + function computeMarkerDiffSeverity(d0, d1, ca) { + if (ca.getMarkerYear(d0) !== ca.getMarkerYear(d1)) { + return 5; + } + if (ca.getMarkerMonth(d0) !== ca.getMarkerMonth(d1)) { + return 4; + } + if (ca.getMarkerDay(d0) !== ca.getMarkerDay(d1)) { + return 2; + } + if (timeAsMs(d0) !== timeAsMs(d1)) { + return 1; + } + return 0; + } + function computePartialFormattingOptions(options, biggestUnit) { + var partialOptions = {}; + for (var name_2 in options) { + if (!(name_2 in STANDARD_DATE_PROP_SEVERITIES) || // not a date part prop (like timeZone) + STANDARD_DATE_PROP_SEVERITIES[name_2] <= biggestUnit) { + partialOptions[name_2] = options[name_2]; + } + } + return partialOptions; + } + function findCommonInsertion(full0, partial0, full1, partial1) { + var i0 = 0; + while (i0 < full0.length) { + var found0 = full0.indexOf(partial0, i0); + if (found0 === -1) { + break; + } + var before0 = full0.substr(0, found0); + i0 = found0 + partial0.length; + var after0 = full0.substr(i0); + var i1 = 0; + while (i1 < full1.length) { + var found1 = full1.indexOf(partial1, i1); + if (found1 === -1) { + break; + } + var before1 = full1.substr(0, found1); + i1 = found1 + partial1.length; + var after1 = full1.substr(i1); + if (before0 === before1 && after0 === after1) { + return { + before: before0, + after: after0, + }; + } + } + } + return null; + } + + function expandZonedMarker(dateInfo, calendarSystem) { + var a = calendarSystem.markerToArray(dateInfo.marker); + return { + marker: dateInfo.marker, + timeZoneOffset: dateInfo.timeZoneOffset, + array: a, + year: a[0], + month: a[1], + day: a[2], + hour: a[3], + minute: a[4], + second: a[5], + millisecond: a[6], + }; + } + + function createVerboseFormattingArg(start, end, context, betterDefaultSeparator) { + var startInfo = expandZonedMarker(start, context.calendarSystem); + var endInfo = end ? expandZonedMarker(end, context.calendarSystem) : null; + return { + date: startInfo, + start: startInfo, + end: endInfo, + timeZone: context.timeZone, + localeCodes: context.locale.codes, + defaultSeparator: betterDefaultSeparator || context.defaultSeparator, + }; + } + + /* + TODO: fix the terminology of "formatter" vs "formatting func" + */ + /* + At the time of instantiation, this object does not know which cmd-formatting system it will use. + It receives this at the time of formatting, as a setting. + */ + var CmdFormatter = /** @class */ (function () { + function CmdFormatter(cmdStr) { + this.cmdStr = cmdStr; + } + CmdFormatter.prototype.format = function (date, context, betterDefaultSeparator) { + return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(date, null, context, betterDefaultSeparator)); + }; + CmdFormatter.prototype.formatRange = function (start, end, context, betterDefaultSeparator) { + return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(start, end, context, betterDefaultSeparator)); + }; + return CmdFormatter; + }()); + + var FuncFormatter = /** @class */ (function () { + function FuncFormatter(func) { + this.func = func; + } + FuncFormatter.prototype.format = function (date, context, betterDefaultSeparator) { + return this.func(createVerboseFormattingArg(date, null, context, betterDefaultSeparator)); + }; + FuncFormatter.prototype.formatRange = function (start, end, context, betterDefaultSeparator) { + return this.func(createVerboseFormattingArg(start, end, context, betterDefaultSeparator)); + }; + return FuncFormatter; + }()); + + function createFormatter(input) { + if (typeof input === 'object' && input) { // non-null object + return new NativeFormatter(input); + } + if (typeof input === 'string') { + return new CmdFormatter(input); + } + if (typeof input === 'function') { + return new FuncFormatter(input); + } + return null; + } + + // base options + // ------------ + var BASE_OPTION_REFINERS = { + navLinkDayClick: identity, + navLinkWeekClick: identity, + duration: createDuration, + bootstrapFontAwesome: identity, + buttonIcons: identity, + customButtons: identity, + defaultAllDayEventDuration: createDuration, + defaultTimedEventDuration: createDuration, + nextDayThreshold: createDuration, + scrollTime: createDuration, + scrollTimeReset: Boolean, + slotMinTime: createDuration, + slotMaxTime: createDuration, + dayPopoverFormat: createFormatter, + slotDuration: createDuration, + snapDuration: createDuration, + headerToolbar: identity, + footerToolbar: identity, + defaultRangeSeparator: String, + titleRangeSeparator: String, + forceEventDuration: Boolean, + dayHeaders: Boolean, + dayHeaderFormat: createFormatter, + dayHeaderClassNames: identity, + dayHeaderContent: identity, + dayHeaderDidMount: identity, + dayHeaderWillUnmount: identity, + dayCellClassNames: identity, + dayCellContent: identity, + dayCellDidMount: identity, + dayCellWillUnmount: identity, + initialView: String, + aspectRatio: Number, + weekends: Boolean, + weekNumberCalculation: identity, + weekNumbers: Boolean, + weekNumberClassNames: identity, + weekNumberContent: identity, + weekNumberDidMount: identity, + weekNumberWillUnmount: identity, + editable: Boolean, + viewClassNames: identity, + viewDidMount: identity, + viewWillUnmount: identity, + nowIndicator: Boolean, + nowIndicatorClassNames: identity, + nowIndicatorContent: identity, + nowIndicatorDidMount: identity, + nowIndicatorWillUnmount: identity, + showNonCurrentDates: Boolean, + lazyFetching: Boolean, + startParam: String, + endParam: String, + timeZoneParam: String, + timeZone: String, + locales: identity, + locale: identity, + themeSystem: String, + dragRevertDuration: Number, + dragScroll: Boolean, + allDayMaintainDuration: Boolean, + unselectAuto: Boolean, + dropAccept: identity, + eventOrder: parseFieldSpecs, + eventOrderStrict: Boolean, + handleWindowResize: Boolean, + windowResizeDelay: Number, + longPressDelay: Number, + eventDragMinDistance: Number, + expandRows: Boolean, + height: identity, + contentHeight: identity, + direction: String, + weekNumberFormat: createFormatter, + eventResizableFromStart: Boolean, + displayEventTime: Boolean, + displayEventEnd: Boolean, + weekText: String, + progressiveEventRendering: Boolean, + businessHours: identity, + initialDate: identity, + now: identity, + eventDataTransform: identity, + stickyHeaderDates: identity, + stickyFooterScrollbar: identity, + viewHeight: identity, + defaultAllDay: Boolean, + eventSourceFailure: identity, + eventSourceSuccess: identity, + eventDisplay: String, + eventStartEditable: Boolean, + eventDurationEditable: Boolean, + eventOverlap: identity, + eventConstraint: identity, + eventAllow: identity, + eventBackgroundColor: String, + eventBorderColor: String, + eventTextColor: String, + eventColor: String, + eventClassNames: identity, + eventContent: identity, + eventDidMount: identity, + eventWillUnmount: identity, + selectConstraint: identity, + selectOverlap: identity, + selectAllow: identity, + droppable: Boolean, + unselectCancel: String, + slotLabelFormat: identity, + slotLaneClassNames: identity, + slotLaneContent: identity, + slotLaneDidMount: identity, + slotLaneWillUnmount: identity, + slotLabelClassNames: identity, + slotLabelContent: identity, + slotLabelDidMount: identity, + slotLabelWillUnmount: identity, + dayMaxEvents: identity, + dayMaxEventRows: identity, + dayMinWidth: Number, + slotLabelInterval: createDuration, + allDayText: String, + allDayClassNames: identity, + allDayContent: identity, + allDayDidMount: identity, + allDayWillUnmount: identity, + slotMinWidth: Number, + navLinks: Boolean, + eventTimeFormat: createFormatter, + rerenderDelay: Number, + moreLinkText: identity, + selectMinDistance: Number, + selectable: Boolean, + selectLongPressDelay: Number, + eventLongPressDelay: Number, + selectMirror: Boolean, + eventMaxStack: Number, + eventMinHeight: Number, + eventMinWidth: Number, + eventShortHeight: Number, + slotEventOverlap: Boolean, + plugins: identity, + firstDay: Number, + dayCount: Number, + dateAlignment: String, + dateIncrement: createDuration, + hiddenDays: identity, + monthMode: Boolean, + fixedWeekCount: Boolean, + validRange: identity, + visibleRange: identity, + titleFormat: identity, + // only used by list-view, but languages define the value, so we need it in base options + noEventsText: String, + moreLinkClick: identity, + moreLinkClassNames: identity, + moreLinkContent: identity, + moreLinkDidMount: identity, + moreLinkWillUnmount: identity, + }; + // do NOT give a type here. need `typeof BASE_OPTION_DEFAULTS` to give real results. + // raw values. + var BASE_OPTION_DEFAULTS = { + eventDisplay: 'auto', + defaultRangeSeparator: ' - ', + titleRangeSeparator: ' \u2013 ', + defaultTimedEventDuration: '01:00:00', + defaultAllDayEventDuration: { day: 1 }, + forceEventDuration: false, + nextDayThreshold: '00:00:00', + dayHeaders: true, + initialView: '', + aspectRatio: 1.35, + headerToolbar: { + start: 'title', + center: '', + end: 'today prev,next', + }, + weekends: true, + weekNumbers: false, + weekNumberCalculation: 'local', + editable: false, + nowIndicator: false, + scrollTime: '06:00:00', + scrollTimeReset: true, + slotMinTime: '00:00:00', + slotMaxTime: '24:00:00', + showNonCurrentDates: true, + lazyFetching: true, + startParam: 'start', + endParam: 'end', + timeZoneParam: 'timeZone', + timeZone: 'local', + locales: [], + locale: '', + themeSystem: 'standard', + dragRevertDuration: 500, + dragScroll: true, + allDayMaintainDuration: false, + unselectAuto: true, + dropAccept: '*', + eventOrder: 'start,-duration,allDay,title', + dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' }, + handleWindowResize: true, + windowResizeDelay: 100, + longPressDelay: 1000, + eventDragMinDistance: 5, + expandRows: false, + navLinks: false, + selectable: false, + eventMinHeight: 15, + eventMinWidth: 30, + eventShortHeight: 30, + }; + // calendar listeners + // ------------------ + var CALENDAR_LISTENER_REFINERS = { + datesSet: identity, + eventsSet: identity, + eventAdd: identity, + eventChange: identity, + eventRemove: identity, + windowResize: identity, + eventClick: identity, + eventMouseEnter: identity, + eventMouseLeave: identity, + select: identity, + unselect: identity, + loading: identity, + // internal + _unmount: identity, + _beforeprint: identity, + _afterprint: identity, + _noEventDrop: identity, + _noEventResize: identity, + _resize: identity, + _scrollRequest: identity, + }; + // calendar-specific options + // ------------------------- + var CALENDAR_OPTION_REFINERS = { + buttonText: identity, + views: identity, + plugins: identity, + initialEvents: identity, + events: identity, + eventSources: identity, + }; + var COMPLEX_OPTION_COMPARATORS = { + headerToolbar: isBoolComplexEqual, + footerToolbar: isBoolComplexEqual, + buttonText: isBoolComplexEqual, + buttonIcons: isBoolComplexEqual, + }; + function isBoolComplexEqual(a, b) { + if (typeof a === 'object' && typeof b === 'object' && a && b) { // both non-null objects + return isPropsEqual(a, b); + } + return a === b; + } + // view-specific options + // --------------------- + var VIEW_OPTION_REFINERS = { + type: String, + component: identity, + buttonText: String, + buttonTextKey: String, + dateProfileGeneratorClass: identity, + usesMinMaxTime: Boolean, + classNames: identity, + content: identity, + didMount: identity, + willUnmount: identity, + }; + // util funcs + // ---------------------------------------------------------------------------------------------------- + function mergeRawOptions(optionSets) { + return mergeProps(optionSets, COMPLEX_OPTION_COMPARATORS); + } + function refineProps(input, refiners) { + var refined = {}; + var extra = {}; + for (var propName in refiners) { + if (propName in input) { + refined[propName] = refiners[propName](input[propName]); + } + } + for (var propName in input) { + if (!(propName in refiners)) { + extra[propName] = input[propName]; + } + } + return { refined: refined, extra: extra }; + } + function identity(raw) { + return raw; + } + + function parseEvents(rawEvents, eventSource, context, allowOpenRange) { + var eventStore = createEmptyEventStore(); + var eventRefiners = buildEventRefiners(context); + for (var _i = 0, rawEvents_1 = rawEvents; _i < rawEvents_1.length; _i++) { + var rawEvent = rawEvents_1[_i]; + var tuple = parseEvent(rawEvent, eventSource, context, allowOpenRange, eventRefiners); + if (tuple) { + eventTupleToStore(tuple, eventStore); + } + } + return eventStore; + } + function eventTupleToStore(tuple, eventStore) { + if (eventStore === void 0) { eventStore = createEmptyEventStore(); } + eventStore.defs[tuple.def.defId] = tuple.def; + if (tuple.instance) { + eventStore.instances[tuple.instance.instanceId] = tuple.instance; + } + return eventStore; + } + // retrieves events that have the same groupId as the instance specified by `instanceId` + // or they are the same as the instance. + // why might instanceId not be in the store? an event from another calendar? + function getRelevantEvents(eventStore, instanceId) { + var instance = eventStore.instances[instanceId]; + if (instance) { + var def_1 = eventStore.defs[instance.defId]; + // get events/instances with same group + var newStore = filterEventStoreDefs(eventStore, function (lookDef) { return isEventDefsGrouped(def_1, lookDef); }); + // add the original + // TODO: wish we could use eventTupleToStore or something like it + newStore.defs[def_1.defId] = def_1; + newStore.instances[instance.instanceId] = instance; + return newStore; + } + return createEmptyEventStore(); + } + function isEventDefsGrouped(def0, def1) { + return Boolean(def0.groupId && def0.groupId === def1.groupId); + } + function createEmptyEventStore() { + return { defs: {}, instances: {} }; + } + function mergeEventStores(store0, store1) { + return { + defs: __assign(__assign({}, store0.defs), store1.defs), + instances: __assign(__assign({}, store0.instances), store1.instances), + }; + } + function filterEventStoreDefs(eventStore, filterFunc) { + var defs = filterHash(eventStore.defs, filterFunc); + var instances = filterHash(eventStore.instances, function (instance) { return (defs[instance.defId] // still exists? + ); }); + return { defs: defs, instances: instances }; + } + function excludeSubEventStore(master, sub) { + var defs = master.defs, instances = master.instances; + var filteredDefs = {}; + var filteredInstances = {}; + for (var defId in defs) { + if (!sub.defs[defId]) { // not explicitly excluded + filteredDefs[defId] = defs[defId]; + } + } + for (var instanceId in instances) { + if (!sub.instances[instanceId] && // not explicitly excluded + filteredDefs[instances[instanceId].defId] // def wasn't filtered away + ) { + filteredInstances[instanceId] = instances[instanceId]; + } + } + return { + defs: filteredDefs, + instances: filteredInstances, + }; + } + + function normalizeConstraint(input, context) { + if (Array.isArray(input)) { + return parseEvents(input, null, context, true); // allowOpenRange=true + } + if (typeof input === 'object' && input) { // non-null object + return parseEvents([input], null, context, true); // allowOpenRange=true + } + if (input != null) { + return String(input); + } + return null; + } + + function parseClassNames(raw) { + if (Array.isArray(raw)) { + return raw; + } + if (typeof raw === 'string') { + return raw.split(/\s+/); + } + return []; + } + + // TODO: better called "EventSettings" or "EventConfig" + // TODO: move this file into structs + // TODO: separate constraint/overlap/allow, because selection uses only that, not other props + var EVENT_UI_REFINERS = { + display: String, + editable: Boolean, + startEditable: Boolean, + durationEditable: Boolean, + constraint: identity, + overlap: identity, + allow: identity, + className: parseClassNames, + classNames: parseClassNames, + color: String, + backgroundColor: String, + borderColor: String, + textColor: String, + }; + var EMPTY_EVENT_UI = { + display: null, + startEditable: null, + durationEditable: null, + constraints: [], + overlap: null, + allows: [], + backgroundColor: '', + borderColor: '', + textColor: '', + classNames: [], + }; + function createEventUi(refined, context) { + var constraint = normalizeConstraint(refined.constraint, context); + return { + display: refined.display || null, + startEditable: refined.startEditable != null ? refined.startEditable : refined.editable, + durationEditable: refined.durationEditable != null ? refined.durationEditable : refined.editable, + constraints: constraint != null ? [constraint] : [], + overlap: refined.overlap != null ? refined.overlap : null, + allows: refined.allow != null ? [refined.allow] : [], + backgroundColor: refined.backgroundColor || refined.color || '', + borderColor: refined.borderColor || refined.color || '', + textColor: refined.textColor || '', + classNames: (refined.className || []).concat(refined.classNames || []), // join singular and plural + }; + } + // TODO: prevent against problems with <2 args! + function combineEventUis(uis) { + return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI); + } + function combineTwoEventUis(item0, item1) { + return { + display: item1.display != null ? item1.display : item0.display, + startEditable: item1.startEditable != null ? item1.startEditable : item0.startEditable, + durationEditable: item1.durationEditable != null ? item1.durationEditable : item0.durationEditable, + constraints: item0.constraints.concat(item1.constraints), + overlap: typeof item1.overlap === 'boolean' ? item1.overlap : item0.overlap, + allows: item0.allows.concat(item1.allows), + backgroundColor: item1.backgroundColor || item0.backgroundColor, + borderColor: item1.borderColor || item0.borderColor, + textColor: item1.textColor || item0.textColor, + classNames: item0.classNames.concat(item1.classNames), + }; + } + + var EVENT_NON_DATE_REFINERS = { + id: String, + groupId: String, + title: String, + url: String, + }; + var EVENT_DATE_REFINERS = { + start: identity, + end: identity, + date: identity, + allDay: Boolean, + }; + var EVENT_REFINERS = __assign(__assign(__assign({}, EVENT_NON_DATE_REFINERS), EVENT_DATE_REFINERS), { extendedProps: identity }); + function parseEvent(raw, eventSource, context, allowOpenRange, refiners) { + if (refiners === void 0) { refiners = buildEventRefiners(context); } + var _a = refineEventDef(raw, context, refiners), refined = _a.refined, extra = _a.extra; + var defaultAllDay = computeIsDefaultAllDay(eventSource, context); + var recurringRes = parseRecurring(refined, defaultAllDay, context.dateEnv, context.pluginHooks.recurringTypes); + if (recurringRes) { + var def = parseEventDef(refined, extra, eventSource ? eventSource.sourceId : '', recurringRes.allDay, Boolean(recurringRes.duration), context); + def.recurringDef = { + typeId: recurringRes.typeId, + typeData: recurringRes.typeData, + duration: recurringRes.duration, + }; + return { def: def, instance: null }; + } + var singleRes = parseSingle(refined, defaultAllDay, context, allowOpenRange); + if (singleRes) { + var def = parseEventDef(refined, extra, eventSource ? eventSource.sourceId : '', singleRes.allDay, singleRes.hasEnd, context); + var instance = createEventInstance(def.defId, singleRes.range, singleRes.forcedStartTzo, singleRes.forcedEndTzo); + return { def: def, instance: instance }; + } + return null; + } + function refineEventDef(raw, context, refiners) { + if (refiners === void 0) { refiners = buildEventRefiners(context); } + return refineProps(raw, refiners); + } + function buildEventRefiners(context) { + return __assign(__assign(__assign({}, EVENT_UI_REFINERS), EVENT_REFINERS), context.pluginHooks.eventRefiners); + } + /* + Will NOT populate extendedProps with the leftover properties. + Will NOT populate date-related props. + */ + function parseEventDef(refined, extra, sourceId, allDay, hasEnd, context) { + var def = { + title: refined.title || '', + groupId: refined.groupId || '', + publicId: refined.id || '', + url: refined.url || '', + recurringDef: null, + defId: guid(), + sourceId: sourceId, + allDay: allDay, + hasEnd: hasEnd, + ui: createEventUi(refined, context), + extendedProps: __assign(__assign({}, (refined.extendedProps || {})), extra), + }; + for (var _i = 0, _a = context.pluginHooks.eventDefMemberAdders; _i < _a.length; _i++) { + var memberAdder = _a[_i]; + __assign(def, memberAdder(refined)); + } + // help out EventApi from having user modify props + Object.freeze(def.ui.classNames); + Object.freeze(def.extendedProps); + return def; + } + function parseSingle(refined, defaultAllDay, context, allowOpenRange) { + var allDay = refined.allDay; + var startMeta; + var startMarker = null; + var hasEnd = false; + var endMeta; + var endMarker = null; + var startInput = refined.start != null ? refined.start : refined.date; + startMeta = context.dateEnv.createMarkerMeta(startInput); + if (startMeta) { + startMarker = startMeta.marker; + } + else if (!allowOpenRange) { + return null; + } + if (refined.end != null) { + endMeta = context.dateEnv.createMarkerMeta(refined.end); + } + if (allDay == null) { + if (defaultAllDay != null) { + allDay = defaultAllDay; + } + else { + // fall back to the date props LAST + allDay = (!startMeta || startMeta.isTimeUnspecified) && + (!endMeta || endMeta.isTimeUnspecified); + } + } + if (allDay && startMarker) { + startMarker = startOfDay(startMarker); + } + if (endMeta) { + endMarker = endMeta.marker; + if (allDay) { + endMarker = startOfDay(endMarker); + } + if (startMarker && endMarker <= startMarker) { + endMarker = null; + } + } + if (endMarker) { + hasEnd = true; + } + else if (!allowOpenRange) { + hasEnd = context.options.forceEventDuration || false; + endMarker = context.dateEnv.add(startMarker, allDay ? + context.options.defaultAllDayEventDuration : + context.options.defaultTimedEventDuration); + } + return { + allDay: allDay, + hasEnd: hasEnd, + range: { start: startMarker, end: endMarker }, + forcedStartTzo: startMeta ? startMeta.forcedTzo : null, + forcedEndTzo: endMeta ? endMeta.forcedTzo : null, + }; + } + function computeIsDefaultAllDay(eventSource, context) { + var res = null; + if (eventSource) { + res = eventSource.defaultAllDay; + } + if (res == null) { + res = context.options.defaultAllDay; + } + return res; + } + + /* Date stuff that doesn't belong in datelib core + ----------------------------------------------------------------------------------------------------------------------*/ + // given a timed range, computes an all-day range that has the same exact duration, + // but whose start time is aligned with the start of the day. + function computeAlignedDayRange(timedRange) { + var dayCnt = Math.floor(diffDays(timedRange.start, timedRange.end)) || 1; + var start = startOfDay(timedRange.start); + var end = addDays(start, dayCnt); + return { start: start, end: end }; + } + // given a timed range, computes an all-day range based on how for the end date bleeds into the next day + // TODO: give nextDayThreshold a default arg + function computeVisibleDayRange(timedRange, nextDayThreshold) { + if (nextDayThreshold === void 0) { nextDayThreshold = createDuration(0); } + var startDay = null; + var endDay = null; + if (timedRange.end) { + endDay = startOfDay(timedRange.end); + var endTimeMS = timedRange.end.valueOf() - endDay.valueOf(); // # of milliseconds into `endDay` + // If the end time is actually inclusively part of the next day and is equal to or + // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. + // Otherwise, leaving it as inclusive will cause it to exclude `endDay`. + if (endTimeMS && endTimeMS >= asRoughMs(nextDayThreshold)) { + endDay = addDays(endDay, 1); + } + } + if (timedRange.start) { + startDay = startOfDay(timedRange.start); // the beginning of the day the range starts + // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day. + if (endDay && endDay <= startDay) { + endDay = addDays(startDay, 1); + } + } + return { start: startDay, end: endDay }; + } + // spans from one day into another? + function isMultiDayRange(range) { + var visibleRange = computeVisibleDayRange(range); + return diffDays(visibleRange.start, visibleRange.end) > 1; + } + function diffDates(date0, date1, dateEnv, largeUnit) { + if (largeUnit === 'year') { + return createDuration(dateEnv.diffWholeYears(date0, date1), 'year'); + } + if (largeUnit === 'month') { + return createDuration(dateEnv.diffWholeMonths(date0, date1), 'month'); + } + return diffDayAndTime(date0, date1); // returns a duration + } + + function parseRange(input, dateEnv) { + var start = null; + var end = null; + if (input.start) { + start = dateEnv.createMarker(input.start); + } + if (input.end) { + end = dateEnv.createMarker(input.end); + } + if (!start && !end) { + return null; + } + if (start && end && end < start) { + return null; + } + return { start: start, end: end }; + } + // SIDE-EFFECT: will mutate ranges. + // Will return a new array result. + function invertRanges(ranges, constraintRange) { + var invertedRanges = []; + var start = constraintRange.start; // the end of the previous range. the start of the new range + var i; + var dateRange; + // ranges need to be in order. required for our date-walking algorithm + ranges.sort(compareRanges); + for (i = 0; i < ranges.length; i += 1) { + dateRange = ranges[i]; + // add the span of time before the event (if there is any) + if (dateRange.start > start) { // compare millisecond time (skip any ambig logic) + invertedRanges.push({ start: start, end: dateRange.start }); + } + if (dateRange.end > start) { + start = dateRange.end; + } + } + // add the span of time after the last event (if there is any) + if (start < constraintRange.end) { // compare millisecond time (skip any ambig logic) + invertedRanges.push({ start: start, end: constraintRange.end }); + } + return invertedRanges; + } + function compareRanges(range0, range1) { + return range0.start.valueOf() - range1.start.valueOf(); // earlier ranges go first + } + function intersectRanges(range0, range1) { + var start = range0.start, end = range0.end; + var newRange = null; + if (range1.start !== null) { + if (start === null) { + start = range1.start; + } + else { + start = new Date(Math.max(start.valueOf(), range1.start.valueOf())); + } + } + if (range1.end != null) { + if (end === null) { + end = range1.end; + } + else { + end = new Date(Math.min(end.valueOf(), range1.end.valueOf())); + } + } + if (start === null || end === null || start < end) { + newRange = { start: start, end: end }; + } + return newRange; + } + function rangesEqual(range0, range1) { + return (range0.start === null ? null : range0.start.valueOf()) === (range1.start === null ? null : range1.start.valueOf()) && + (range0.end === null ? null : range0.end.valueOf()) === (range1.end === null ? null : range1.end.valueOf()); + } + function rangesIntersect(range0, range1) { + return (range0.end === null || range1.start === null || range0.end > range1.start) && + (range0.start === null || range1.end === null || range0.start < range1.end); + } + function rangeContainsRange(outerRange, innerRange) { + return (outerRange.start === null || (innerRange.start !== null && innerRange.start >= outerRange.start)) && + (outerRange.end === null || (innerRange.end !== null && innerRange.end <= outerRange.end)); + } + function rangeContainsMarker(range, date) { + return (range.start === null || date >= range.start) && + (range.end === null || date < range.end); + } + // If the given date is not within the given range, move it inside. + // (If it's past the end, make it one millisecond before the end). + function constrainMarkerToRange(date, range) { + if (range.start != null && date < range.start) { + return range.start; + } + if (range.end != null && date >= range.end) { + return new Date(range.end.valueOf() - 1); + } + return date; + } + + /* + Specifying nextDayThreshold signals that all-day ranges should be sliced. + */ + function sliceEventStore(eventStore, eventUiBases, framingRange, nextDayThreshold) { + var inverseBgByGroupId = {}; + var inverseBgByDefId = {}; + var defByGroupId = {}; + var bgRanges = []; + var fgRanges = []; + var eventUis = compileEventUis(eventStore.defs, eventUiBases); + for (var defId in eventStore.defs) { + var def = eventStore.defs[defId]; + var ui = eventUis[def.defId]; + if (ui.display === 'inverse-background') { + if (def.groupId) { + inverseBgByGroupId[def.groupId] = []; + if (!defByGroupId[def.groupId]) { + defByGroupId[def.groupId] = def; + } + } + else { + inverseBgByDefId[defId] = []; + } + } + } + for (var instanceId in eventStore.instances) { + var instance = eventStore.instances[instanceId]; + var def = eventStore.defs[instance.defId]; + var ui = eventUis[def.defId]; + var origRange = instance.range; + var normalRange = (!def.allDay && nextDayThreshold) ? + computeVisibleDayRange(origRange, nextDayThreshold) : + origRange; + var slicedRange = intersectRanges(normalRange, framingRange); + if (slicedRange) { + if (ui.display === 'inverse-background') { + if (def.groupId) { + inverseBgByGroupId[def.groupId].push(slicedRange); + } + else { + inverseBgByDefId[instance.defId].push(slicedRange); + } + } + else if (ui.display !== 'none') { + (ui.display === 'background' ? bgRanges : fgRanges).push({ + def: def, + ui: ui, + instance: instance, + range: slicedRange, + isStart: normalRange.start && normalRange.start.valueOf() === slicedRange.start.valueOf(), + isEnd: normalRange.end && normalRange.end.valueOf() === slicedRange.end.valueOf(), + }); + } + } + } + for (var groupId in inverseBgByGroupId) { // BY GROUP + var ranges = inverseBgByGroupId[groupId]; + var invertedRanges = invertRanges(ranges, framingRange); + for (var _i = 0, invertedRanges_1 = invertedRanges; _i < invertedRanges_1.length; _i++) { + var invertedRange = invertedRanges_1[_i]; + var def = defByGroupId[groupId]; + var ui = eventUis[def.defId]; + bgRanges.push({ + def: def, + ui: ui, + instance: null, + range: invertedRange, + isStart: false, + isEnd: false, + }); + } + } + for (var defId in inverseBgByDefId) { + var ranges = inverseBgByDefId[defId]; + var invertedRanges = invertRanges(ranges, framingRange); + for (var _a = 0, invertedRanges_2 = invertedRanges; _a < invertedRanges_2.length; _a++) { + var invertedRange = invertedRanges_2[_a]; + bgRanges.push({ + def: eventStore.defs[defId], + ui: eventUis[defId], + instance: null, + range: invertedRange, + isStart: false, + isEnd: false, + }); + } + } + return { bg: bgRanges, fg: fgRanges }; + } + function hasBgRendering(def) { + return def.ui.display === 'background' || def.ui.display === 'inverse-background'; + } + function setElSeg(el, seg) { + el.fcSeg = seg; + } + function getElSeg(el) { + return el.fcSeg || + el.parentNode.fcSeg || // for the harness + null; + } + // event ui computation + function compileEventUis(eventDefs, eventUiBases) { + return mapHash(eventDefs, function (eventDef) { return compileEventUi(eventDef, eventUiBases); }); + } + function compileEventUi(eventDef, eventUiBases) { + var uis = []; + if (eventUiBases['']) { + uis.push(eventUiBases['']); + } + if (eventUiBases[eventDef.defId]) { + uis.push(eventUiBases[eventDef.defId]); + } + uis.push(eventDef.ui); + return combineEventUis(uis); + } + function sortEventSegs(segs, eventOrderSpecs) { + var objs = segs.map(buildSegCompareObj); + objs.sort(function (obj0, obj1) { return compareByFieldSpecs(obj0, obj1, eventOrderSpecs); }); + return objs.map(function (c) { return c._seg; }); + } + // returns a object with all primitive props that can be compared + function buildSegCompareObj(seg) { + var eventRange = seg.eventRange; + var eventDef = eventRange.def; + var range = eventRange.instance ? eventRange.instance.range : eventRange.range; + var start = range.start ? range.start.valueOf() : 0; // TODO: better support for open-range events + var end = range.end ? range.end.valueOf() : 0; // " + return __assign(__assign(__assign({}, eventDef.extendedProps), eventDef), { id: eventDef.publicId, start: start, + end: end, duration: end - start, allDay: Number(eventDef.allDay), _seg: seg }); + } + function computeSegDraggable(seg, context) { + var pluginHooks = context.pluginHooks; + var transformers = pluginHooks.isDraggableTransformers; + var _a = seg.eventRange, def = _a.def, ui = _a.ui; + var val = ui.startEditable; + for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) { + var transformer = transformers_1[_i]; + val = transformer(val, def, ui, context); + } + return val; + } + function computeSegStartResizable(seg, context) { + return seg.isStart && seg.eventRange.ui.durationEditable && context.options.eventResizableFromStart; + } + function computeSegEndResizable(seg, context) { + return seg.isEnd && seg.eventRange.ui.durationEditable; + } + function buildSegTimeText(seg, timeFormat, context, defaultDisplayEventTime, // defaults to true + defaultDisplayEventEnd, // defaults to true + startOverride, endOverride) { + var dateEnv = context.dateEnv, options = context.options; + var displayEventTime = options.displayEventTime, displayEventEnd = options.displayEventEnd; + var eventDef = seg.eventRange.def; + var eventInstance = seg.eventRange.instance; + if (displayEventTime == null) { + displayEventTime = defaultDisplayEventTime !== false; + } + if (displayEventEnd == null) { + displayEventEnd = defaultDisplayEventEnd !== false; + } + var wholeEventStart = eventInstance.range.start; + var wholeEventEnd = eventInstance.range.end; + var segStart = startOverride || seg.start || seg.eventRange.range.start; + var segEnd = endOverride || seg.end || seg.eventRange.range.end; + var isStartDay = startOfDay(wholeEventStart).valueOf() === startOfDay(segStart).valueOf(); + var isEndDay = startOfDay(addMs(wholeEventEnd, -1)).valueOf() === startOfDay(addMs(segEnd, -1)).valueOf(); + if (displayEventTime && !eventDef.allDay && (isStartDay || isEndDay)) { + segStart = isStartDay ? wholeEventStart : segStart; + segEnd = isEndDay ? wholeEventEnd : segEnd; + if (displayEventEnd && eventDef.hasEnd) { + return dateEnv.formatRange(segStart, segEnd, timeFormat, { + forcedStartTzo: startOverride ? null : eventInstance.forcedStartTzo, + forcedEndTzo: endOverride ? null : eventInstance.forcedEndTzo, + }); + } + return dateEnv.format(segStart, timeFormat, { + forcedTzo: startOverride ? null : eventInstance.forcedStartTzo, // nooooo, same + }); + } + return ''; + } + function getSegMeta(seg, todayRange, nowDate) { + var segRange = seg.eventRange.range; + return { + isPast: segRange.end < (nowDate || todayRange.start), + isFuture: segRange.start >= (nowDate || todayRange.end), + isToday: todayRange && rangeContainsMarker(todayRange, segRange.start), + }; + } + function getEventClassNames(props) { + var classNames = ['fc-event']; + if (props.isMirror) { + classNames.push('fc-event-mirror'); + } + if (props.isDraggable) { + classNames.push('fc-event-draggable'); + } + if (props.isStartResizable || props.isEndResizable) { + classNames.push('fc-event-resizable'); + } + if (props.isDragging) { + classNames.push('fc-event-dragging'); + } + if (props.isResizing) { + classNames.push('fc-event-resizing'); + } + if (props.isSelected) { + classNames.push('fc-event-selected'); + } + if (props.isStart) { + classNames.push('fc-event-start'); + } + if (props.isEnd) { + classNames.push('fc-event-end'); + } + if (props.isPast) { + classNames.push('fc-event-past'); + } + if (props.isToday) { + classNames.push('fc-event-today'); + } + if (props.isFuture) { + classNames.push('fc-event-future'); + } + return classNames; + } + function buildEventRangeKey(eventRange) { + return eventRange.instance + ? eventRange.instance.instanceId + : eventRange.def.defId + ":" + eventRange.range.start.toISOString(); + // inverse-background events don't have specific instances. TODO: better solution + } + + var STANDARD_PROPS = { + start: identity, + end: identity, + allDay: Boolean, + }; + function parseDateSpan(raw, dateEnv, defaultDuration) { + var span = parseOpenDateSpan(raw, dateEnv); + var range = span.range; + if (!range.start) { + return null; + } + if (!range.end) { + if (defaultDuration == null) { + return null; + } + range.end = dateEnv.add(range.start, defaultDuration); + } + return span; + } + /* + TODO: somehow combine with parseRange? + Will return null if the start/end props were present but parsed invalidly. + */ + function parseOpenDateSpan(raw, dateEnv) { + var _a = refineProps(raw, STANDARD_PROPS), standardProps = _a.refined, extra = _a.extra; + var startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null; + var endMeta = standardProps.end ? dateEnv.createMarkerMeta(standardProps.end) : null; + var allDay = standardProps.allDay; + if (allDay == null) { + allDay = (startMeta && startMeta.isTimeUnspecified) && + (!endMeta || endMeta.isTimeUnspecified); + } + return __assign({ range: { + start: startMeta ? startMeta.marker : null, + end: endMeta ? endMeta.marker : null, + }, allDay: allDay }, extra); + } + function isDateSpansEqual(span0, span1) { + return rangesEqual(span0.range, span1.range) && + span0.allDay === span1.allDay && + isSpanPropsEqual(span0, span1); + } + // the NON-DATE-RELATED props + function isSpanPropsEqual(span0, span1) { + for (var propName in span1) { + if (propName !== 'range' && propName !== 'allDay') { + if (span0[propName] !== span1[propName]) { + return false; + } + } + } + // are there any props that span0 has that span1 DOESN'T have? + // both have range/allDay, so no need to special-case. + for (var propName in span0) { + if (!(propName in span1)) { + return false; + } + } + return true; + } + function buildDateSpanApi(span, dateEnv) { + return __assign(__assign({}, buildRangeApi(span.range, dateEnv, span.allDay)), { allDay: span.allDay }); + } + function buildRangeApiWithTimeZone(range, dateEnv, omitTime) { + return __assign(__assign({}, buildRangeApi(range, dateEnv, omitTime)), { timeZone: dateEnv.timeZone }); + } + function buildRangeApi(range, dateEnv, omitTime) { + return { + start: dateEnv.toDate(range.start), + end: dateEnv.toDate(range.end), + startStr: dateEnv.formatIso(range.start, { omitTime: omitTime }), + endStr: dateEnv.formatIso(range.end, { omitTime: omitTime }), + }; + } + function fabricateEventRange(dateSpan, eventUiBases, context) { + var res = refineEventDef({ editable: false }, context); + var def = parseEventDef(res.refined, res.extra, '', // sourceId + dateSpan.allDay, true, // hasEnd + context); + return { + def: def, + ui: compileEventUi(def, eventUiBases), + instance: createEventInstance(def.defId, dateSpan.range), + range: dateSpan.range, + isStart: true, + isEnd: true, + }; + } + + function triggerDateSelect(selection, pev, context) { + context.emitter.trigger('select', __assign(__assign({}, buildDateSpanApiWithContext(selection, context)), { jsEvent: pev ? pev.origEvent : null, view: context.viewApi || context.calendarApi.view })); + } + function triggerDateUnselect(pev, context) { + context.emitter.trigger('unselect', { + jsEvent: pev ? pev.origEvent : null, + view: context.viewApi || context.calendarApi.view, + }); + } + function buildDateSpanApiWithContext(dateSpan, context) { + var props = {}; + for (var _i = 0, _a = context.pluginHooks.dateSpanTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(props, transform(dateSpan, context)); + } + __assign(props, buildDateSpanApi(dateSpan, context.dateEnv)); + return props; + } + // Given an event's allDay status and start date, return what its fallback end date should be. + // TODO: rename to computeDefaultEventEnd + function getDefaultEventEnd(allDay, marker, context) { + var dateEnv = context.dateEnv, options = context.options; + var end = marker; + if (allDay) { + end = startOfDay(end); + end = dateEnv.add(end, options.defaultAllDayEventDuration); + } + else { + end = dateEnv.add(end, options.defaultTimedEventDuration); + } + return end; + } + + // applies the mutation to ALL defs/instances within the event store + function applyMutationToEventStore(eventStore, eventConfigBase, mutation, context) { + var eventConfigs = compileEventUis(eventStore.defs, eventConfigBase); + var dest = createEmptyEventStore(); + for (var defId in eventStore.defs) { + var def = eventStore.defs[defId]; + dest.defs[defId] = applyMutationToEventDef(def, eventConfigs[defId], mutation, context); + } + for (var instanceId in eventStore.instances) { + var instance = eventStore.instances[instanceId]; + var def = dest.defs[instance.defId]; // important to grab the newly modified def + dest.instances[instanceId] = applyMutationToEventInstance(instance, def, eventConfigs[instance.defId], mutation, context); + } + return dest; + } + function applyMutationToEventDef(eventDef, eventConfig, mutation, context) { + var standardProps = mutation.standardProps || {}; + // if hasEnd has not been specified, guess a good value based on deltas. + // if duration will change, there's no way the default duration will persist, + // and thus, we need to mark the event as having a real end + if (standardProps.hasEnd == null && + eventConfig.durationEditable && + (mutation.startDelta || mutation.endDelta)) { + standardProps.hasEnd = true; // TODO: is this mutation okay? + } + var copy = __assign(__assign(__assign({}, eventDef), standardProps), { ui: __assign(__assign({}, eventDef.ui), standardProps.ui) }); + if (mutation.extendedProps) { + copy.extendedProps = __assign(__assign({}, copy.extendedProps), mutation.extendedProps); + } + for (var _i = 0, _a = context.pluginHooks.eventDefMutationAppliers; _i < _a.length; _i++) { + var applier = _a[_i]; + applier(copy, mutation, context); + } + if (!copy.hasEnd && context.options.forceEventDuration) { + copy.hasEnd = true; + } + return copy; + } + function applyMutationToEventInstance(eventInstance, eventDef, // must first be modified by applyMutationToEventDef + eventConfig, mutation, context) { + var dateEnv = context.dateEnv; + var forceAllDay = mutation.standardProps && mutation.standardProps.allDay === true; + var clearEnd = mutation.standardProps && mutation.standardProps.hasEnd === false; + var copy = __assign({}, eventInstance); + if (forceAllDay) { + copy.range = computeAlignedDayRange(copy.range); + } + if (mutation.datesDelta && eventConfig.startEditable) { + copy.range = { + start: dateEnv.add(copy.range.start, mutation.datesDelta), + end: dateEnv.add(copy.range.end, mutation.datesDelta), + }; + } + if (mutation.startDelta && eventConfig.durationEditable) { + copy.range = { + start: dateEnv.add(copy.range.start, mutation.startDelta), + end: copy.range.end, + }; + } + if (mutation.endDelta && eventConfig.durationEditable) { + copy.range = { + start: copy.range.start, + end: dateEnv.add(copy.range.end, mutation.endDelta), + }; + } + if (clearEnd) { + copy.range = { + start: copy.range.start, + end: getDefaultEventEnd(eventDef.allDay, copy.range.start, context), + }; + } + // in case event was all-day but the supplied deltas were not + // better util for this? + if (eventDef.allDay) { + copy.range = { + start: startOfDay(copy.range.start), + end: startOfDay(copy.range.end), + }; + } + // handle invalid durations + if (copy.range.end < copy.range.start) { + copy.range.end = getDefaultEventEnd(eventDef.allDay, copy.range.start, context); + } + return copy; + } + + // no public types yet. when there are, export from: + // import {} from './api-type-deps' + var ViewApi = /** @class */ (function () { + function ViewApi(type, getCurrentData, dateEnv) { + this.type = type; + this.getCurrentData = getCurrentData; + this.dateEnv = dateEnv; + } + Object.defineProperty(ViewApi.prototype, "calendar", { + get: function () { + return this.getCurrentData().calendarApi; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(ViewApi.prototype, "title", { + get: function () { + return this.getCurrentData().viewTitle; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(ViewApi.prototype, "activeStart", { + get: function () { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.start); + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(ViewApi.prototype, "activeEnd", { + get: function () { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.end); + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(ViewApi.prototype, "currentStart", { + get: function () { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.start); + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(ViewApi.prototype, "currentEnd", { + get: function () { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.end); + }, + enumerable: false, + configurable: true + }); + ViewApi.prototype.getOption = function (name) { + return this.getCurrentData().options[name]; // are the view-specific options + }; + return ViewApi; + }()); + + var EVENT_SOURCE_REFINERS$1 = { + id: String, + defaultAllDay: Boolean, + url: String, + format: String, + events: identity, + eventDataTransform: identity, + // for any network-related sources + success: identity, + failure: identity, + }; + function parseEventSource(raw, context, refiners) { + if (refiners === void 0) { refiners = buildEventSourceRefiners(context); } + var rawObj; + if (typeof raw === 'string') { + rawObj = { url: raw }; + } + else if (typeof raw === 'function' || Array.isArray(raw)) { + rawObj = { events: raw }; + } + else if (typeof raw === 'object' && raw) { // not null + rawObj = raw; + } + if (rawObj) { + var _a = refineProps(rawObj, refiners), refined = _a.refined, extra = _a.extra; + var metaRes = buildEventSourceMeta(refined, context); + if (metaRes) { + return { + _raw: raw, + isFetching: false, + latestFetchId: '', + fetchRange: null, + defaultAllDay: refined.defaultAllDay, + eventDataTransform: refined.eventDataTransform, + success: refined.success, + failure: refined.failure, + publicId: refined.id || '', + sourceId: guid(), + sourceDefId: metaRes.sourceDefId, + meta: metaRes.meta, + ui: createEventUi(refined, context), + extendedProps: extra, + }; + } + } + return null; + } + function buildEventSourceRefiners(context) { + return __assign(__assign(__assign({}, EVENT_UI_REFINERS), EVENT_SOURCE_REFINERS$1), context.pluginHooks.eventSourceRefiners); + } + function buildEventSourceMeta(raw, context) { + var defs = context.pluginHooks.eventSourceDefs; + for (var i = defs.length - 1; i >= 0; i -= 1) { // later-added plugins take precedence + var def = defs[i]; + var meta = def.parseMeta(raw); + if (meta) { + return { sourceDefId: i, meta: meta }; + } + } + return null; + } + + function reduceCurrentDate(currentDate, action) { + switch (action.type) { + case 'CHANGE_DATE': + return action.dateMarker; + default: + return currentDate; + } + } + function getInitialDate(options, dateEnv) { + var initialDateInput = options.initialDate; + // compute the initial ambig-timezone date + if (initialDateInput != null) { + return dateEnv.createMarker(initialDateInput); + } + return getNow(options.now, dateEnv); // getNow already returns unzoned + } + function getNow(nowInput, dateEnv) { + if (typeof nowInput === 'function') { + nowInput = nowInput(); + } + if (nowInput == null) { + return dateEnv.createNowMarker(); + } + return dateEnv.createMarker(nowInput); + } + + var CalendarApi = /** @class */ (function () { + function CalendarApi() { + } + CalendarApi.prototype.getCurrentData = function () { + return this.currentDataManager.getCurrentData(); + }; + CalendarApi.prototype.dispatch = function (action) { + return this.currentDataManager.dispatch(action); + }; + Object.defineProperty(CalendarApi.prototype, "view", { + get: function () { return this.getCurrentData().viewApi; } // for public API + , + enumerable: false, + configurable: true + }); + CalendarApi.prototype.batchRendering = function (callback) { + callback(); + }; + CalendarApi.prototype.updateSize = function () { + this.trigger('_resize', true); + }; + // Options + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.setOption = function (name, val) { + this.dispatch({ + type: 'SET_OPTION', + optionName: name, + rawOptionValue: val, + }); + }; + CalendarApi.prototype.getOption = function (name) { + return this.currentDataManager.currentCalendarOptionsInput[name]; + }; + CalendarApi.prototype.getAvailableLocaleCodes = function () { + return Object.keys(this.getCurrentData().availableRawLocales); + }; + // Trigger + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.on = function (handlerName, handler) { + var currentDataManager = this.currentDataManager; + if (currentDataManager.currentCalendarOptionsRefiners[handlerName]) { + currentDataManager.emitter.on(handlerName, handler); + } + else { + console.warn("Unknown listener name '" + handlerName + "'"); + } + }; + CalendarApi.prototype.off = function (handlerName, handler) { + this.currentDataManager.emitter.off(handlerName, handler); + }; + // not meant for public use + CalendarApi.prototype.trigger = function (handlerName) { + var _a; + var args = []; + for (var _i = 1; _i < arguments.length; _i++) { + args[_i - 1] = arguments[_i]; + } + (_a = this.currentDataManager.emitter).trigger.apply(_a, __spreadArray([handlerName], args)); + }; + // View + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.changeView = function (viewType, dateOrRange) { + var _this = this; + this.batchRendering(function () { + _this.unselect(); + if (dateOrRange) { + if (dateOrRange.start && dateOrRange.end) { // a range + _this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType: viewType, + }); + _this.dispatch({ + type: 'SET_OPTION', + optionName: 'visibleRange', + rawOptionValue: dateOrRange, + }); + } + else { + var dateEnv = _this.getCurrentData().dateEnv; + _this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType: viewType, + dateMarker: dateEnv.createMarker(dateOrRange), + }); + } + } + else { + _this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType: viewType, + }); + } + }); + }; + // Forces navigation to a view for the given date. + // `viewType` can be a specific view name or a generic one like "week" or "day". + // needs to change + CalendarApi.prototype.zoomTo = function (dateMarker, viewType) { + var state = this.getCurrentData(); + var spec; + viewType = viewType || 'day'; // day is default zoom + spec = state.viewSpecs[viewType] || this.getUnitViewSpec(viewType); + this.unselect(); + if (spec) { + this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType: spec.type, + dateMarker: dateMarker, + }); + } + else { + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: dateMarker, + }); + } + }; + // Given a duration singular unit, like "week" or "day", finds a matching view spec. + // Preference is given to views that have corresponding buttons. + CalendarApi.prototype.getUnitViewSpec = function (unit) { + var _a = this.getCurrentData(), viewSpecs = _a.viewSpecs, toolbarConfig = _a.toolbarConfig; + var viewTypes = [].concat(toolbarConfig.viewsWithButtons); + var i; + var spec; + for (var viewType in viewSpecs) { + viewTypes.push(viewType); + } + for (i = 0; i < viewTypes.length; i += 1) { + spec = viewSpecs[viewTypes[i]]; + if (spec) { + if (spec.singleUnit === unit) { + return spec; + } + } + } + return null; + }; + // Current Date + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.prev = function () { + this.unselect(); + this.dispatch({ type: 'PREV' }); + }; + CalendarApi.prototype.next = function () { + this.unselect(); + this.dispatch({ type: 'NEXT' }); + }; + CalendarApi.prototype.prevYear = function () { + var state = this.getCurrentData(); + this.unselect(); + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.addYears(state.currentDate, -1), + }); + }; + CalendarApi.prototype.nextYear = function () { + var state = this.getCurrentData(); + this.unselect(); + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.addYears(state.currentDate, 1), + }); + }; + CalendarApi.prototype.today = function () { + var state = this.getCurrentData(); + this.unselect(); + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: getNow(state.calendarOptions.now, state.dateEnv), + }); + }; + CalendarApi.prototype.gotoDate = function (zonedDateInput) { + var state = this.getCurrentData(); + this.unselect(); + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.createMarker(zonedDateInput), + }); + }; + CalendarApi.prototype.incrementDate = function (deltaInput) { + var state = this.getCurrentData(); + var delta = createDuration(deltaInput); + if (delta) { // else, warn about invalid input? + this.unselect(); + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.add(state.currentDate, delta), + }); + } + }; + // for external API + CalendarApi.prototype.getDate = function () { + var state = this.getCurrentData(); + return state.dateEnv.toDate(state.currentDate); + }; + // Date Formatting Utils + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.formatDate = function (d, formatter) { + var dateEnv = this.getCurrentData().dateEnv; + return dateEnv.format(dateEnv.createMarker(d), createFormatter(formatter)); + }; + // `settings` is for formatter AND isEndExclusive + CalendarApi.prototype.formatRange = function (d0, d1, settings) { + var dateEnv = this.getCurrentData().dateEnv; + return dateEnv.formatRange(dateEnv.createMarker(d0), dateEnv.createMarker(d1), createFormatter(settings), settings); + }; + CalendarApi.prototype.formatIso = function (d, omitTime) { + var dateEnv = this.getCurrentData().dateEnv; + return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime: omitTime }); + }; + // Date Selection / Event Selection / DayClick + // ----------------------------------------------------------------------------------------------------------------- + // this public method receives start/end dates in any format, with any timezone + // NOTE: args were changed from v3 + CalendarApi.prototype.select = function (dateOrObj, endDate) { + var selectionInput; + if (endDate == null) { + if (dateOrObj.start != null) { + selectionInput = dateOrObj; + } + else { + selectionInput = { + start: dateOrObj, + end: null, + }; + } + } + else { + selectionInput = { + start: dateOrObj, + end: endDate, + }; + } + var state = this.getCurrentData(); + var selection = parseDateSpan(selectionInput, state.dateEnv, createDuration({ days: 1 })); + if (selection) { // throw parse error otherwise? + this.dispatch({ type: 'SELECT_DATES', selection: selection }); + triggerDateSelect(selection, null, state); + } + }; + // public method + CalendarApi.prototype.unselect = function (pev) { + var state = this.getCurrentData(); + if (state.dateSelection) { + this.dispatch({ type: 'UNSELECT_DATES' }); + triggerDateUnselect(pev, state); + } + }; + // Public Events API + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.addEvent = function (eventInput, sourceInput) { + if (eventInput instanceof EventApi) { + var def = eventInput._def; + var instance = eventInput._instance; + var currentData = this.getCurrentData(); + // not already present? don't want to add an old snapshot + if (!currentData.eventStore.defs[def.defId]) { + this.dispatch({ + type: 'ADD_EVENTS', + eventStore: eventTupleToStore({ def: def, instance: instance }), // TODO: better util for two args? + }); + this.triggerEventAdd(eventInput); + } + return eventInput; + } + var state = this.getCurrentData(); + var eventSource; + if (sourceInput instanceof EventSourceApi) { + eventSource = sourceInput.internalEventSource; + } + else if (typeof sourceInput === 'boolean') { + if (sourceInput) { // true. part of the first event source + eventSource = hashValuesToArray(state.eventSources)[0]; + } + } + else if (sourceInput != null) { // an ID. accepts a number too + var sourceApi = this.getEventSourceById(sourceInput); // TODO: use an internal function + if (!sourceApi) { + console.warn("Could not find an event source with ID \"" + sourceInput + "\""); // TODO: test + return null; + } + eventSource = sourceApi.internalEventSource; + } + var tuple = parseEvent(eventInput, eventSource, state, false); + if (tuple) { + var newEventApi = new EventApi(state, tuple.def, tuple.def.recurringDef ? null : tuple.instance); + this.dispatch({ + type: 'ADD_EVENTS', + eventStore: eventTupleToStore(tuple), + }); + this.triggerEventAdd(newEventApi); + return newEventApi; + } + return null; + }; + CalendarApi.prototype.triggerEventAdd = function (eventApi) { + var _this = this; + var emitter = this.getCurrentData().emitter; + emitter.trigger('eventAdd', { + event: eventApi, + relatedEvents: [], + revert: function () { + _this.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: eventApiToStore(eventApi), + }); + }, + }); + }; + // TODO: optimize + CalendarApi.prototype.getEventById = function (id) { + var state = this.getCurrentData(); + var _a = state.eventStore, defs = _a.defs, instances = _a.instances; + id = String(id); + for (var defId in defs) { + var def = defs[defId]; + if (def.publicId === id) { + if (def.recurringDef) { + return new EventApi(state, def, null); + } + for (var instanceId in instances) { + var instance = instances[instanceId]; + if (instance.defId === def.defId) { + return new EventApi(state, def, instance); + } + } + } + } + return null; + }; + CalendarApi.prototype.getEvents = function () { + var currentData = this.getCurrentData(); + return buildEventApis(currentData.eventStore, currentData); + }; + CalendarApi.prototype.removeAllEvents = function () { + this.dispatch({ type: 'REMOVE_ALL_EVENTS' }); + }; + // Public Event Sources API + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.getEventSources = function () { + var state = this.getCurrentData(); + var sourceHash = state.eventSources; + var sourceApis = []; + for (var internalId in sourceHash) { + sourceApis.push(new EventSourceApi(state, sourceHash[internalId])); + } + return sourceApis; + }; + CalendarApi.prototype.getEventSourceById = function (id) { + var state = this.getCurrentData(); + var sourceHash = state.eventSources; + id = String(id); + for (var sourceId in sourceHash) { + if (sourceHash[sourceId].publicId === id) { + return new EventSourceApi(state, sourceHash[sourceId]); + } + } + return null; + }; + CalendarApi.prototype.addEventSource = function (sourceInput) { + var state = this.getCurrentData(); + if (sourceInput instanceof EventSourceApi) { + // not already present? don't want to add an old snapshot + if (!state.eventSources[sourceInput.internalEventSource.sourceId]) { + this.dispatch({ + type: 'ADD_EVENT_SOURCES', + sources: [sourceInput.internalEventSource], + }); + } + return sourceInput; + } + var eventSource = parseEventSource(sourceInput, state); + if (eventSource) { // TODO: error otherwise? + this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] }); + return new EventSourceApi(state, eventSource); + } + return null; + }; + CalendarApi.prototype.removeAllEventSources = function () { + this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' }); + }; + CalendarApi.prototype.refetchEvents = function () { + this.dispatch({ type: 'FETCH_EVENT_SOURCES', isRefetch: true }); + }; + // Scroll + // ----------------------------------------------------------------------------------------------------------------- + CalendarApi.prototype.scrollToTime = function (timeInput) { + var time = createDuration(timeInput); + if (time) { + this.trigger('_scrollRequest', { time: time }); + } + }; + return CalendarApi; + }()); + + var EventApi = /** @class */ (function () { + // instance will be null if expressing a recurring event that has no current instances, + // OR if trying to validate an incoming external event that has no dates assigned + function EventApi(context, def, instance) { + this._context = context; + this._def = def; + this._instance = instance || null; + } + /* + TODO: make event struct more responsible for this + */ + EventApi.prototype.setProp = function (name, val) { + var _a, _b; + if (name in EVENT_DATE_REFINERS) { + console.warn('Could not set date-related prop \'name\'. Use one of the date-related methods instead.'); + // TODO: make proper aliasing system? + } + else if (name === 'id') { + val = EVENT_NON_DATE_REFINERS[name](val); + this.mutate({ + standardProps: { publicId: val }, // hardcoded internal name + }); + } + else if (name in EVENT_NON_DATE_REFINERS) { + val = EVENT_NON_DATE_REFINERS[name](val); + this.mutate({ + standardProps: (_a = {}, _a[name] = val, _a), + }); + } + else if (name in EVENT_UI_REFINERS) { + var ui = EVENT_UI_REFINERS[name](val); + if (name === 'color') { + ui = { backgroundColor: val, borderColor: val }; + } + else if (name === 'editable') { + ui = { startEditable: val, durationEditable: val }; + } + else { + ui = (_b = {}, _b[name] = val, _b); + } + this.mutate({ + standardProps: { ui: ui }, + }); + } + else { + console.warn("Could not set prop '" + name + "'. Use setExtendedProp instead."); + } + }; + EventApi.prototype.setExtendedProp = function (name, val) { + var _a; + this.mutate({ + extendedProps: (_a = {}, _a[name] = val, _a), + }); + }; + EventApi.prototype.setStart = function (startInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = this._context.dateEnv; + var start = dateEnv.createMarker(startInput); + if (start && this._instance) { // TODO: warning if parsed bad + var instanceRange = this._instance.range; + var startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity); // what if parsed bad!? + if (options.maintainDuration) { + this.mutate({ datesDelta: startDelta }); + } + else { + this.mutate({ startDelta: startDelta }); + } + } + }; + EventApi.prototype.setEnd = function (endInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = this._context.dateEnv; + var end; + if (endInput != null) { + end = dateEnv.createMarker(endInput); + if (!end) { + return; // TODO: warning if parsed bad + } + } + if (this._instance) { + if (end) { + var endDelta = diffDates(this._instance.range.end, end, dateEnv, options.granularity); + this.mutate({ endDelta: endDelta }); + } + else { + this.mutate({ standardProps: { hasEnd: false } }); + } + } + }; + EventApi.prototype.setDates = function (startInput, endInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = this._context.dateEnv; + var standardProps = { allDay: options.allDay }; + var start = dateEnv.createMarker(startInput); + var end; + if (!start) { + return; // TODO: warning if parsed bad + } + if (endInput != null) { + end = dateEnv.createMarker(endInput); + if (!end) { // TODO: warning if parsed bad + return; + } + } + if (this._instance) { + var instanceRange = this._instance.range; + // when computing the diff for an event being converted to all-day, + // compute diff off of the all-day values the way event-mutation does. + if (options.allDay === true) { + instanceRange = computeAlignedDayRange(instanceRange); + } + var startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity); + if (end) { + var endDelta = diffDates(instanceRange.end, end, dateEnv, options.granularity); + if (durationsEqual(startDelta, endDelta)) { + this.mutate({ datesDelta: startDelta, standardProps: standardProps }); + } + else { + this.mutate({ startDelta: startDelta, endDelta: endDelta, standardProps: standardProps }); + } + } + else { // means "clear the end" + standardProps.hasEnd = false; + this.mutate({ datesDelta: startDelta, standardProps: standardProps }); + } + } + }; + EventApi.prototype.moveStart = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // TODO: warning if parsed bad + this.mutate({ startDelta: delta }); + } + }; + EventApi.prototype.moveEnd = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // TODO: warning if parsed bad + this.mutate({ endDelta: delta }); + } + }; + EventApi.prototype.moveDates = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // TODO: warning if parsed bad + this.mutate({ datesDelta: delta }); + } + }; + EventApi.prototype.setAllDay = function (allDay, options) { + if (options === void 0) { options = {}; } + var standardProps = { allDay: allDay }; + var maintainDuration = options.maintainDuration; + if (maintainDuration == null) { + maintainDuration = this._context.options.allDayMaintainDuration; + } + if (this._def.allDay !== allDay) { + standardProps.hasEnd = maintainDuration; + } + this.mutate({ standardProps: standardProps }); + }; + EventApi.prototype.formatRange = function (formatInput) { + var dateEnv = this._context.dateEnv; + var instance = this._instance; + var formatter = createFormatter(formatInput); + if (this._def.hasEnd) { + return dateEnv.formatRange(instance.range.start, instance.range.end, formatter, { + forcedStartTzo: instance.forcedStartTzo, + forcedEndTzo: instance.forcedEndTzo, + }); + } + return dateEnv.format(instance.range.start, formatter, { + forcedTzo: instance.forcedStartTzo, + }); + }; + EventApi.prototype.mutate = function (mutation) { + var instance = this._instance; + if (instance) { + var def = this._def; + var context_1 = this._context; + var eventStore_1 = context_1.getCurrentData().eventStore; + var relevantEvents = getRelevantEvents(eventStore_1, instance.instanceId); + var eventConfigBase = { + '': { + display: '', + startEditable: true, + durationEditable: true, + constraints: [], + overlap: null, + allows: [], + backgroundColor: '', + borderColor: '', + textColor: '', + classNames: [], + }, + }; + relevantEvents = applyMutationToEventStore(relevantEvents, eventConfigBase, mutation, context_1); + var oldEvent = new EventApi(context_1, def, instance); // snapshot + this._def = relevantEvents.defs[def.defId]; + this._instance = relevantEvents.instances[instance.instanceId]; + context_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents, + }); + context_1.emitter.trigger('eventChange', { + oldEvent: oldEvent, + event: this, + relatedEvents: buildEventApis(relevantEvents, context_1, instance), + revert: function () { + context_1.dispatch({ + type: 'RESET_EVENTS', + eventStore: eventStore_1, + }); + }, + }); + } + }; + EventApi.prototype.remove = function () { + var context = this._context; + var asStore = eventApiToStore(this); + context.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: asStore, + }); + context.emitter.trigger('eventRemove', { + event: this, + relatedEvents: [], + revert: function () { + context.dispatch({ + type: 'MERGE_EVENTS', + eventStore: asStore, + }); + }, + }); + }; + Object.defineProperty(EventApi.prototype, "source", { + get: function () { + var sourceId = this._def.sourceId; + if (sourceId) { + return new EventSourceApi(this._context, this._context.getCurrentData().eventSources[sourceId]); + } + return null; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "start", { + get: function () { + return this._instance ? + this._context.dateEnv.toDate(this._instance.range.start) : + null; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "end", { + get: function () { + return (this._instance && this._def.hasEnd) ? + this._context.dateEnv.toDate(this._instance.range.end) : + null; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "startStr", { + get: function () { + var instance = this._instance; + if (instance) { + return this._context.dateEnv.formatIso(instance.range.start, { + omitTime: this._def.allDay, + forcedTzo: instance.forcedStartTzo, + }); + } + return ''; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "endStr", { + get: function () { + var instance = this._instance; + if (instance && this._def.hasEnd) { + return this._context.dateEnv.formatIso(instance.range.end, { + omitTime: this._def.allDay, + forcedTzo: instance.forcedEndTzo, + }); + } + return ''; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "id", { + // computable props that all access the def + // TODO: find a TypeScript-compatible way to do this at scale + get: function () { return this._def.publicId; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "groupId", { + get: function () { return this._def.groupId; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "allDay", { + get: function () { return this._def.allDay; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "title", { + get: function () { return this._def.title; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "url", { + get: function () { return this._def.url; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "display", { + get: function () { return this._def.ui.display || 'auto'; } // bad. just normalize the type earlier + , + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "startEditable", { + get: function () { return this._def.ui.startEditable; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "durationEditable", { + get: function () { return this._def.ui.durationEditable; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "constraint", { + get: function () { return this._def.ui.constraints[0] || null; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "overlap", { + get: function () { return this._def.ui.overlap; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "allow", { + get: function () { return this._def.ui.allows[0] || null; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "backgroundColor", { + get: function () { return this._def.ui.backgroundColor; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "borderColor", { + get: function () { return this._def.ui.borderColor; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "textColor", { + get: function () { return this._def.ui.textColor; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "classNames", { + // NOTE: user can't modify these because Object.freeze was called in event-def parsing + get: function () { return this._def.ui.classNames; }, + enumerable: false, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "extendedProps", { + get: function () { return this._def.extendedProps; }, + enumerable: false, + configurable: true + }); + EventApi.prototype.toPlainObject = function (settings) { + if (settings === void 0) { settings = {}; } + var def = this._def; + var ui = def.ui; + var _a = this, startStr = _a.startStr, endStr = _a.endStr; + var res = {}; + if (def.title) { + res.title = def.title; + } + if (startStr) { + res.start = startStr; + } + if (endStr) { + res.end = endStr; + } + if (def.publicId) { + res.id = def.publicId; + } + if (def.groupId) { + res.groupId = def.groupId; + } + if (def.url) { + res.url = def.url; + } + if (ui.display && ui.display !== 'auto') { + res.display = ui.display; + } + // TODO: what about recurring-event properties??? + // TODO: include startEditable/durationEditable/constraint/overlap/allow + if (settings.collapseColor && ui.backgroundColor && ui.backgroundColor === ui.borderColor) { + res.color = ui.backgroundColor; + } + else { + if (ui.backgroundColor) { + res.backgroundColor = ui.backgroundColor; + } + if (ui.borderColor) { + res.borderColor = ui.borderColor; + } + } + if (ui.textColor) { + res.textColor = ui.textColor; + } + if (ui.classNames.length) { + res.classNames = ui.classNames; + } + if (Object.keys(def.extendedProps).length) { + if (settings.collapseExtendedProps) { + __assign(res, def.extendedProps); + } + else { + res.extendedProps = def.extendedProps; + } + } + return res; + }; + EventApi.prototype.toJSON = function () { + return this.toPlainObject(); + }; + return EventApi; + }()); + function eventApiToStore(eventApi) { + var _a, _b; + var def = eventApi._def; + var instance = eventApi._instance; + return { + defs: (_a = {}, _a[def.defId] = def, _a), + instances: instance + ? (_b = {}, _b[instance.instanceId] = instance, _b) : {}, + }; + } + function buildEventApis(eventStore, context, excludeInstance) { + var defs = eventStore.defs, instances = eventStore.instances; + var eventApis = []; + var excludeInstanceId = excludeInstance ? excludeInstance.instanceId : ''; + for (var id in instances) { + var instance = instances[id]; + var def = defs[instance.defId]; + if (instance.instanceId !== excludeInstanceId) { + eventApis.push(new EventApi(context, def, instance)); + } + } + return eventApis; + } + + var calendarSystemClassMap = {}; + function registerCalendarSystem(name, theClass) { + calendarSystemClassMap[name] = theClass; + } + function createCalendarSystem(name) { + return new calendarSystemClassMap[name](); + } + var GregorianCalendarSystem = /** @class */ (function () { + function GregorianCalendarSystem() { + } + GregorianCalendarSystem.prototype.getMarkerYear = function (d) { + return d.getUTCFullYear(); + }; + GregorianCalendarSystem.prototype.getMarkerMonth = function (d) { + return d.getUTCMonth(); + }; + GregorianCalendarSystem.prototype.getMarkerDay = function (d) { + return d.getUTCDate(); + }; + GregorianCalendarSystem.prototype.arrayToMarker = function (arr) { + return arrayToUtcDate(arr); + }; + GregorianCalendarSystem.prototype.markerToArray = function (marker) { + return dateToUtcArray(marker); + }; + return GregorianCalendarSystem; + }()); + registerCalendarSystem('gregory', GregorianCalendarSystem); + + var ISO_RE = /^\s*(\d{4})(-?(\d{2})(-?(\d{2})([T ](\d{2}):?(\d{2})(:?(\d{2})(\.(\d+))?)?(Z|(([-+])(\d{2})(:?(\d{2}))?))?)?)?)?$/; + function parse(str) { + var m = ISO_RE.exec(str); + if (m) { + var marker = new Date(Date.UTC(Number(m[1]), m[3] ? Number(m[3]) - 1 : 0, Number(m[5] || 1), Number(m[7] || 0), Number(m[8] || 0), Number(m[10] || 0), m[12] ? Number("0." + m[12]) * 1000 : 0)); + if (isValidDate(marker)) { + var timeZoneOffset = null; + if (m[13]) { + timeZoneOffset = (m[15] === '-' ? -1 : 1) * (Number(m[16] || 0) * 60 + + Number(m[18] || 0)); + } + return { + marker: marker, + isTimeUnspecified: !m[6], + timeZoneOffset: timeZoneOffset, + }; + } + } + return null; + } + + var DateEnv = /** @class */ (function () { + function DateEnv(settings) { + var timeZone = this.timeZone = settings.timeZone; + var isNamedTimeZone = timeZone !== 'local' && timeZone !== 'UTC'; + if (settings.namedTimeZoneImpl && isNamedTimeZone) { + this.namedTimeZoneImpl = new settings.namedTimeZoneImpl(timeZone); + } + this.canComputeOffset = Boolean(!isNamedTimeZone || this.namedTimeZoneImpl); + this.calendarSystem = createCalendarSystem(settings.calendarSystem); + this.locale = settings.locale; + this.weekDow = settings.locale.week.dow; + this.weekDoy = settings.locale.week.doy; + if (settings.weekNumberCalculation === 'ISO') { + this.weekDow = 1; + this.weekDoy = 4; + } + if (typeof settings.firstDay === 'number') { + this.weekDow = settings.firstDay; + } + if (typeof settings.weekNumberCalculation === 'function') { + this.weekNumberFunc = settings.weekNumberCalculation; + } + this.weekText = settings.weekText != null ? settings.weekText : settings.locale.options.weekText; + this.cmdFormatter = settings.cmdFormatter; + this.defaultSeparator = settings.defaultSeparator; + } + // Creating / Parsing + DateEnv.prototype.createMarker = function (input) { + var meta = this.createMarkerMeta(input); + if (meta === null) { + return null; + } + return meta.marker; + }; + DateEnv.prototype.createNowMarker = function () { + if (this.canComputeOffset) { + return this.timestampToMarker(new Date().valueOf()); + } + // if we can't compute the current date val for a timezone, + // better to give the current local date vals than UTC + return arrayToUtcDate(dateToLocalArray(new Date())); + }; + DateEnv.prototype.createMarkerMeta = function (input) { + if (typeof input === 'string') { + return this.parse(input); + } + var marker = null; + if (typeof input === 'number') { + marker = this.timestampToMarker(input); + } + else if (input instanceof Date) { + input = input.valueOf(); + if (!isNaN(input)) { + marker = this.timestampToMarker(input); + } + } + else if (Array.isArray(input)) { + marker = arrayToUtcDate(input); + } + if (marker === null || !isValidDate(marker)) { + return null; + } + return { marker: marker, isTimeUnspecified: false, forcedTzo: null }; + }; + DateEnv.prototype.parse = function (s) { + var parts = parse(s); + if (parts === null) { + return null; + } + var marker = parts.marker; + var forcedTzo = null; + if (parts.timeZoneOffset !== null) { + if (this.canComputeOffset) { + marker = this.timestampToMarker(marker.valueOf() - parts.timeZoneOffset * 60 * 1000); + } + else { + forcedTzo = parts.timeZoneOffset; + } + } + return { marker: marker, isTimeUnspecified: parts.isTimeUnspecified, forcedTzo: forcedTzo }; + }; + // Accessors + DateEnv.prototype.getYear = function (marker) { + return this.calendarSystem.getMarkerYear(marker); + }; + DateEnv.prototype.getMonth = function (marker) { + return this.calendarSystem.getMarkerMonth(marker); + }; + // Adding / Subtracting + DateEnv.prototype.add = function (marker, dur) { + var a = this.calendarSystem.markerToArray(marker); + a[0] += dur.years; + a[1] += dur.months; + a[2] += dur.days; + a[6] += dur.milliseconds; + return this.calendarSystem.arrayToMarker(a); + }; + DateEnv.prototype.subtract = function (marker, dur) { + var a = this.calendarSystem.markerToArray(marker); + a[0] -= dur.years; + a[1] -= dur.months; + a[2] -= dur.days; + a[6] -= dur.milliseconds; + return this.calendarSystem.arrayToMarker(a); + }; + DateEnv.prototype.addYears = function (marker, n) { + var a = this.calendarSystem.markerToArray(marker); + a[0] += n; + return this.calendarSystem.arrayToMarker(a); + }; + DateEnv.prototype.addMonths = function (marker, n) { + var a = this.calendarSystem.markerToArray(marker); + a[1] += n; + return this.calendarSystem.arrayToMarker(a); + }; + // Diffing Whole Units + DateEnv.prototype.diffWholeYears = function (m0, m1) { + var calendarSystem = this.calendarSystem; + if (timeAsMs(m0) === timeAsMs(m1) && + calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1) && + calendarSystem.getMarkerMonth(m0) === calendarSystem.getMarkerMonth(m1)) { + return calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0); + } + return null; + }; + DateEnv.prototype.diffWholeMonths = function (m0, m1) { + var calendarSystem = this.calendarSystem; + if (timeAsMs(m0) === timeAsMs(m1) && + calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1)) { + return (calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0)) + + (calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12; + } + return null; + }; + // Range / Duration + DateEnv.prototype.greatestWholeUnit = function (m0, m1) { + var n = this.diffWholeYears(m0, m1); + if (n !== null) { + return { unit: 'year', value: n }; + } + n = this.diffWholeMonths(m0, m1); + if (n !== null) { + return { unit: 'month', value: n }; + } + n = diffWholeWeeks(m0, m1); + if (n !== null) { + return { unit: 'week', value: n }; + } + n = diffWholeDays(m0, m1); + if (n !== null) { + return { unit: 'day', value: n }; + } + n = diffHours(m0, m1); + if (isInt(n)) { + return { unit: 'hour', value: n }; + } + n = diffMinutes(m0, m1); + if (isInt(n)) { + return { unit: 'minute', value: n }; + } + n = diffSeconds(m0, m1); + if (isInt(n)) { + return { unit: 'second', value: n }; + } + return { unit: 'millisecond', value: m1.valueOf() - m0.valueOf() }; + }; + DateEnv.prototype.countDurationsBetween = function (m0, m1, d) { + // TODO: can use greatestWholeUnit + var diff; + if (d.years) { + diff = this.diffWholeYears(m0, m1); + if (diff !== null) { + return diff / asRoughYears(d); + } + } + if (d.months) { + diff = this.diffWholeMonths(m0, m1); + if (diff !== null) { + return diff / asRoughMonths(d); + } + } + if (d.days) { + diff = diffWholeDays(m0, m1); + if (diff !== null) { + return diff / asRoughDays(d); + } + } + return (m1.valueOf() - m0.valueOf()) / asRoughMs(d); + }; + // Start-Of + // these DON'T return zoned-dates. only UTC start-of dates + DateEnv.prototype.startOf = function (m, unit) { + if (unit === 'year') { + return this.startOfYear(m); + } + if (unit === 'month') { + return this.startOfMonth(m); + } + if (unit === 'week') { + return this.startOfWeek(m); + } + if (unit === 'day') { + return startOfDay(m); + } + if (unit === 'hour') { + return startOfHour(m); + } + if (unit === 'minute') { + return startOfMinute(m); + } + if (unit === 'second') { + return startOfSecond(m); + } + return null; + }; + DateEnv.prototype.startOfYear = function (m) { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + ]); + }; + DateEnv.prototype.startOfMonth = function (m) { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + this.calendarSystem.getMarkerMonth(m), + ]); + }; + DateEnv.prototype.startOfWeek = function (m) { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + this.calendarSystem.getMarkerMonth(m), + m.getUTCDate() - ((m.getUTCDay() - this.weekDow + 7) % 7), + ]); + }; + // Week Number + DateEnv.prototype.computeWeekNumber = function (marker) { + if (this.weekNumberFunc) { + return this.weekNumberFunc(this.toDate(marker)); + } + return weekOfYear(marker, this.weekDow, this.weekDoy); + }; + // TODO: choke on timeZoneName: long + DateEnv.prototype.format = function (marker, formatter, dateOptions) { + if (dateOptions === void 0) { dateOptions = {}; } + return formatter.format({ + marker: marker, + timeZoneOffset: dateOptions.forcedTzo != null ? + dateOptions.forcedTzo : + this.offsetForMarker(marker), + }, this); + }; + DateEnv.prototype.formatRange = function (start, end, formatter, dateOptions) { + if (dateOptions === void 0) { dateOptions = {}; } + if (dateOptions.isEndExclusive) { + end = addMs(end, -1); + } + return formatter.formatRange({ + marker: start, + timeZoneOffset: dateOptions.forcedStartTzo != null ? + dateOptions.forcedStartTzo : + this.offsetForMarker(start), + }, { + marker: end, + timeZoneOffset: dateOptions.forcedEndTzo != null ? + dateOptions.forcedEndTzo : + this.offsetForMarker(end), + }, this, dateOptions.defaultSeparator); + }; + /* + DUMB: the omitTime arg is dumb. if we omit the time, we want to omit the timezone offset. and if we do that, + might as well use buildIsoString or some other util directly + */ + DateEnv.prototype.formatIso = function (marker, extraOptions) { + if (extraOptions === void 0) { extraOptions = {}; } + var timeZoneOffset = null; + if (!extraOptions.omitTimeZoneOffset) { + if (extraOptions.forcedTzo != null) { + timeZoneOffset = extraOptions.forcedTzo; + } + else { + timeZoneOffset = this.offsetForMarker(marker); + } + } + return buildIsoString(marker, timeZoneOffset, extraOptions.omitTime); + }; + // TimeZone + DateEnv.prototype.timestampToMarker = function (ms) { + if (this.timeZone === 'local') { + return arrayToUtcDate(dateToLocalArray(new Date(ms))); + } + if (this.timeZone === 'UTC' || !this.namedTimeZoneImpl) { + return new Date(ms); + } + return arrayToUtcDate(this.namedTimeZoneImpl.timestampToArray(ms)); + }; + DateEnv.prototype.offsetForMarker = function (m) { + if (this.timeZone === 'local') { + return -arrayToLocalDate(dateToUtcArray(m)).getTimezoneOffset(); // convert "inverse" offset to "normal" offset + } + if (this.timeZone === 'UTC') { + return 0; + } + if (this.namedTimeZoneImpl) { + return this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)); + } + return null; + }; + // Conversion + DateEnv.prototype.toDate = function (m, forcedTzo) { + if (this.timeZone === 'local') { + return arrayToLocalDate(dateToUtcArray(m)); + } + if (this.timeZone === 'UTC') { + return new Date(m.valueOf()); // make sure it's a copy + } + if (!this.namedTimeZoneImpl) { + return new Date(m.valueOf() - (forcedTzo || 0)); + } + return new Date(m.valueOf() - + this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)) * 1000 * 60); + }; + return DateEnv; + }()); + + var globalLocales = []; + + var RAW_EN_LOCALE = { + code: 'en', + week: { + dow: 0, + doy: 4, // 4 days need to be within the year to be considered the first week + }, + direction: 'ltr', + buttonText: { + prev: 'prev', + next: 'next', + prevYear: 'prev year', + nextYear: 'next year', + year: 'year', + today: 'today', + month: 'month', + week: 'week', + day: 'day', + list: 'list', + }, + weekText: 'W', + allDayText: 'all-day', + moreLinkText: 'more', + noEventsText: 'No events to display', + }; + function organizeRawLocales(explicitRawLocales) { + var defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en'; + var allRawLocales = globalLocales.concat(explicitRawLocales); + var rawLocaleMap = { + en: RAW_EN_LOCALE, // necessary? + }; + for (var _i = 0, allRawLocales_1 = allRawLocales; _i < allRawLocales_1.length; _i++) { + var rawLocale = allRawLocales_1[_i]; + rawLocaleMap[rawLocale.code] = rawLocale; + } + return { + map: rawLocaleMap, + defaultCode: defaultCode, + }; + } + function buildLocale(inputSingular, available) { + if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) { + return parseLocale(inputSingular.code, [inputSingular.code], inputSingular); + } + return queryLocale(inputSingular, available); + } + function queryLocale(codeArg, available) { + var codes = [].concat(codeArg || []); // will convert to array + var raw = queryRawLocale(codes, available) || RAW_EN_LOCALE; + return parseLocale(codeArg, codes, raw); + } + function queryRawLocale(codes, available) { + for (var i = 0; i < codes.length; i += 1) { + var parts = codes[i].toLocaleLowerCase().split('-'); + for (var j = parts.length; j > 0; j -= 1) { + var simpleId = parts.slice(0, j).join('-'); + if (available[simpleId]) { + return available[simpleId]; + } + } + } + return null; + } + function parseLocale(codeArg, codes, raw) { + var merged = mergeProps([RAW_EN_LOCALE, raw], ['buttonText']); + delete merged.code; // don't want this part of the options + var week = merged.week; + delete merged.week; + return { + codeArg: codeArg, + codes: codes, + week: week, + simpleNumberFormat: new Intl.NumberFormat(codeArg), + options: merged, + }; + } + + function formatDate(dateInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = buildDateEnv$1(options); + var formatter = createFormatter(options); + var dateMeta = dateEnv.createMarkerMeta(dateInput); + if (!dateMeta) { // TODO: warning? + return ''; + } + return dateEnv.format(dateMeta.marker, formatter, { + forcedTzo: dateMeta.forcedTzo, + }); + } + function formatRange(startInput, endInput, options) { + var dateEnv = buildDateEnv$1(typeof options === 'object' && options ? options : {}); // pass in if non-null object + var formatter = createFormatter(options); + var startMeta = dateEnv.createMarkerMeta(startInput); + var endMeta = dateEnv.createMarkerMeta(endInput); + if (!startMeta || !endMeta) { // TODO: warning? + return ''; + } + return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, { + forcedStartTzo: startMeta.forcedTzo, + forcedEndTzo: endMeta.forcedTzo, + isEndExclusive: options.isEndExclusive, + defaultSeparator: BASE_OPTION_DEFAULTS.defaultRangeSeparator, + }); + } + // TODO: more DRY and optimized + function buildDateEnv$1(settings) { + var locale = buildLocale(settings.locale || 'en', organizeRawLocales([]).map); // TODO: don't hardcode 'en' everywhere + return new DateEnv(__assign(__assign({ timeZone: BASE_OPTION_DEFAULTS.timeZone, calendarSystem: 'gregory' }, settings), { locale: locale })); + } + + var DEF_DEFAULTS = { + startTime: '09:00', + endTime: '17:00', + daysOfWeek: [1, 2, 3, 4, 5], + display: 'inverse-background', + classNames: 'fc-non-business', + groupId: '_businessHours', // so multiple defs get grouped + }; + /* + TODO: pass around as EventDefHash!!! + */ + function parseBusinessHours(input, context) { + return parseEvents(refineInputs(input), null, context); + } + function refineInputs(input) { + var rawDefs; + if (input === true) { + rawDefs = [{}]; // will get DEF_DEFAULTS verbatim + } + else if (Array.isArray(input)) { + // if specifying an array, every sub-definition NEEDS a day-of-week + rawDefs = input.filter(function (rawDef) { return rawDef.daysOfWeek; }); + } + else if (typeof input === 'object' && input) { // non-null object + rawDefs = [input]; + } + else { // is probably false + rawDefs = []; + } + rawDefs = rawDefs.map(function (rawDef) { return (__assign(__assign({}, DEF_DEFAULTS), rawDef)); }); + return rawDefs; + } + + function pointInsideRect(point, rect) { + return point.left >= rect.left && + point.left < rect.right && + point.top >= rect.top && + point.top < rect.bottom; + } + // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false + function intersectRects(rect1, rect2) { + var res = { + left: Math.max(rect1.left, rect2.left), + right: Math.min(rect1.right, rect2.right), + top: Math.max(rect1.top, rect2.top), + bottom: Math.min(rect1.bottom, rect2.bottom), + }; + if (res.left < res.right && res.top < res.bottom) { + return res; + } + return false; + } + function translateRect(rect, deltaX, deltaY) { + return { + left: rect.left + deltaX, + right: rect.right + deltaX, + top: rect.top + deltaY, + bottom: rect.bottom + deltaY, + }; + } + // Returns a new point that will have been moved to reside within the given rectangle + function constrainPoint(point, rect) { + return { + left: Math.min(Math.max(point.left, rect.left), rect.right), + top: Math.min(Math.max(point.top, rect.top), rect.bottom), + }; + } + // Returns a point that is the center of the given rectangle + function getRectCenter(rect) { + return { + left: (rect.left + rect.right) / 2, + top: (rect.top + rect.bottom) / 2, + }; + } + // Subtracts point2's coordinates from point1's coordinates, returning a delta + function diffPoints(point1, point2) { + return { + left: point1.left - point2.left, + top: point1.top - point2.top, + }; + } + + var canVGrowWithinCell; + function getCanVGrowWithinCell() { + if (canVGrowWithinCell == null) { + canVGrowWithinCell = computeCanVGrowWithinCell(); + } + return canVGrowWithinCell; + } + function computeCanVGrowWithinCell() { + // for SSR, because this function is call immediately at top-level + // TODO: just make this logic execute top-level, immediately, instead of doing lazily + if (typeof document === 'undefined') { + return true; + } + var el = document.createElement('div'); + el.style.position = 'absolute'; + el.style.top = '0px'; + el.style.left = '0px'; + el.innerHTML = '
'; + 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 = ''; + + /// + if (typeof FullCalendarVDom === 'undefined') { + throw new Error('Please import the top-level fullcalendar lib before attempting to import a plugin.'); + } + var Component = FullCalendarVDom.Component; + var createElement = FullCalendarVDom.createElement; + var render = FullCalendarVDom.render; + var createRef = FullCalendarVDom.createRef; + var Fragment = FullCalendarVDom.Fragment; + var createContext = FullCalendarVDom.createContext; + var createPortal = FullCalendarVDom.createPortal; + var flushToDom = FullCalendarVDom.flushToDom; + var unmountComponentAtNode = FullCalendarVDom.unmountComponentAtNode; + /* eslint-enable */ + + var ScrollResponder = /** @class */ (function () { + function ScrollResponder(execFunc, emitter, scrollTime, scrollTimeReset) { + var _this = this; + this.execFunc = execFunc; + this.emitter = emitter; + this.scrollTime = scrollTime; + this.scrollTimeReset = scrollTimeReset; + this.handleScrollRequest = function (request) { + _this.queuedRequest = __assign({}, _this.queuedRequest || {}, request); + _this.drain(); + }; + emitter.on('_scrollRequest', this.handleScrollRequest); + this.fireInitialScroll(); + } + ScrollResponder.prototype.detach = function () { + this.emitter.off('_scrollRequest', this.handleScrollRequest); + }; + ScrollResponder.prototype.update = function (isDatesNew) { + if (isDatesNew && this.scrollTimeReset) { + this.fireInitialScroll(); // will drain + } + else { + this.drain(); + } + }; + ScrollResponder.prototype.fireInitialScroll = function () { + this.handleScrollRequest({ + time: this.scrollTime, + }); + }; + ScrollResponder.prototype.drain = function () { + if (this.queuedRequest && this.execFunc(this.queuedRequest)) { + this.queuedRequest = null; + } + }; + return ScrollResponder; + }()); + + var ViewContextType = createContext({}); // for Components + function buildViewContext(viewSpec, viewApi, viewOptions, dateProfileGenerator, dateEnv, theme, pluginHooks, dispatch, getCurrentData, emitter, calendarApi, registerInteractiveComponent, unregisterInteractiveComponent) { + return { + dateEnv: dateEnv, + options: viewOptions, + pluginHooks: pluginHooks, + emitter: emitter, + dispatch: dispatch, + getCurrentData: getCurrentData, + calendarApi: calendarApi, + viewSpec: viewSpec, + viewApi: viewApi, + dateProfileGenerator: dateProfileGenerator, + theme: theme, + isRtl: viewOptions.direction === 'rtl', + addResizeHandler: function (handler) { + emitter.on('_resize', handler); + }, + removeResizeHandler: function (handler) { + emitter.off('_resize', handler); + }, + createScrollResponder: function (execFunc) { + return new ScrollResponder(execFunc, emitter, createDuration(viewOptions.scrollTime), viewOptions.scrollTimeReset); + }, + registerInteractiveComponent: registerInteractiveComponent, + unregisterInteractiveComponent: unregisterInteractiveComponent, + }; + } + + /* eslint max-classes-per-file: off */ + var PureComponent = /** @class */ (function (_super) { + __extends(PureComponent, _super); + function PureComponent() { + return _super !== null && _super.apply(this, arguments) || this; + } + PureComponent.prototype.shouldComponentUpdate = function (nextProps, nextState) { + if (this.debug) { + // eslint-disable-next-line no-console + console.log(getUnequalProps(nextProps, this.props), getUnequalProps(nextState, this.state)); + } + return !compareObjs(this.props, nextProps, this.propEquality) || + !compareObjs(this.state, nextState, this.stateEquality); + }; + PureComponent.addPropsEquality = addPropsEquality; + PureComponent.addStateEquality = addStateEquality; + PureComponent.contextType = ViewContextType; + return PureComponent; + }(Component)); + PureComponent.prototype.propEquality = {}; + PureComponent.prototype.stateEquality = {}; + var BaseComponent = /** @class */ (function (_super) { + __extends(BaseComponent, _super); + function BaseComponent() { + return _super !== null && _super.apply(this, arguments) || this; + } + BaseComponent.contextType = ViewContextType; + return BaseComponent; + }(PureComponent)); + function addPropsEquality(propEquality) { + var hash = Object.create(this.prototype.propEquality); + __assign(hash, propEquality); + this.prototype.propEquality = hash; + } + function addStateEquality(stateEquality) { + var hash = Object.create(this.prototype.stateEquality); + __assign(hash, stateEquality); + this.prototype.stateEquality = hash; + } + // use other one + function setRef(ref, current) { + if (typeof ref === 'function') { + ref(current); + } + else if (ref) { + // see https://github.com/facebook/react/issues/13029 + ref.current = current; + } + } + + /* + an INTERACTABLE date component + + PURPOSES: + - hook up to fg, fill, and mirror renderers + - interface for dragging and hits + */ + var DateComponent = /** @class */ (function (_super) { + __extends(DateComponent, _super); + function DateComponent() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.uid = guid(); + return _this; + } + // Hit System + // ----------------------------------------------------------------------------------------------------------------- + DateComponent.prototype.prepareHits = function () { + }; + DateComponent.prototype.queryHit = function (positionLeft, positionTop, elWidth, elHeight) { + return null; // this should be abstract + }; + // Pointer Interaction Utils + // ----------------------------------------------------------------------------------------------------------------- + DateComponent.prototype.isValidSegDownEl = function (el) { + return !this.props.eventDrag && // HACK + !this.props.eventResize && // HACK + !elementClosest(el, '.fc-event-mirror'); + }; + DateComponent.prototype.isValidDateDownEl = function (el) { + return !elementClosest(el, '.fc-event:not(.fc-bg-event)') && + !elementClosest(el, '.fc-more-link') && // a "more.." link + !elementClosest(el, 'a[data-navlink]') && // a clickable nav link + !elementClosest(el, '.fc-popover'); // hack + }; + return DateComponent; + }(BaseComponent)); + + // TODO: easier way to add new hooks? need to update a million things + function createPlugin(input) { + return { + id: guid(), + deps: input.deps || [], + reducers: input.reducers || [], + isLoadingFuncs: input.isLoadingFuncs || [], + contextInit: [].concat(input.contextInit || []), + eventRefiners: input.eventRefiners || {}, + eventDefMemberAdders: input.eventDefMemberAdders || [], + eventSourceRefiners: input.eventSourceRefiners || {}, + isDraggableTransformers: input.isDraggableTransformers || [], + eventDragMutationMassagers: input.eventDragMutationMassagers || [], + eventDefMutationAppliers: input.eventDefMutationAppliers || [], + dateSelectionTransformers: input.dateSelectionTransformers || [], + datePointTransforms: input.datePointTransforms || [], + dateSpanTransforms: input.dateSpanTransforms || [], + views: input.views || {}, + viewPropsTransformers: input.viewPropsTransformers || [], + isPropsValid: input.isPropsValid || null, + externalDefTransforms: input.externalDefTransforms || [], + viewContainerAppends: input.viewContainerAppends || [], + eventDropTransformers: input.eventDropTransformers || [], + componentInteractions: input.componentInteractions || [], + calendarInteractions: input.calendarInteractions || [], + themeClasses: input.themeClasses || {}, + eventSourceDefs: input.eventSourceDefs || [], + cmdFormatter: input.cmdFormatter, + recurringTypes: input.recurringTypes || [], + namedTimeZonedImpl: input.namedTimeZonedImpl, + initialView: input.initialView || '', + elementDraggingImpl: input.elementDraggingImpl, + optionChangeHandlers: input.optionChangeHandlers || {}, + scrollGridImpl: input.scrollGridImpl || null, + contentTypeHandlers: input.contentTypeHandlers || {}, + listenerRefiners: input.listenerRefiners || {}, + optionRefiners: input.optionRefiners || {}, + propSetHandlers: input.propSetHandlers || {}, + }; + } + function buildPluginHooks(pluginDefs, globalDefs) { + var isAdded = {}; + var hooks = { + reducers: [], + isLoadingFuncs: [], + contextInit: [], + eventRefiners: {}, + eventDefMemberAdders: [], + eventSourceRefiners: {}, + isDraggableTransformers: [], + eventDragMutationMassagers: [], + eventDefMutationAppliers: [], + dateSelectionTransformers: [], + datePointTransforms: [], + dateSpanTransforms: [], + views: {}, + viewPropsTransformers: [], + isPropsValid: null, + externalDefTransforms: [], + viewContainerAppends: [], + eventDropTransformers: [], + componentInteractions: [], + calendarInteractions: [], + themeClasses: {}, + eventSourceDefs: [], + cmdFormatter: null, + recurringTypes: [], + namedTimeZonedImpl: null, + initialView: '', + elementDraggingImpl: null, + optionChangeHandlers: {}, + scrollGridImpl: null, + contentTypeHandlers: {}, + listenerRefiners: {}, + optionRefiners: {}, + propSetHandlers: {}, + }; + function addDefs(defs) { + for (var _i = 0, defs_1 = defs; _i < defs_1.length; _i++) { + var def = defs_1[_i]; + if (!isAdded[def.id]) { + isAdded[def.id] = true; + addDefs(def.deps); + hooks = combineHooks(hooks, def); + } + } + } + if (pluginDefs) { + addDefs(pluginDefs); + } + addDefs(globalDefs); + return hooks; + } + function buildBuildPluginHooks() { + var currentOverrideDefs = []; + var currentGlobalDefs = []; + var currentHooks; + return function (overrideDefs, globalDefs) { + if (!currentHooks || !isArraysEqual(overrideDefs, currentOverrideDefs) || !isArraysEqual(globalDefs, currentGlobalDefs)) { + currentHooks = buildPluginHooks(overrideDefs, globalDefs); + } + currentOverrideDefs = overrideDefs; + currentGlobalDefs = globalDefs; + return currentHooks; + }; + } + function combineHooks(hooks0, hooks1) { + return { + reducers: hooks0.reducers.concat(hooks1.reducers), + isLoadingFuncs: hooks0.isLoadingFuncs.concat(hooks1.isLoadingFuncs), + contextInit: hooks0.contextInit.concat(hooks1.contextInit), + eventRefiners: __assign(__assign({}, hooks0.eventRefiners), hooks1.eventRefiners), + eventDefMemberAdders: hooks0.eventDefMemberAdders.concat(hooks1.eventDefMemberAdders), + eventSourceRefiners: __assign(__assign({}, hooks0.eventSourceRefiners), hooks1.eventSourceRefiners), + isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers), + eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers), + eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers), + dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers), + datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms), + dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms), + views: __assign(__assign({}, hooks0.views), hooks1.views), + viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers), + isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid, + externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms), + viewContainerAppends: hooks0.viewContainerAppends.concat(hooks1.viewContainerAppends), + eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers), + calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions), + componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions), + themeClasses: __assign(__assign({}, hooks0.themeClasses), hooks1.themeClasses), + eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs), + cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter, + recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes), + namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl, + initialView: hooks0.initialView || hooks1.initialView, + elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, + optionChangeHandlers: __assign(__assign({}, hooks0.optionChangeHandlers), hooks1.optionChangeHandlers), + scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl, + contentTypeHandlers: __assign(__assign({}, hooks0.contentTypeHandlers), hooks1.contentTypeHandlers), + listenerRefiners: __assign(__assign({}, hooks0.listenerRefiners), hooks1.listenerRefiners), + optionRefiners: __assign(__assign({}, hooks0.optionRefiners), hooks1.optionRefiners), + propSetHandlers: __assign(__assign({}, hooks0.propSetHandlers), hooks1.propSetHandlers), + }; + } + + var StandardTheme = /** @class */ (function (_super) { + __extends(StandardTheme, _super); + function StandardTheme() { + return _super !== null && _super.apply(this, arguments) || this; + } + return StandardTheme; + }(Theme)); + StandardTheme.prototype.classes = { + root: 'fc-theme-standard', + tableCellShaded: 'fc-cell-shaded', + buttonGroup: 'fc-button-group', + button: 'fc-button fc-button-primary', + buttonActive: 'fc-button-active', + }; + StandardTheme.prototype.baseIconClass = 'fc-icon'; + StandardTheme.prototype.iconClasses = { + close: 'fc-icon-x', + prev: 'fc-icon-chevron-left', + next: 'fc-icon-chevron-right', + prevYear: 'fc-icon-chevrons-left', + nextYear: 'fc-icon-chevrons-right', + }; + StandardTheme.prototype.rtlIconClasses = { + prev: 'fc-icon-chevron-right', + next: 'fc-icon-chevron-left', + prevYear: 'fc-icon-chevrons-right', + nextYear: 'fc-icon-chevrons-left', + }; + StandardTheme.prototype.iconOverrideOption = 'buttonIcons'; // TODO: make TS-friendly + StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon'; + StandardTheme.prototype.iconOverridePrefix = 'fc-icon-'; + + function compileViewDefs(defaultConfigs, overrideConfigs) { + var hash = {}; + var viewType; + for (viewType in defaultConfigs) { + ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs); + } + for (viewType in overrideConfigs) { + ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs); + } + return hash; + } + function ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) { + if (hash[viewType]) { + return hash[viewType]; + } + var viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs); + if (viewDef) { + hash[viewType] = viewDef; + } + return viewDef; + } + function buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) { + var defaultConfig = defaultConfigs[viewType]; + var overrideConfig = overrideConfigs[viewType]; + var queryProp = function (name) { return ((defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] : + ((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null)); }; + var theComponent = queryProp('component'); + var superType = queryProp('superType'); + var superDef = null; + if (superType) { + if (superType === viewType) { + throw new Error('Can\'t have a custom view type that references itself'); + } + superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs); + } + if (!theComponent && superDef) { + theComponent = superDef.component; + } + if (!theComponent) { + return null; // don't throw a warning, might be settings for a single-unit view + } + return { + type: viewType, + component: theComponent, + defaults: __assign(__assign({}, (superDef ? superDef.defaults : {})), (defaultConfig ? defaultConfig.rawOptions : {})), + overrides: __assign(__assign({}, (superDef ? superDef.overrides : {})), (overrideConfig ? overrideConfig.rawOptions : {})), + }; + } + + /* eslint max-classes-per-file: off */ + // NOTE: in JSX, you should always use this class with arg. otherwise, will default to any??? + var RenderHook = /** @class */ (function (_super) { + __extends(RenderHook, _super); + function RenderHook() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.rootElRef = createRef(); + _this.handleRootEl = function (el) { + setRef(_this.rootElRef, el); + if (_this.props.elRef) { + setRef(_this.props.elRef, el); + } + }; + return _this; + } + RenderHook.prototype.render = function () { + var _this = this; + var props = this.props; + var hookProps = props.hookProps; + return (createElement(MountHook, { hookProps: hookProps, didMount: props.didMount, willUnmount: props.willUnmount, elRef: this.handleRootEl }, function (rootElRef) { return (createElement(ContentHook, { hookProps: hookProps, content: props.content, defaultContent: props.defaultContent, backupElRef: _this.rootElRef }, function (innerElRef, innerContent) { return props.children(rootElRef, normalizeClassNames(props.classNames, hookProps), innerElRef, innerContent); })); })); + }; + return RenderHook; + }(BaseComponent)); + // TODO: rename to be about function, not default. use in above type + // for forcing rerender of components that use the ContentHook + var CustomContentRenderContext = createContext(0); + function ContentHook(props) { + return (createElement(CustomContentRenderContext.Consumer, null, function (renderId) { return (createElement(ContentHookInner, __assign({ renderId: renderId }, props))); })); + } + var ContentHookInner = /** @class */ (function (_super) { + __extends(ContentHookInner, _super); + function ContentHookInner() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.innerElRef = createRef(); + return _this; + } + ContentHookInner.prototype.render = function () { + return this.props.children(this.innerElRef, this.renderInnerContent()); + }; + ContentHookInner.prototype.componentDidMount = function () { + this.updateCustomContent(); + }; + ContentHookInner.prototype.componentDidUpdate = function () { + this.updateCustomContent(); + }; + ContentHookInner.prototype.componentWillUnmount = function () { + if (this.customContentInfo && this.customContentInfo.destroy) { + this.customContentInfo.destroy(); + } + }; + ContentHookInner.prototype.renderInnerContent = function () { + var customContentInfo = this.customContentInfo; // only populated if using non-[p]react node(s) + var innerContent = this.getInnerContent(); + var meta = this.getContentMeta(innerContent); + // initial run, or content-type changing? (from vue -> react for example) + if (!customContentInfo || customContentInfo.contentKey !== meta.contentKey) { + // clearing old value + if (customContentInfo) { + if (customContentInfo.destroy) { + customContentInfo.destroy(); + } + customContentInfo = this.customContentInfo = null; + } + // assigning new value + if (meta.contentKey) { + customContentInfo = this.customContentInfo = __assign({ contentKey: meta.contentKey, contentVal: innerContent[meta.contentKey] }, meta.buildLifecycleFuncs()); + } + // updating + } + else if (customContentInfo) { + customContentInfo.contentVal = innerContent[meta.contentKey]; + } + return customContentInfo + ? [] // signal that something was specified + : innerContent; // assume a [p]react vdom node. use it + }; + ContentHookInner.prototype.getInnerContent = function () { + var props = this.props; + var innerContent = normalizeContent(props.content, props.hookProps); + if (innerContent === undefined) { // use the default + innerContent = normalizeContent(props.defaultContent, props.hookProps); + } + return innerContent == null ? null : innerContent; // convert undefined to null (better for React) + }; + ContentHookInner.prototype.getContentMeta = function (innerContent) { + var contentTypeHandlers = this.context.pluginHooks.contentTypeHandlers; + var contentKey = ''; + var buildLifecycleFuncs = null; + if (innerContent) { // allowed to be null, for convenience to caller + for (var searchKey in contentTypeHandlers) { + if (innerContent[searchKey] !== undefined) { + contentKey = searchKey; + buildLifecycleFuncs = contentTypeHandlers[searchKey]; + break; + } + } + } + return { contentKey: contentKey, buildLifecycleFuncs: buildLifecycleFuncs }; + }; + ContentHookInner.prototype.updateCustomContent = function () { + if (this.customContentInfo) { // for non-[p]react + this.customContentInfo.render(this.innerElRef.current || this.props.backupElRef.current, // the element to render into + this.customContentInfo.contentVal); + } + }; + return ContentHookInner; + }(BaseComponent)); + var MountHook = /** @class */ (function (_super) { + __extends(MountHook, _super); + function MountHook() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.handleRootEl = function (rootEl) { + _this.rootEl = rootEl; + if (_this.props.elRef) { + setRef(_this.props.elRef, rootEl); + } + }; + return _this; + } + MountHook.prototype.render = function () { + return this.props.children(this.handleRootEl); + }; + MountHook.prototype.componentDidMount = function () { + var callback = this.props.didMount; + if (callback) { + callback(__assign(__assign({}, this.props.hookProps), { el: this.rootEl })); + } + }; + MountHook.prototype.componentWillUnmount = function () { + var callback = this.props.willUnmount; + if (callback) { + callback(__assign(__assign({}, this.props.hookProps), { el: this.rootEl })); + } + }; + return MountHook; + }(BaseComponent)); + function buildClassNameNormalizer() { + var currentGenerator; + var currentHookProps; + var currentClassNames = []; + return function (generator, hookProps) { + if (!currentHookProps || !isPropsEqual(currentHookProps, hookProps) || generator !== currentGenerator) { + currentGenerator = generator; + currentHookProps = hookProps; + currentClassNames = normalizeClassNames(generator, hookProps); + } + return currentClassNames; + }; + } + function normalizeClassNames(classNames, hookProps) { + if (typeof classNames === 'function') { + classNames = classNames(hookProps); + } + return parseClassNames(classNames); + } + function normalizeContent(input, hookProps) { + if (typeof input === 'function') { + return input(hookProps, createElement); // give the function the vdom-creation func + } + return input; + } + + var ViewRoot = /** @class */ (function (_super) { + __extends(ViewRoot, _super); + function ViewRoot() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.normalizeClassNames = buildClassNameNormalizer(); + return _this; + } + ViewRoot.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var options = context.options; + var hookProps = { view: context.viewApi }; + var customClassNames = this.normalizeClassNames(options.viewClassNames, hookProps); + return (createElement(MountHook, { hookProps: hookProps, didMount: options.viewDidMount, willUnmount: options.viewWillUnmount, elRef: props.elRef }, function (rootElRef) { return props.children(rootElRef, ["fc-" + props.viewSpec.type + "-view", 'fc-view'].concat(customClassNames)); })); + }; + return ViewRoot; + }(BaseComponent)); + + function parseViewConfigs(inputs) { + return mapHash(inputs, parseViewConfig); + } + function parseViewConfig(input) { + var rawOptions = typeof input === 'function' ? + { component: input } : + input; + var component = rawOptions.component; + if (rawOptions.content) { + component = createViewHookComponent(rawOptions); + // TODO: remove content/classNames/didMount/etc from options? + } + return { + superType: rawOptions.type, + component: component, + rawOptions: rawOptions, + }; + } + function createViewHookComponent(options) { + return function (viewProps) { return (createElement(ViewContextType.Consumer, null, function (context) { return (createElement(ViewRoot, { viewSpec: context.viewSpec }, function (viewElRef, viewClassNames) { + var hookProps = __assign(__assign({}, viewProps), { nextDayThreshold: context.options.nextDayThreshold }); + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.classNames, content: options.content, didMount: options.didMount, willUnmount: options.willUnmount, elRef: viewElRef }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("div", { className: viewClassNames.concat(customClassNames).join(' '), ref: rootElRef }, innerContent)); })); + })); })); }; + } + + function buildViewSpecs(defaultInputs, optionOverrides, dynamicOptionOverrides, localeDefaults) { + var defaultConfigs = parseViewConfigs(defaultInputs); + var overrideConfigs = parseViewConfigs(optionOverrides.views); + var viewDefs = compileViewDefs(defaultConfigs, overrideConfigs); + return mapHash(viewDefs, function (viewDef) { return buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults); }); + } + function buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults) { + var durationInput = viewDef.overrides.duration || + viewDef.defaults.duration || + dynamicOptionOverrides.duration || + optionOverrides.duration; + var duration = null; + var durationUnit = ''; + var singleUnit = ''; + var singleUnitOverrides = {}; + if (durationInput) { + duration = createDurationCached(durationInput); + if (duration) { // valid? + var denom = greatestDurationDenominator(duration); + durationUnit = denom.unit; + if (denom.value === 1) { + singleUnit = durationUnit; + singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].rawOptions : {}; + } + } + } + var queryButtonText = function (optionsSubset) { + var buttonTextMap = optionsSubset.buttonText || {}; + var buttonTextKey = viewDef.defaults.buttonTextKey; + if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) { + return buttonTextMap[buttonTextKey]; + } + if (buttonTextMap[viewDef.type] != null) { + return buttonTextMap[viewDef.type]; + } + if (buttonTextMap[singleUnit] != null) { + return buttonTextMap[singleUnit]; + } + return null; + }; + return { + type: viewDef.type, + component: viewDef.component, + duration: duration, + durationUnit: durationUnit, + singleUnit: singleUnit, + optionDefaults: viewDef.defaults, + optionOverrides: __assign(__assign({}, singleUnitOverrides), viewDef.overrides), + buttonTextOverride: queryButtonText(dynamicOptionOverrides) || + queryButtonText(optionOverrides) || // constructor-specified buttonText lookup hash takes precedence + viewDef.overrides.buttonText, + buttonTextDefault: queryButtonText(localeDefaults) || + viewDef.defaults.buttonText || + queryButtonText(BASE_OPTION_DEFAULTS) || + viewDef.type, // fall back to given view name + }; + } + // hack to get memoization working + var durationInputMap = {}; + function createDurationCached(durationInput) { + var json = JSON.stringify(durationInput); + var res = durationInputMap[json]; + if (res === undefined) { + res = createDuration(durationInput); + durationInputMap[json] = res; + } + return res; + } + + var DateProfileGenerator = /** @class */ (function () { + function DateProfileGenerator(props) { + this.props = props; + this.nowDate = getNow(props.nowInput, props.dateEnv); + this.initHiddenDays(); + } + /* Date Range Computation + ------------------------------------------------------------------------------------------------------------------*/ + // Builds a structure with info about what the dates/ranges will be for the "prev" view. + DateProfileGenerator.prototype.buildPrev = function (currentDateProfile, currentDate, forceToValid) { + var dateEnv = this.props.dateEnv; + var prevDate = dateEnv.subtract(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month + currentDateProfile.dateIncrement); + return this.build(prevDate, -1, forceToValid); + }; + // Builds a structure with info about what the dates/ranges will be for the "next" view. + DateProfileGenerator.prototype.buildNext = function (currentDateProfile, currentDate, forceToValid) { + var dateEnv = this.props.dateEnv; + var nextDate = dateEnv.add(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month + currentDateProfile.dateIncrement); + return this.build(nextDate, 1, forceToValid); + }; + // Builds a structure holding dates/ranges for rendering around the given date. + // Optional direction param indicates whether the date is being incremented/decremented + // from its previous value. decremented = -1, incremented = 1 (default). + DateProfileGenerator.prototype.build = function (currentDate, direction, forceToValid) { + if (forceToValid === void 0) { forceToValid = true; } + var props = this.props; + var validRange; + var currentInfo; + var isRangeAllDay; + var renderRange; + var activeRange; + var isValid; + validRange = this.buildValidRange(); + validRange = this.trimHiddenDays(validRange); + if (forceToValid) { + currentDate = constrainMarkerToRange(currentDate, validRange); + } + currentInfo = this.buildCurrentRangeInfo(currentDate, direction); + isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit); + renderRange = this.buildRenderRange(this.trimHiddenDays(currentInfo.range), currentInfo.unit, isRangeAllDay); + renderRange = this.trimHiddenDays(renderRange); + activeRange = renderRange; + if (!props.showNonCurrentDates) { + activeRange = intersectRanges(activeRange, currentInfo.range); + } + activeRange = this.adjustActiveRange(activeRange); + activeRange = intersectRanges(activeRange, validRange); // might return null + // it's invalid if the originally requested date is not contained, + // or if the range is completely outside of the valid range. + isValid = rangesIntersect(currentInfo.range, validRange); + return { + // constraint for where prev/next operations can go and where events can be dragged/resized to. + // an object with optional start and end properties. + validRange: validRange, + // range the view is formally responsible for. + // for example, a month view might have 1st-31st, excluding padded dates + currentRange: currentInfo.range, + // name of largest unit being displayed, like "month" or "week" + currentRangeUnit: currentInfo.unit, + isRangeAllDay: isRangeAllDay, + // dates that display events and accept drag-n-drop + // will be `null` if no dates accept events + activeRange: activeRange, + // date range with a rendered skeleton + // includes not-active days that need some sort of DOM + renderRange: renderRange, + // Duration object that denotes the first visible time of any given day + slotMinTime: props.slotMinTime, + // Duration object that denotes the exclusive visible end time of any given day + slotMaxTime: props.slotMaxTime, + isValid: isValid, + // how far the current date will move for a prev/next operation + dateIncrement: this.buildDateIncrement(currentInfo.duration), + // pass a fallback (might be null) ^ + }; + }; + // Builds an object with optional start/end properties. + // Indicates the minimum/maximum dates to display. + // not responsible for trimming hidden days. + DateProfileGenerator.prototype.buildValidRange = function () { + var input = this.props.validRangeInput; + var simpleInput = typeof input === 'function' + ? input.call(this.props.calendarApi, this.nowDate) + : input; + return this.refineRange(simpleInput) || + { start: null, end: null }; // completely open-ended + }; + // Builds a structure with info about the "current" range, the range that is + // highlighted as being the current month for example. + // See build() for a description of `direction`. + // Guaranteed to have `range` and `unit` properties. `duration` is optional. + DateProfileGenerator.prototype.buildCurrentRangeInfo = function (date, direction) { + var props = this.props; + var duration = null; + var unit = null; + var range = null; + var dayCount; + if (props.duration) { + duration = props.duration; + unit = props.durationUnit; + range = this.buildRangeFromDuration(date, direction, duration, unit); + } + else if ((dayCount = this.props.dayCount)) { + unit = 'day'; + range = this.buildRangeFromDayCount(date, direction, dayCount); + } + else if ((range = this.buildCustomVisibleRange(date))) { + unit = props.dateEnv.greatestWholeUnit(range.start, range.end).unit; + } + else { + duration = this.getFallbackDuration(); + unit = greatestDurationDenominator(duration).unit; + range = this.buildRangeFromDuration(date, direction, duration, unit); + } + return { duration: duration, unit: unit, range: range }; + }; + DateProfileGenerator.prototype.getFallbackDuration = function () { + return createDuration({ day: 1 }); + }; + // Returns a new activeRange to have time values (un-ambiguate) + // slotMinTime or slotMaxTime causes the range to expand. + DateProfileGenerator.prototype.adjustActiveRange = function (range) { + var _a = this.props, dateEnv = _a.dateEnv, usesMinMaxTime = _a.usesMinMaxTime, slotMinTime = _a.slotMinTime, slotMaxTime = _a.slotMaxTime; + var start = range.start, end = range.end; + if (usesMinMaxTime) { + // expand active range if slotMinTime is negative (why not when positive?) + if (asRoughDays(slotMinTime) < 0) { + start = startOfDay(start); // necessary? + start = dateEnv.add(start, slotMinTime); + } + // expand active range if slotMaxTime is beyond one day (why not when negative?) + if (asRoughDays(slotMaxTime) > 1) { + end = startOfDay(end); // necessary? + end = addDays(end, -1); + end = dateEnv.add(end, slotMaxTime); + } + } + return { start: start, end: end }; + }; + // Builds the "current" range when it is specified as an explicit duration. + // `unit` is the already-computed greatestDurationDenominator unit of duration. + DateProfileGenerator.prototype.buildRangeFromDuration = function (date, direction, duration, unit) { + var _a = this.props, dateEnv = _a.dateEnv, dateAlignment = _a.dateAlignment; + var start; + var end; + var res; + // compute what the alignment should be + if (!dateAlignment) { + var dateIncrement = this.props.dateIncrement; + if (dateIncrement) { + // use the smaller of the two units + if (asRoughMs(dateIncrement) < asRoughMs(duration)) { + dateAlignment = greatestDurationDenominator(dateIncrement).unit; + } + else { + dateAlignment = unit; + } + } + else { + dateAlignment = unit; + } + } + // if the view displays a single day or smaller + if (asRoughDays(duration) <= 1) { + if (this.isHiddenDay(start)) { + start = this.skipHiddenDays(start, direction); + start = startOfDay(start); + } + } + function computeRes() { + start = dateEnv.startOf(date, dateAlignment); + end = dateEnv.add(start, duration); + res = { start: start, end: end }; + } + computeRes(); + // if range is completely enveloped by hidden days, go past the hidden days + if (!this.trimHiddenDays(res)) { + date = this.skipHiddenDays(date, direction); + computeRes(); + } + return res; + }; + // Builds the "current" range when a dayCount is specified. + DateProfileGenerator.prototype.buildRangeFromDayCount = function (date, direction, dayCount) { + var _a = this.props, dateEnv = _a.dateEnv, dateAlignment = _a.dateAlignment; + var runningCount = 0; + var start = date; + var end; + if (dateAlignment) { + start = dateEnv.startOf(start, dateAlignment); + } + start = startOfDay(start); + start = this.skipHiddenDays(start, direction); + end = start; + do { + end = addDays(end, 1); + if (!this.isHiddenDay(end)) { + runningCount += 1; + } + } while (runningCount < dayCount); + return { start: start, end: end }; + }; + // Builds a normalized range object for the "visible" range, + // which is a way to define the currentRange and activeRange at the same time. + DateProfileGenerator.prototype.buildCustomVisibleRange = function (date) { + var props = this.props; + var input = props.visibleRangeInput; + var simpleInput = typeof input === 'function' + ? input.call(props.calendarApi, props.dateEnv.toDate(date)) + : input; + var range = this.refineRange(simpleInput); + if (range && (range.start == null || range.end == null)) { + return null; + } + return range; + }; + // Computes the range that will represent the element/cells for *rendering*, + // but which may have voided days/times. + // not responsible for trimming hidden days. + DateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) { + return currentRange; + }; + // Compute the duration value that should be added/substracted to the current date + // when a prev/next operation happens. + DateProfileGenerator.prototype.buildDateIncrement = function (fallback) { + var dateIncrement = this.props.dateIncrement; + var customAlignment; + if (dateIncrement) { + return dateIncrement; + } + if ((customAlignment = this.props.dateAlignment)) { + return createDuration(1, customAlignment); + } + if (fallback) { + return fallback; + } + return createDuration({ days: 1 }); + }; + DateProfileGenerator.prototype.refineRange = function (rangeInput) { + if (rangeInput) { + var range = parseRange(rangeInput, this.props.dateEnv); + if (range) { + range = computeVisibleDayRange(range); + } + return range; + } + return null; + }; + /* Hidden Days + ------------------------------------------------------------------------------------------------------------------*/ + // Initializes internal variables related to calculating hidden days-of-week + DateProfileGenerator.prototype.initHiddenDays = function () { + var hiddenDays = this.props.hiddenDays || []; // array of day-of-week indices that are hidden + var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) + var dayCnt = 0; + var i; + if (this.props.weekends === false) { + hiddenDays.push(0, 6); // 0=sunday, 6=saturday + } + for (i = 0; i < 7; i += 1) { + if (!(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1)) { + dayCnt += 1; + } + } + if (!dayCnt) { + throw new Error('invalid hiddenDays'); // all days were hidden? bad. + } + this.isHiddenDayHash = isHiddenDayHash; + }; + // Remove days from the beginning and end of the range that are computed as hidden. + // If the whole range is trimmed off, returns null + DateProfileGenerator.prototype.trimHiddenDays = function (range) { + var start = range.start, end = range.end; + if (start) { + start = this.skipHiddenDays(start); + } + if (end) { + end = this.skipHiddenDays(end, -1, true); + } + if (start == null || end == null || start < end) { + return { start: start, end: end }; + } + return null; + }; + // Is the current day hidden? + // `day` is a day-of-week index (0-6), or a Date (used for UTC) + DateProfileGenerator.prototype.isHiddenDay = function (day) { + if (day instanceof Date) { + day = day.getUTCDay(); + } + return this.isHiddenDayHash[day]; + }; + // Incrementing the current day until it is no longer a hidden day, returning a copy. + // DOES NOT CONSIDER validRange! + // If the initial value of `date` is not a hidden day, don't do anything. + // Pass `isExclusive` as `true` if you are dealing with an end date. + // `inc` defaults to `1` (increment one day forward each time) + DateProfileGenerator.prototype.skipHiddenDays = function (date, inc, isExclusive) { + if (inc === void 0) { inc = 1; } + if (isExclusive === void 0) { isExclusive = false; } + while (this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7]) { + date = addDays(date, inc); + } + return date; + }; + return DateProfileGenerator; + }()); + + function reduceViewType(viewType, action) { + switch (action.type) { + case 'CHANGE_VIEW_TYPE': + viewType = action.viewType; + } + return viewType; + } + + function reduceDynamicOptionOverrides(dynamicOptionOverrides, action) { + var _a; + switch (action.type) { + case 'SET_OPTION': + return __assign(__assign({}, dynamicOptionOverrides), (_a = {}, _a[action.optionName] = action.rawOptionValue, _a)); + default: + return dynamicOptionOverrides; + } + } + + function reduceDateProfile(currentDateProfile, action, currentDate, dateProfileGenerator) { + var dp; + switch (action.type) { + case 'CHANGE_VIEW_TYPE': + return dateProfileGenerator.build(action.dateMarker || currentDate); + case 'CHANGE_DATE': + return dateProfileGenerator.build(action.dateMarker); + case 'PREV': + dp = dateProfileGenerator.buildPrev(currentDateProfile, currentDate); + if (dp.isValid) { + return dp; + } + break; + case 'NEXT': + dp = dateProfileGenerator.buildNext(currentDateProfile, currentDate); + if (dp.isValid) { + return dp; + } + break; + } + return currentDateProfile; + } + + function initEventSources(calendarOptions, dateProfile, context) { + var activeRange = dateProfile ? dateProfile.activeRange : null; + return addSources({}, parseInitialSources(calendarOptions, context), activeRange, context); + } + function reduceEventSources(eventSources, action, dateProfile, context) { + var activeRange = dateProfile ? dateProfile.activeRange : null; // need this check? + switch (action.type) { + case 'ADD_EVENT_SOURCES': // already parsed + return addSources(eventSources, action.sources, activeRange, context); + case 'REMOVE_EVENT_SOURCE': + return removeSource(eventSources, action.sourceId); + case 'PREV': // TODO: how do we track all actions that affect dateProfile :( + case 'NEXT': + case 'CHANGE_DATE': + case 'CHANGE_VIEW_TYPE': + if (dateProfile) { + return fetchDirtySources(eventSources, activeRange, context); + } + return eventSources; + case 'FETCH_EVENT_SOURCES': + return fetchSourcesByIds(eventSources, action.sourceIds ? // why no type? + arrayToHash(action.sourceIds) : + excludeStaticSources(eventSources, context), activeRange, action.isRefetch || false, context); + case 'RECEIVE_EVENTS': + case 'RECEIVE_EVENT_ERROR': + return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange); + case 'REMOVE_ALL_EVENT_SOURCES': + return {}; + default: + return eventSources; + } + } + function reduceEventSourcesNewTimeZone(eventSources, dateProfile, context) { + var activeRange = dateProfile ? dateProfile.activeRange : null; // need this check? + return fetchSourcesByIds(eventSources, excludeStaticSources(eventSources, context), activeRange, true, context); + } + function computeEventSourcesLoading(eventSources) { + for (var sourceId in eventSources) { + if (eventSources[sourceId].isFetching) { + return true; + } + } + return false; + } + function addSources(eventSourceHash, sources, fetchRange, context) { + var hash = {}; + for (var _i = 0, sources_1 = sources; _i < sources_1.length; _i++) { + var source = sources_1[_i]; + hash[source.sourceId] = source; + } + if (fetchRange) { + hash = fetchDirtySources(hash, fetchRange, context); + } + return __assign(__assign({}, eventSourceHash), hash); + } + function removeSource(eventSourceHash, sourceId) { + return filterHash(eventSourceHash, function (eventSource) { return eventSource.sourceId !== sourceId; }); + } + function fetchDirtySources(sourceHash, fetchRange, context) { + return fetchSourcesByIds(sourceHash, filterHash(sourceHash, function (eventSource) { return isSourceDirty(eventSource, fetchRange, context); }), fetchRange, false, context); + } + function isSourceDirty(eventSource, fetchRange, context) { + if (!doesSourceNeedRange(eventSource, context)) { + return !eventSource.latestFetchId; + } + return !context.options.lazyFetching || + !eventSource.fetchRange || + eventSource.isFetching || // always cancel outdated in-progress fetches + fetchRange.start < eventSource.fetchRange.start || + fetchRange.end > eventSource.fetchRange.end; + } + function fetchSourcesByIds(prevSources, sourceIdHash, fetchRange, isRefetch, context) { + var nextSources = {}; + for (var sourceId in prevSources) { + var source = prevSources[sourceId]; + if (sourceIdHash[sourceId]) { + nextSources[sourceId] = fetchSource(source, fetchRange, isRefetch, context); + } + else { + nextSources[sourceId] = source; + } + } + return nextSources; + } + function fetchSource(eventSource, fetchRange, isRefetch, context) { + var options = context.options, calendarApi = context.calendarApi; + var sourceDef = context.pluginHooks.eventSourceDefs[eventSource.sourceDefId]; + var fetchId = guid(); + sourceDef.fetch({ + eventSource: eventSource, + range: fetchRange, + isRefetch: isRefetch, + context: context, + }, function (res) { + var rawEvents = res.rawEvents; + if (options.eventSourceSuccess) { + rawEvents = options.eventSourceSuccess.call(calendarApi, rawEvents, res.xhr) || rawEvents; + } + if (eventSource.success) { + rawEvents = eventSource.success.call(calendarApi, rawEvents, res.xhr) || rawEvents; + } + context.dispatch({ + type: 'RECEIVE_EVENTS', + sourceId: eventSource.sourceId, + fetchId: fetchId, + fetchRange: fetchRange, + rawEvents: rawEvents, + }); + }, function (error) { + console.warn(error.message, error); + if (options.eventSourceFailure) { + options.eventSourceFailure.call(calendarApi, error); + } + if (eventSource.failure) { + eventSource.failure(error); + } + context.dispatch({ + type: 'RECEIVE_EVENT_ERROR', + sourceId: eventSource.sourceId, + fetchId: fetchId, + fetchRange: fetchRange, + error: error, + }); + }); + return __assign(__assign({}, eventSource), { isFetching: true, latestFetchId: fetchId }); + } + function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) { + var _a; + var eventSource = sourceHash[sourceId]; + if (eventSource && // not already removed + fetchId === eventSource.latestFetchId) { + return __assign(__assign({}, sourceHash), (_a = {}, _a[sourceId] = __assign(__assign({}, eventSource), { isFetching: false, fetchRange: fetchRange }), _a)); + } + return sourceHash; + } + function excludeStaticSources(eventSources, context) { + return filterHash(eventSources, function (eventSource) { return doesSourceNeedRange(eventSource, context); }); + } + function parseInitialSources(rawOptions, context) { + var refiners = buildEventSourceRefiners(context); + var rawSources = [].concat(rawOptions.eventSources || []); + var sources = []; // parsed + if (rawOptions.initialEvents) { + rawSources.unshift(rawOptions.initialEvents); + } + if (rawOptions.events) { + rawSources.unshift(rawOptions.events); + } + for (var _i = 0, rawSources_1 = rawSources; _i < rawSources_1.length; _i++) { + var rawSource = rawSources_1[_i]; + var source = parseEventSource(rawSource, context, refiners); + if (source) { + sources.push(source); + } + } + return sources; + } + function doesSourceNeedRange(eventSource, context) { + var defs = context.pluginHooks.eventSourceDefs; + return !defs[eventSource.sourceDefId].ignoreRange; + } + + function reduceEventStore(eventStore, action, eventSources, dateProfile, context) { + switch (action.type) { + case 'RECEIVE_EVENTS': // raw + return receiveRawEvents(eventStore, eventSources[action.sourceId], action.fetchId, action.fetchRange, action.rawEvents, context); + case 'ADD_EVENTS': // already parsed, but not expanded + return addEvent(eventStore, action.eventStore, // new ones + dateProfile ? dateProfile.activeRange : null, context); + case 'RESET_EVENTS': + return action.eventStore; + case 'MERGE_EVENTS': // already parsed and expanded + return mergeEventStores(eventStore, action.eventStore); + case 'PREV': // TODO: how do we track all actions that affect dateProfile :( + case 'NEXT': + case 'CHANGE_DATE': + case 'CHANGE_VIEW_TYPE': + if (dateProfile) { + return expandRecurring(eventStore, dateProfile.activeRange, context); + } + return eventStore; + case 'REMOVE_EVENTS': + return excludeSubEventStore(eventStore, action.eventStore); + case 'REMOVE_EVENT_SOURCE': + return excludeEventsBySourceId(eventStore, action.sourceId); + case 'REMOVE_ALL_EVENT_SOURCES': + return filterEventStoreDefs(eventStore, function (eventDef) { return (!eventDef.sourceId // only keep events with no source id + ); }); + case 'REMOVE_ALL_EVENTS': + return createEmptyEventStore(); + default: + return eventStore; + } + } + function receiveRawEvents(eventStore, eventSource, fetchId, fetchRange, rawEvents, context) { + if (eventSource && // not already removed + fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources + ) { + var subset = parseEvents(transformRawEvents(rawEvents, eventSource, context), eventSource, context); + if (fetchRange) { + subset = expandRecurring(subset, fetchRange, context); + } + return mergeEventStores(excludeEventsBySourceId(eventStore, eventSource.sourceId), subset); + } + return eventStore; + } + function transformRawEvents(rawEvents, eventSource, context) { + var calEachTransform = context.options.eventDataTransform; + var sourceEachTransform = eventSource ? eventSource.eventDataTransform : null; + if (sourceEachTransform) { + rawEvents = transformEachRawEvent(rawEvents, sourceEachTransform); + } + if (calEachTransform) { + rawEvents = transformEachRawEvent(rawEvents, calEachTransform); + } + return rawEvents; + } + function transformEachRawEvent(rawEvents, func) { + var refinedEvents; + if (!func) { + refinedEvents = rawEvents; + } + else { + refinedEvents = []; + for (var _i = 0, rawEvents_1 = rawEvents; _i < rawEvents_1.length; _i++) { + var rawEvent = rawEvents_1[_i]; + var refinedEvent = func(rawEvent); + if (refinedEvent) { + refinedEvents.push(refinedEvent); + } + else if (refinedEvent == null) { + refinedEvents.push(rawEvent); + } // if a different falsy value, do nothing + } + } + return refinedEvents; + } + function addEvent(eventStore, subset, expandRange, context) { + if (expandRange) { + subset = expandRecurring(subset, expandRange, context); + } + return mergeEventStores(eventStore, subset); + } + function rezoneEventStoreDates(eventStore, oldDateEnv, newDateEnv) { + var defs = eventStore.defs; + var instances = mapHash(eventStore.instances, function (instance) { + var def = defs[instance.defId]; + if (def.allDay || def.recurringDef) { + return instance; // isn't dependent on timezone + } + return __assign(__assign({}, instance), { range: { + start: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.start, instance.forcedStartTzo)), + end: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.end, instance.forcedEndTzo)), + }, forcedStartTzo: newDateEnv.canComputeOffset ? null : instance.forcedStartTzo, forcedEndTzo: newDateEnv.canComputeOffset ? null : instance.forcedEndTzo }); + }); + return { defs: defs, instances: instances }; + } + function excludeEventsBySourceId(eventStore, sourceId) { + return filterEventStoreDefs(eventStore, function (eventDef) { return eventDef.sourceId !== sourceId; }); + } + // QUESTION: why not just return instances? do a general object-property-exclusion util + function excludeInstances(eventStore, removals) { + return { + defs: eventStore.defs, + instances: filterHash(eventStore.instances, function (instance) { return !removals[instance.instanceId]; }), + }; + } + + function reduceDateSelection(currentSelection, action) { + switch (action.type) { + case 'UNSELECT_DATES': + return null; + case 'SELECT_DATES': + return action.selection; + default: + return currentSelection; + } + } + + function reduceSelectedEvent(currentInstanceId, action) { + switch (action.type) { + case 'UNSELECT_EVENT': + return ''; + case 'SELECT_EVENT': + return action.eventInstanceId; + default: + return currentInstanceId; + } + } + + function reduceEventDrag(currentDrag, action) { + var newDrag; + switch (action.type) { + case 'UNSET_EVENT_DRAG': + return null; + case 'SET_EVENT_DRAG': + newDrag = action.state; + return { + affectedEvents: newDrag.affectedEvents, + mutatedEvents: newDrag.mutatedEvents, + isEvent: newDrag.isEvent, + }; + default: + return currentDrag; + } + } + + function reduceEventResize(currentResize, action) { + var newResize; + switch (action.type) { + case 'UNSET_EVENT_RESIZE': + return null; + case 'SET_EVENT_RESIZE': + newResize = action.state; + return { + affectedEvents: newResize.affectedEvents, + mutatedEvents: newResize.mutatedEvents, + isEvent: newResize.isEvent, + }; + default: + return currentResize; + } + } + + function parseToolbars(calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) { + var viewsWithButtons = []; + var headerToolbar = calendarOptions.headerToolbar ? parseToolbar(calendarOptions.headerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons) : null; + var footerToolbar = calendarOptions.footerToolbar ? parseToolbar(calendarOptions.footerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons) : null; + return { headerToolbar: headerToolbar, footerToolbar: footerToolbar, viewsWithButtons: viewsWithButtons }; + } + function parseToolbar(sectionStrHash, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons) { + return mapHash(sectionStrHash, function (sectionStr) { return parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons); }); + } + /* + BAD: querying icons and text here. should be done at render time + */ + function parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi, viewsWithButtons) { + var isRtl = calendarOptions.direction === 'rtl'; + var calendarCustomButtons = calendarOptions.customButtons || {}; + var calendarButtonTextOverrides = calendarOptionOverrides.buttonText || {}; + var calendarButtonText = calendarOptions.buttonText || {}; + var sectionSubstrs = sectionStr ? sectionStr.split(' ') : []; + return sectionSubstrs.map(function (buttonGroupStr) { return (buttonGroupStr.split(',').map(function (buttonName) { + if (buttonName === 'title') { + return { buttonName: buttonName }; + } + var customButtonProps; + var viewSpec; + var buttonClick; + var buttonIcon; // only one of these will be set + var buttonText; // " + if ((customButtonProps = calendarCustomButtons[buttonName])) { + buttonClick = function (ev) { + if (customButtonProps.click) { + customButtonProps.click.call(ev.target, ev, ev.target); // TODO: use Calendar this context? + } + }; + (buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) || + (buttonIcon = theme.getIconClass(buttonName, isRtl)) || + (buttonText = customButtonProps.text); + } + else if ((viewSpec = viewSpecs[buttonName])) { + viewsWithButtons.push(buttonName); + buttonClick = function () { + calendarApi.changeView(buttonName); + }; + (buttonText = viewSpec.buttonTextOverride) || + (buttonIcon = theme.getIconClass(buttonName, isRtl)) || + (buttonText = viewSpec.buttonTextDefault); + } + else if (calendarApi[buttonName]) { // a calendarApi method + buttonClick = function () { + calendarApi[buttonName](); + }; + (buttonText = calendarButtonTextOverrides[buttonName]) || + (buttonIcon = theme.getIconClass(buttonName, isRtl)) || + (buttonText = calendarButtonText[buttonName]); + // ^ everything else is considered default + } + return { buttonName: buttonName, buttonClick: buttonClick, buttonIcon: buttonIcon, buttonText: buttonText }; + })); }); + } + + var eventSourceDef$3 = { + ignoreRange: true, + parseMeta: function (refined) { + if (Array.isArray(refined.events)) { + return refined.events; + } + return null; + }, + fetch: function (arg, success) { + success({ + rawEvents: arg.eventSource.meta, + }); + }, + }; + var arrayEventSourcePlugin = createPlugin({ + eventSourceDefs: [eventSourceDef$3], + }); + + var eventSourceDef$2 = { + parseMeta: function (refined) { + if (typeof refined.events === 'function') { + return refined.events; + } + return null; + }, + fetch: function (arg, success, failure) { + var dateEnv = arg.context.dateEnv; + var func = arg.eventSource.meta; + unpromisify(func.bind(null, buildRangeApiWithTimeZone(arg.range, dateEnv)), function (rawEvents) { + success({ rawEvents: rawEvents }); // needs an object response + }, failure); + }, + }; + var funcEventSourcePlugin = createPlugin({ + eventSourceDefs: [eventSourceDef$2], + }); + + function requestJson(method, url, params, successCallback, failureCallback) { + method = method.toUpperCase(); + var body = null; + if (method === 'GET') { + url = injectQueryStringParams(url, params); + } + else { + body = encodeParams(params); + } + var xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + if (method !== 'GET') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 400) { + var parsed = false; + var res = void 0; + try { + res = JSON.parse(xhr.responseText); + parsed = true; + } + catch (err) { + // will handle parsed=false + } + if (parsed) { + successCallback(res, xhr); + } + else { + failureCallback('Failure parsing JSON', xhr); + } + } + else { + failureCallback('Request failed', xhr); + } + }; + xhr.onerror = function () { + failureCallback('Request failed', xhr); + }; + xhr.send(body); + } + function injectQueryStringParams(url, params) { + return url + + (url.indexOf('?') === -1 ? '?' : '&') + + encodeParams(params); + } + function encodeParams(params) { + var parts = []; + for (var key in params) { + parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(params[key])); + } + return parts.join('&'); + } + + var JSON_FEED_EVENT_SOURCE_REFINERS = { + method: String, + extraParams: identity, + startParam: String, + endParam: String, + timeZoneParam: String, + }; + + var eventSourceDef$1 = { + parseMeta: function (refined) { + if (refined.url && (refined.format === 'json' || !refined.format)) { + return { + url: refined.url, + format: 'json', + method: (refined.method || 'GET').toUpperCase(), + extraParams: refined.extraParams, + startParam: refined.startParam, + endParam: refined.endParam, + timeZoneParam: refined.timeZoneParam, + }; + } + return null; + }, + fetch: function (arg, success, failure) { + var meta = arg.eventSource.meta; + var requestParams = buildRequestParams$1(meta, arg.range, arg.context); + requestJson(meta.method, meta.url, requestParams, function (rawEvents, xhr) { + success({ rawEvents: rawEvents, xhr: xhr }); + }, function (errorMessage, xhr) { + failure({ message: errorMessage, xhr: xhr }); + }); + }, + }; + var jsonFeedEventSourcePlugin = createPlugin({ + eventSourceRefiners: JSON_FEED_EVENT_SOURCE_REFINERS, + eventSourceDefs: [eventSourceDef$1], + }); + function buildRequestParams$1(meta, range, context) { + var dateEnv = context.dateEnv, options = context.options; + var startParam; + var endParam; + var timeZoneParam; + var customRequestParams; + var params = {}; + startParam = meta.startParam; + if (startParam == null) { + startParam = options.startParam; + } + endParam = meta.endParam; + if (endParam == null) { + endParam = options.endParam; + } + timeZoneParam = meta.timeZoneParam; + if (timeZoneParam == null) { + timeZoneParam = options.timeZoneParam; + } + // retrieve any outbound GET/POST data from the options + if (typeof meta.extraParams === 'function') { + // supplied as a function that returns a key/value object + customRequestParams = meta.extraParams(); + } + else { + // probably supplied as a straight key/value object + customRequestParams = meta.extraParams || {}; + } + __assign(params, customRequestParams); + params[startParam] = dateEnv.formatIso(range.start); + params[endParam] = dateEnv.formatIso(range.end); + if (dateEnv.timeZone !== 'local') { + params[timeZoneParam] = dateEnv.timeZone; + } + return params; + } + + var SIMPLE_RECURRING_REFINERS = { + daysOfWeek: identity, + startTime: createDuration, + endTime: createDuration, + duration: createDuration, + startRecur: identity, + endRecur: identity, + }; + + var recurring = { + parse: function (refined, dateEnv) { + if (refined.daysOfWeek || refined.startTime || refined.endTime || refined.startRecur || refined.endRecur) { + var recurringData = { + daysOfWeek: refined.daysOfWeek || null, + startTime: refined.startTime || null, + endTime: refined.endTime || null, + startRecur: refined.startRecur ? dateEnv.createMarker(refined.startRecur) : null, + endRecur: refined.endRecur ? dateEnv.createMarker(refined.endRecur) : null, + }; + var duration = void 0; + if (refined.duration) { + duration = refined.duration; + } + if (!duration && refined.startTime && refined.endTime) { + duration = subtractDurations(refined.endTime, refined.startTime); + } + return { + allDayGuess: Boolean(!refined.startTime && !refined.endTime), + duration: duration, + typeData: recurringData, // doesn't need endTime anymore but oh well + }; + } + return null; + }, + expand: function (typeData, framingRange, dateEnv) { + var clippedFramingRange = intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur }); + if (clippedFramingRange) { + return expandRanges(typeData.daysOfWeek, typeData.startTime, clippedFramingRange, dateEnv); + } + return []; + }, + }; + var simpleRecurringEventsPlugin = createPlugin({ + recurringTypes: [recurring], + eventRefiners: SIMPLE_RECURRING_REFINERS, + }); + function expandRanges(daysOfWeek, startTime, framingRange, dateEnv) { + var dowHash = daysOfWeek ? arrayToHash(daysOfWeek) : null; + var dayMarker = startOfDay(framingRange.start); + var endMarker = framingRange.end; + var instanceStarts = []; + while (dayMarker < endMarker) { + var instanceStart + // if everyday, or this particular day-of-week + = void 0; + // if everyday, or this particular day-of-week + if (!dowHash || dowHash[dayMarker.getUTCDay()]) { + if (startTime) { + instanceStart = dateEnv.add(dayMarker, startTime); + } + else { + instanceStart = dayMarker; + } + instanceStarts.push(instanceStart); + } + dayMarker = addDays(dayMarker, 1); + } + return instanceStarts; + } + + var changeHandlerPlugin = createPlugin({ + optionChangeHandlers: { + events: function (events, context) { + handleEventSources([events], context); + }, + eventSources: handleEventSources, + }, + }); + /* + BUG: if `event` was supplied, all previously-given `eventSources` will be wiped out + */ + function handleEventSources(inputs, context) { + var unfoundSources = hashValuesToArray(context.getCurrentData().eventSources); + var newInputs = []; + for (var _i = 0, inputs_1 = inputs; _i < inputs_1.length; _i++) { + var input = inputs_1[_i]; + var inputFound = false; + for (var i = 0; i < unfoundSources.length; i += 1) { + if (unfoundSources[i]._raw === input) { + unfoundSources.splice(i, 1); // delete + inputFound = true; + break; + } + } + if (!inputFound) { + newInputs.push(input); + } + } + for (var _a = 0, unfoundSources_1 = unfoundSources; _a < unfoundSources_1.length; _a++) { + var unfoundSource = unfoundSources_1[_a]; + context.dispatch({ + type: 'REMOVE_EVENT_SOURCE', + sourceId: unfoundSource.sourceId, + }); + } + for (var _b = 0, newInputs_1 = newInputs; _b < newInputs_1.length; _b++) { + var newInput = newInputs_1[_b]; + context.calendarApi.addEventSource(newInput); + } + } + + function handleDateProfile(dateProfile, context) { + context.emitter.trigger('datesSet', __assign(__assign({}, buildRangeApiWithTimeZone(dateProfile.activeRange, context.dateEnv)), { view: context.viewApi })); + } + + function handleEventStore(eventStore, context) { + var emitter = context.emitter; + if (emitter.hasHandlers('eventsSet')) { + emitter.trigger('eventsSet', buildEventApis(eventStore, context)); + } + } + + /* + this array is exposed on the root namespace so that UMD plugins can add to it. + see the rollup-bundles script. + */ + var globalPlugins = [ + arrayEventSourcePlugin, + funcEventSourcePlugin, + jsonFeedEventSourcePlugin, + simpleRecurringEventsPlugin, + changeHandlerPlugin, + createPlugin({ + isLoadingFuncs: [ + function (state) { return computeEventSourcesLoading(state.eventSources); }, + ], + contentTypeHandlers: { + html: function () { return ({ render: injectHtml }); }, + domNodes: function () { return ({ render: injectDomNodes }); }, + }, + propSetHandlers: { + dateProfile: handleDateProfile, + eventStore: handleEventStore, + }, + }), + ]; + function injectHtml(el, html) { + el.innerHTML = html; + } + function injectDomNodes(el, domNodes) { + var oldNodes = Array.prototype.slice.call(el.childNodes); // TODO: use array util + var newNodes = Array.prototype.slice.call(domNodes); // TODO: use array util + if (!isArraysEqual(oldNodes, newNodes)) { + for (var _i = 0, newNodes_1 = newNodes; _i < newNodes_1.length; _i++) { + var newNode = newNodes_1[_i]; + el.appendChild(newNode); + } + oldNodes.forEach(removeElement); + } + } + + var DelayedRunner = /** @class */ (function () { + function DelayedRunner(drainedOption) { + this.drainedOption = drainedOption; + this.isRunning = false; + this.isDirty = false; + this.pauseDepths = {}; + this.timeoutId = 0; + } + DelayedRunner.prototype.request = function (delay) { + this.isDirty = true; + if (!this.isPaused()) { + this.clearTimeout(); + if (delay == null) { + this.tryDrain(); + } + else { + this.timeoutId = setTimeout(// NOT OPTIMAL! TODO: look at debounce + this.tryDrain.bind(this), delay); + } + } + }; + DelayedRunner.prototype.pause = function (scope) { + if (scope === void 0) { scope = ''; } + var pauseDepths = this.pauseDepths; + pauseDepths[scope] = (pauseDepths[scope] || 0) + 1; + this.clearTimeout(); + }; + DelayedRunner.prototype.resume = function (scope, force) { + if (scope === void 0) { scope = ''; } + var pauseDepths = this.pauseDepths; + if (scope in pauseDepths) { + if (force) { + delete pauseDepths[scope]; + } + else { + pauseDepths[scope] -= 1; + var depth = pauseDepths[scope]; + if (depth <= 0) { + delete pauseDepths[scope]; + } + } + this.tryDrain(); + } + }; + DelayedRunner.prototype.isPaused = function () { + return Object.keys(this.pauseDepths).length; + }; + DelayedRunner.prototype.tryDrain = function () { + if (!this.isRunning && !this.isPaused()) { + this.isRunning = true; + while (this.isDirty) { + this.isDirty = false; + this.drained(); // might set isDirty to true again + } + this.isRunning = false; + } + }; + DelayedRunner.prototype.clear = function () { + this.clearTimeout(); + this.isDirty = false; + this.pauseDepths = {}; + }; + DelayedRunner.prototype.clearTimeout = function () { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = 0; + } + }; + DelayedRunner.prototype.drained = function () { + if (this.drainedOption) { + this.drainedOption(); + } + }; + return DelayedRunner; + }()); + + var TaskRunner = /** @class */ (function () { + function TaskRunner(runTaskOption, drainedOption) { + this.runTaskOption = runTaskOption; + this.drainedOption = drainedOption; + this.queue = []; + this.delayedRunner = new DelayedRunner(this.drain.bind(this)); + } + TaskRunner.prototype.request = function (task, delay) { + this.queue.push(task); + this.delayedRunner.request(delay); + }; + TaskRunner.prototype.pause = function (scope) { + this.delayedRunner.pause(scope); + }; + TaskRunner.prototype.resume = function (scope, force) { + this.delayedRunner.resume(scope, force); + }; + TaskRunner.prototype.drain = function () { + var queue = this.queue; + while (queue.length) { + var completedTasks = []; + var task = void 0; + while ((task = queue.shift())) { + this.runTask(task); + completedTasks.push(task); + } + this.drained(completedTasks); + } // keep going, in case new tasks were added in the drained handler + }; + TaskRunner.prototype.runTask = function (task) { + if (this.runTaskOption) { + this.runTaskOption(task); + } + }; + TaskRunner.prototype.drained = function (completedTasks) { + if (this.drainedOption) { + this.drainedOption(completedTasks); + } + }; + return TaskRunner; + }()); + + // Computes what the title at the top of the calendarApi should be for this view + function buildTitle(dateProfile, viewOptions, dateEnv) { + var range; + // for views that span a large unit of time, show the proper interval, ignoring stray days before and after + if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) { + range = dateProfile.currentRange; + } + else { // for day units or smaller, use the actual day range + range = dateProfile.activeRange; + } + return dateEnv.formatRange(range.start, range.end, createFormatter(viewOptions.titleFormat || buildTitleFormat(dateProfile)), { + isEndExclusive: dateProfile.isRangeAllDay, + defaultSeparator: viewOptions.titleRangeSeparator, + }); + } + // Generates the format string that should be used to generate the title for the current date range. + // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. + function buildTitleFormat(dateProfile) { + var currentRangeUnit = dateProfile.currentRangeUnit; + if (currentRangeUnit === 'year') { + return { year: 'numeric' }; + } + if (currentRangeUnit === 'month') { + return { year: 'numeric', month: 'long' }; // like "September 2014" + } + var days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end); + if (days !== null && days > 1) { + // multi-day range. shorter, like "Sep 9 - 10 2014" + return { year: 'numeric', month: 'short', day: 'numeric' }; + } + // one day. longer, like "September 9 2014" + return { year: 'numeric', month: 'long', day: 'numeric' }; + } + + // in future refactor, do the redux-style function(state=initial) for initial-state + // also, whatever is happening in constructor, have it happen in action queue too + var CalendarDataManager = /** @class */ (function () { + function CalendarDataManager(props) { + var _this = this; + this.computeOptionsData = memoize(this._computeOptionsData); + this.computeCurrentViewData = memoize(this._computeCurrentViewData); + this.organizeRawLocales = memoize(organizeRawLocales); + this.buildLocale = memoize(buildLocale); + this.buildPluginHooks = buildBuildPluginHooks(); + this.buildDateEnv = memoize(buildDateEnv); + this.buildTheme = memoize(buildTheme); + this.parseToolbars = memoize(parseToolbars); + this.buildViewSpecs = memoize(buildViewSpecs); + this.buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator); + this.buildViewApi = memoize(buildViewApi); + this.buildViewUiProps = memoizeObjArg(buildViewUiProps); + this.buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqual); + this.buildEventUiBases = memoize(buildEventUiBases); + this.parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours); + this.buildTitle = memoize(buildTitle); + this.emitter = new Emitter(); + this.actionRunner = new TaskRunner(this._handleAction.bind(this), this.updateData.bind(this)); + this.currentCalendarOptionsInput = {}; + this.currentCalendarOptionsRefined = {}; + this.currentViewOptionsInput = {}; + this.currentViewOptionsRefined = {}; + this.currentCalendarOptionsRefiners = {}; + this.getCurrentData = function () { return _this.data; }; + this.dispatch = function (action) { + _this.actionRunner.request(action); // protects against recursive calls to _handleAction + }; + this.props = props; + this.actionRunner.pause(); + var dynamicOptionOverrides = {}; + var optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi); + var currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView; + var currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides); + // wire things up + // TODO: not DRY + props.calendarApi.currentDataManager = this; + this.emitter.setThisContext(props.calendarApi); + this.emitter.setOptions(currentViewData.options); + var currentDate = getInitialDate(optionsData.calendarOptions, optionsData.dateEnv); + var dateProfile = currentViewData.dateProfileGenerator.build(currentDate); + if (!rangeContainsMarker(dateProfile.activeRange, currentDate)) { + currentDate = dateProfile.currentRange.start; + } + var calendarContext = { + dateEnv: optionsData.dateEnv, + options: optionsData.calendarOptions, + pluginHooks: optionsData.pluginHooks, + calendarApi: props.calendarApi, + dispatch: this.dispatch, + emitter: this.emitter, + getCurrentData: this.getCurrentData, + }; + // needs to be after setThisContext + for (var _i = 0, _a = optionsData.pluginHooks.contextInit; _i < _a.length; _i++) { + var callback = _a[_i]; + callback(calendarContext); + } + // NOT DRY + var eventSources = initEventSources(optionsData.calendarOptions, dateProfile, calendarContext); + var initialState = { + dynamicOptionOverrides: dynamicOptionOverrides, + currentViewType: currentViewType, + currentDate: currentDate, + dateProfile: dateProfile, + businessHours: this.parseContextBusinessHours(calendarContext), + eventSources: eventSources, + eventUiBases: {}, + eventStore: createEmptyEventStore(), + renderableEventStore: createEmptyEventStore(), + dateSelection: null, + eventSelection: '', + eventDrag: null, + eventResize: null, + selectionConfig: this.buildViewUiProps(calendarContext).selectionConfig, + }; + var contextAndState = __assign(__assign({}, calendarContext), initialState); + for (var _b = 0, _c = optionsData.pluginHooks.reducers; _b < _c.length; _b++) { + var reducer = _c[_b]; + __assign(initialState, reducer(null, null, contextAndState)); + } + if (computeIsLoading(initialState, calendarContext)) { + this.emitter.trigger('loading', true); // NOT DRY + } + this.state = initialState; + this.updateData(); + this.actionRunner.resume(); + } + CalendarDataManager.prototype.resetOptions = function (optionOverrides, append) { + var props = this.props; + props.optionOverrides = append + ? __assign(__assign({}, props.optionOverrides), optionOverrides) : optionOverrides; + this.actionRunner.request({ + type: 'NOTHING', + }); + }; + CalendarDataManager.prototype._handleAction = function (action) { + var _a = this, props = _a.props, state = _a.state, emitter = _a.emitter; + var dynamicOptionOverrides = reduceDynamicOptionOverrides(state.dynamicOptionOverrides, action); + var optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi); + var currentViewType = reduceViewType(state.currentViewType, action); + var currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides); + // wire things up + // TODO: not DRY + props.calendarApi.currentDataManager = this; + emitter.setThisContext(props.calendarApi); + emitter.setOptions(currentViewData.options); + var calendarContext = { + dateEnv: optionsData.dateEnv, + options: optionsData.calendarOptions, + pluginHooks: optionsData.pluginHooks, + calendarApi: props.calendarApi, + dispatch: this.dispatch, + emitter: emitter, + getCurrentData: this.getCurrentData, + }; + var currentDate = state.currentDate, dateProfile = state.dateProfile; + if (this.data && this.data.dateProfileGenerator !== currentViewData.dateProfileGenerator) { // hack + dateProfile = currentViewData.dateProfileGenerator.build(currentDate); + } + currentDate = reduceCurrentDate(currentDate, action); + dateProfile = reduceDateProfile(dateProfile, action, currentDate, currentViewData.dateProfileGenerator); + if (action.type === 'PREV' || // TODO: move this logic into DateProfileGenerator + action.type === 'NEXT' || // " + !rangeContainsMarker(dateProfile.currentRange, currentDate)) { + currentDate = dateProfile.currentRange.start; + } + var eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendarContext); + var eventStore = reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendarContext); + var isEventsLoading = computeEventSourcesLoading(eventSources); // BAD. also called in this func in computeIsLoading + var renderableEventStore = (isEventsLoading && !currentViewData.options.progressiveEventRendering) ? + (state.renderableEventStore || eventStore) : // try from previous state + eventStore; + var _b = this.buildViewUiProps(calendarContext), eventUiSingleBase = _b.eventUiSingleBase, selectionConfig = _b.selectionConfig; // will memoize obj + var eventUiBySource = this.buildEventUiBySource(eventSources); + var eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource); + var newState = { + dynamicOptionOverrides: dynamicOptionOverrides, + currentViewType: currentViewType, + currentDate: currentDate, + dateProfile: dateProfile, + eventSources: eventSources, + eventStore: eventStore, + renderableEventStore: renderableEventStore, + selectionConfig: selectionConfig, + eventUiBases: eventUiBases, + businessHours: this.parseContextBusinessHours(calendarContext), + dateSelection: reduceDateSelection(state.dateSelection, action), + eventSelection: reduceSelectedEvent(state.eventSelection, action), + eventDrag: reduceEventDrag(state.eventDrag, action), + eventResize: reduceEventResize(state.eventResize, action), + }; + var contextAndState = __assign(__assign({}, calendarContext), newState); + for (var _i = 0, _c = optionsData.pluginHooks.reducers; _i < _c.length; _i++) { + var reducer = _c[_i]; + __assign(newState, reducer(state, action, contextAndState)); // give the OLD state, for old value + } + var wasLoading = computeIsLoading(state, calendarContext); + var isLoading = computeIsLoading(newState, calendarContext); + // TODO: use propSetHandlers in plugin system + if (!wasLoading && isLoading) { + emitter.trigger('loading', true); + } + else if (wasLoading && !isLoading) { + emitter.trigger('loading', false); + } + this.state = newState; + if (props.onAction) { + props.onAction(action); + } + }; + CalendarDataManager.prototype.updateData = function () { + var _a = this, props = _a.props, state = _a.state; + var oldData = this.data; + var optionsData = this.computeOptionsData(props.optionOverrides, state.dynamicOptionOverrides, props.calendarApi); + var currentViewData = this.computeCurrentViewData(state.currentViewType, optionsData, props.optionOverrides, state.dynamicOptionOverrides); + var data = this.data = __assign(__assign(__assign({ viewTitle: this.buildTitle(state.dateProfile, currentViewData.options, optionsData.dateEnv), calendarApi: props.calendarApi, dispatch: this.dispatch, emitter: this.emitter, getCurrentData: this.getCurrentData }, optionsData), currentViewData), state); + var changeHandlers = optionsData.pluginHooks.optionChangeHandlers; + var oldCalendarOptions = oldData && oldData.calendarOptions; + var newCalendarOptions = optionsData.calendarOptions; + if (oldCalendarOptions && oldCalendarOptions !== newCalendarOptions) { + if (oldCalendarOptions.timeZone !== newCalendarOptions.timeZone) { + // hack + state.eventSources = data.eventSources = reduceEventSourcesNewTimeZone(data.eventSources, state.dateProfile, data); + state.eventStore = data.eventStore = rezoneEventStoreDates(data.eventStore, oldData.dateEnv, data.dateEnv); + } + for (var optionName in changeHandlers) { + if (oldCalendarOptions[optionName] !== newCalendarOptions[optionName]) { + changeHandlers[optionName](newCalendarOptions[optionName], data); + } + } + } + if (props.onData) { + props.onData(data); + } + }; + CalendarDataManager.prototype._computeOptionsData = function (optionOverrides, dynamicOptionOverrides, calendarApi) { + // TODO: blacklist options that are handled by optionChangeHandlers + var _a = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides), refinedOptions = _a.refinedOptions, pluginHooks = _a.pluginHooks, localeDefaults = _a.localeDefaults, availableLocaleData = _a.availableLocaleData, extra = _a.extra; + warnUnknownOptions(extra); + var dateEnv = this.buildDateEnv(refinedOptions.timeZone, refinedOptions.locale, refinedOptions.weekNumberCalculation, refinedOptions.firstDay, refinedOptions.weekText, pluginHooks, availableLocaleData, refinedOptions.defaultRangeSeparator); + var viewSpecs = this.buildViewSpecs(pluginHooks.views, optionOverrides, dynamicOptionOverrides, localeDefaults); + var theme = this.buildTheme(refinedOptions, pluginHooks); + var toolbarConfig = this.parseToolbars(refinedOptions, optionOverrides, theme, viewSpecs, calendarApi); + return { + calendarOptions: refinedOptions, + pluginHooks: pluginHooks, + dateEnv: dateEnv, + viewSpecs: viewSpecs, + theme: theme, + toolbarConfig: toolbarConfig, + localeDefaults: localeDefaults, + availableRawLocales: availableLocaleData.map, + }; + }; + // always called from behind a memoizer + CalendarDataManager.prototype.processRawCalendarOptions = function (optionOverrides, dynamicOptionOverrides) { + var _a = mergeRawOptions([ + BASE_OPTION_DEFAULTS, + optionOverrides, + dynamicOptionOverrides, + ]), locales = _a.locales, locale = _a.locale; + var availableLocaleData = this.organizeRawLocales(locales); + var availableRawLocales = availableLocaleData.map; + var localeDefaults = this.buildLocale(locale || availableLocaleData.defaultCode, availableRawLocales).options; + var pluginHooks = this.buildPluginHooks(optionOverrides.plugins || [], globalPlugins); + var refiners = this.currentCalendarOptionsRefiners = __assign(__assign(__assign(__assign(__assign({}, BASE_OPTION_REFINERS), CALENDAR_LISTENER_REFINERS), CALENDAR_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners); + var extra = {}; + var raw = mergeRawOptions([ + BASE_OPTION_DEFAULTS, + localeDefaults, + optionOverrides, + dynamicOptionOverrides, + ]); + var refined = {}; + var currentRaw = this.currentCalendarOptionsInput; + var currentRefined = this.currentCalendarOptionsRefined; + var anyChanges = false; + for (var optionName in raw) { + if (optionName !== 'plugins') { // because plugins is special-cased + if (raw[optionName] === currentRaw[optionName] || + (COMPLEX_OPTION_COMPARATORS[optionName] && + (optionName in currentRaw) && + COMPLEX_OPTION_COMPARATORS[optionName](currentRaw[optionName], raw[optionName]))) { + refined[optionName] = currentRefined[optionName]; + } + else if (refiners[optionName]) { + refined[optionName] = refiners[optionName](raw[optionName]); + anyChanges = true; + } + else { + extra[optionName] = currentRaw[optionName]; + } + } + } + if (anyChanges) { + this.currentCalendarOptionsInput = raw; + this.currentCalendarOptionsRefined = refined; + } + return { + rawOptions: this.currentCalendarOptionsInput, + refinedOptions: this.currentCalendarOptionsRefined, + pluginHooks: pluginHooks, + availableLocaleData: availableLocaleData, + localeDefaults: localeDefaults, + extra: extra, + }; + }; + CalendarDataManager.prototype._computeCurrentViewData = function (viewType, optionsData, optionOverrides, dynamicOptionOverrides) { + var viewSpec = optionsData.viewSpecs[viewType]; + if (!viewSpec) { + throw new Error("viewType \"" + viewType + "\" is not available. Please make sure you've loaded all neccessary plugins"); + } + var _a = this.processRawViewOptions(viewSpec, optionsData.pluginHooks, optionsData.localeDefaults, optionOverrides, dynamicOptionOverrides), refinedOptions = _a.refinedOptions, extra = _a.extra; + warnUnknownOptions(extra); + var dateProfileGenerator = this.buildDateProfileGenerator({ + dateProfileGeneratorClass: viewSpec.optionDefaults.dateProfileGeneratorClass, + duration: viewSpec.duration, + durationUnit: viewSpec.durationUnit, + usesMinMaxTime: viewSpec.optionDefaults.usesMinMaxTime, + dateEnv: optionsData.dateEnv, + calendarApi: this.props.calendarApi, + slotMinTime: refinedOptions.slotMinTime, + slotMaxTime: refinedOptions.slotMaxTime, + showNonCurrentDates: refinedOptions.showNonCurrentDates, + dayCount: refinedOptions.dayCount, + dateAlignment: refinedOptions.dateAlignment, + dateIncrement: refinedOptions.dateIncrement, + hiddenDays: refinedOptions.hiddenDays, + weekends: refinedOptions.weekends, + nowInput: refinedOptions.now, + validRangeInput: refinedOptions.validRange, + visibleRangeInput: refinedOptions.visibleRange, + monthMode: refinedOptions.monthMode, + fixedWeekCount: refinedOptions.fixedWeekCount, + }); + var viewApi = this.buildViewApi(viewType, this.getCurrentData, optionsData.dateEnv); + return { viewSpec: viewSpec, options: refinedOptions, dateProfileGenerator: dateProfileGenerator, viewApi: viewApi }; + }; + CalendarDataManager.prototype.processRawViewOptions = function (viewSpec, pluginHooks, localeDefaults, optionOverrides, dynamicOptionOverrides) { + var raw = mergeRawOptions([ + BASE_OPTION_DEFAULTS, + viewSpec.optionDefaults, + localeDefaults, + optionOverrides, + viewSpec.optionOverrides, + dynamicOptionOverrides, + ]); + var refiners = __assign(__assign(__assign(__assign(__assign(__assign({}, BASE_OPTION_REFINERS), CALENDAR_LISTENER_REFINERS), CALENDAR_OPTION_REFINERS), VIEW_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners); + var refined = {}; + var currentRaw = this.currentViewOptionsInput; + var currentRefined = this.currentViewOptionsRefined; + var anyChanges = false; + var extra = {}; + for (var optionName in raw) { + if (raw[optionName] === currentRaw[optionName]) { + refined[optionName] = currentRefined[optionName]; + } + else { + if (raw[optionName] === this.currentCalendarOptionsInput[optionName]) { + if (optionName in this.currentCalendarOptionsRefined) { // might be an "extra" prop + refined[optionName] = this.currentCalendarOptionsRefined[optionName]; + } + } + else if (refiners[optionName]) { + refined[optionName] = refiners[optionName](raw[optionName]); + } + else { + extra[optionName] = raw[optionName]; + } + anyChanges = true; + } + } + if (anyChanges) { + this.currentViewOptionsInput = raw; + this.currentViewOptionsRefined = refined; + } + return { + rawOptions: this.currentViewOptionsInput, + refinedOptions: this.currentViewOptionsRefined, + extra: extra, + }; + }; + return CalendarDataManager; + }()); + function buildDateEnv(timeZone, explicitLocale, weekNumberCalculation, firstDay, weekText, pluginHooks, availableLocaleData, defaultSeparator) { + var locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map); + return new DateEnv({ + calendarSystem: 'gregory', + timeZone: timeZone, + namedTimeZoneImpl: pluginHooks.namedTimeZonedImpl, + locale: locale, + weekNumberCalculation: weekNumberCalculation, + firstDay: firstDay, + weekText: weekText, + cmdFormatter: pluginHooks.cmdFormatter, + defaultSeparator: defaultSeparator, + }); + } + function buildTheme(options, pluginHooks) { + var ThemeClass = pluginHooks.themeClasses[options.themeSystem] || StandardTheme; + return new ThemeClass(options); + } + function buildDateProfileGenerator(props) { + var DateProfileGeneratorClass = props.dateProfileGeneratorClass || DateProfileGenerator; + return new DateProfileGeneratorClass(props); + } + function buildViewApi(type, getCurrentData, dateEnv) { + return new ViewApi(type, getCurrentData, dateEnv); + } + function buildEventUiBySource(eventSources) { + return mapHash(eventSources, function (eventSource) { return eventSource.ui; }); + } + function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) { + var eventUiBases = { '': eventUiSingleBase }; + for (var defId in eventDefs) { + var def = eventDefs[defId]; + if (def.sourceId && eventUiBySource[def.sourceId]) { + eventUiBases[defId] = eventUiBySource[def.sourceId]; + } + } + return eventUiBases; + } + function buildViewUiProps(calendarContext) { + var options = calendarContext.options; + return { + eventUiSingleBase: createEventUi({ + display: options.eventDisplay, + editable: options.editable, + startEditable: options.eventStartEditable, + durationEditable: options.eventDurationEditable, + constraint: options.eventConstraint, + overlap: typeof options.eventOverlap === 'boolean' ? options.eventOverlap : undefined, + allow: options.eventAllow, + backgroundColor: options.eventBackgroundColor, + borderColor: options.eventBorderColor, + textColor: options.eventTextColor, + color: options.eventColor, + // classNames: options.eventClassNames // render hook will handle this + }, calendarContext), + selectionConfig: createEventUi({ + constraint: options.selectConstraint, + overlap: typeof options.selectOverlap === 'boolean' ? options.selectOverlap : undefined, + allow: options.selectAllow, + }, calendarContext), + }; + } + function computeIsLoading(state, context) { + for (var _i = 0, _a = context.pluginHooks.isLoadingFuncs; _i < _a.length; _i++) { + var isLoadingFunc = _a[_i]; + if (isLoadingFunc(state)) { + return true; + } + } + return false; + } + function parseContextBusinessHours(calendarContext) { + return parseBusinessHours(calendarContext.options.businessHours, calendarContext); + } + function warnUnknownOptions(options, viewName) { + for (var optionName in options) { + console.warn("Unknown option '" + optionName + "'" + + (viewName ? " for view '" + viewName + "'" : '')); + } + } + + // TODO: move this to react plugin? + var CalendarDataProvider = /** @class */ (function (_super) { + __extends(CalendarDataProvider, _super); + function CalendarDataProvider(props) { + var _this = _super.call(this, props) || this; + _this.handleData = function (data) { + if (!_this.dataManager) { // still within initial run, before assignment in constructor + // eslint-disable-next-line react/no-direct-mutation-state + _this.state = data; // can't use setState yet + } + else { + _this.setState(data); + } + }; + _this.dataManager = new CalendarDataManager({ + optionOverrides: props.optionOverrides, + calendarApi: props.calendarApi, + onData: _this.handleData, + }); + return _this; + } + CalendarDataProvider.prototype.render = function () { + return this.props.children(this.state); + }; + CalendarDataProvider.prototype.componentDidUpdate = function (prevProps) { + var newOptionOverrides = this.props.optionOverrides; + if (newOptionOverrides !== prevProps.optionOverrides) { // prevent recursive handleData + this.dataManager.resetOptions(newOptionOverrides); + } + }; + return CalendarDataProvider; + }(Component)); + + // HELPERS + /* + if nextDayThreshold is specified, slicing is done in an all-day fashion. + you can get nextDayThreshold from context.nextDayThreshold + */ + function sliceEvents(props, allDay) { + return sliceEventStore(props.eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? props.nextDayThreshold : null).fg; + } + + var NamedTimeZoneImpl = /** @class */ (function () { + function NamedTimeZoneImpl(timeZoneName) { + this.timeZoneName = timeZoneName; + } + return NamedTimeZoneImpl; + }()); + + var SegHierarchy = /** @class */ (function () { + function SegHierarchy() { + // settings + this.strictOrder = false; + this.allowReslicing = false; + this.maxCoord = -1; // -1 means no max + this.maxStackCnt = -1; // -1 means no max + this.levelCoords = []; // ordered + this.entriesByLevel = []; // parallel with levelCoords + this.stackCnts = {}; // TODO: use better technique!? + } + SegHierarchy.prototype.addSegs = function (inputs) { + var hiddenEntries = []; + for (var _i = 0, inputs_1 = inputs; _i < inputs_1.length; _i++) { + var input = inputs_1[_i]; + this.insertEntry(input, hiddenEntries); + } + return hiddenEntries; + }; + SegHierarchy.prototype.insertEntry = function (entry, hiddenEntries) { + var insertion = this.findInsertion(entry); + if (this.isInsertionValid(insertion, entry)) { + this.insertEntryAt(entry, insertion); + return 1; + } + return this.handleInvalidInsertion(insertion, entry, hiddenEntries); + }; + SegHierarchy.prototype.isInsertionValid = function (insertion, entry) { + return (this.maxCoord === -1 || insertion.levelCoord + entry.thickness <= this.maxCoord) && + (this.maxStackCnt === -1 || insertion.stackCnt < this.maxStackCnt); + }; + // returns number of new entries inserted + SegHierarchy.prototype.handleInvalidInsertion = function (insertion, entry, hiddenEntries) { + if (this.allowReslicing && insertion.touchingEntry) { + return this.splitEntry(entry, insertion.touchingEntry, hiddenEntries); + } + hiddenEntries.push(entry); + return 0; + }; + SegHierarchy.prototype.splitEntry = function (entry, barrier, hiddenEntries) { + var partCnt = 0; + var splitHiddenEntries = []; + var entrySpan = entry.span; + var barrierSpan = barrier.span; + if (entrySpan.start < barrierSpan.start) { + partCnt += this.insertEntry({ + index: entry.index, + thickness: entry.thickness, + span: { start: entrySpan.start, end: barrierSpan.start }, + }, splitHiddenEntries); + } + if (entrySpan.end > barrierSpan.end) { + partCnt += this.insertEntry({ + index: entry.index, + thickness: entry.thickness, + span: { start: barrierSpan.end, end: entrySpan.end }, + }, splitHiddenEntries); + } + if (partCnt) { + hiddenEntries.push.apply(hiddenEntries, __spreadArray([{ + index: entry.index, + thickness: entry.thickness, + span: intersectSpans(barrierSpan, entrySpan), // guaranteed to intersect + }], splitHiddenEntries)); + return partCnt; + } + hiddenEntries.push(entry); + return 0; + }; + SegHierarchy.prototype.insertEntryAt = function (entry, insertion) { + var _a = this, entriesByLevel = _a.entriesByLevel, levelCoords = _a.levelCoords; + if (insertion.lateral === -1) { + // create a new level + insertAt(levelCoords, insertion.level, insertion.levelCoord); + insertAt(entriesByLevel, insertion.level, [entry]); + } + else { + // insert into existing level + insertAt(entriesByLevel[insertion.level], insertion.lateral, entry); + } + this.stackCnts[buildEntryKey(entry)] = insertion.stackCnt; + }; + SegHierarchy.prototype.findInsertion = function (newEntry) { + var _a = this, levelCoords = _a.levelCoords, entriesByLevel = _a.entriesByLevel, strictOrder = _a.strictOrder, stackCnts = _a.stackCnts; + var levelCnt = levelCoords.length; + var candidateCoord = 0; + var touchingLevel = -1; + var touchingLateral = -1; + var touchingEntry = null; + var stackCnt = 0; + for (var trackingLevel = 0; trackingLevel < levelCnt; trackingLevel += 1) { + var trackingCoord = levelCoords[trackingLevel]; + // if the current level is past the placed entry, we have found a good empty space and can stop. + // if strictOrder, keep finding more lateral intersections. + if (!strictOrder && trackingCoord >= candidateCoord + newEntry.thickness) { + break; + } + var trackingEntries = entriesByLevel[trackingLevel]; + var trackingEntry = void 0; + var searchRes = binarySearch(trackingEntries, newEntry.span.start, getEntrySpanEnd); // find first entry after newEntry's end + var lateralIndex = searchRes[0] + searchRes[1]; // if exact match (which doesn't collide), go to next one + while ( // loop through entries that horizontally intersect + (trackingEntry = trackingEntries[lateralIndex]) && // but not past the whole entry list + trackingEntry.span.start < newEntry.span.end // and not entirely past newEntry + ) { + var trackingEntryBottom = trackingCoord + trackingEntry.thickness; + // intersects into the top of the candidate? + if (trackingEntryBottom > candidateCoord) { + candidateCoord = trackingEntryBottom; + touchingEntry = trackingEntry; + touchingLevel = trackingLevel; + touchingLateral = lateralIndex; + } + // butts up against top of candidate? (will happen if just intersected as well) + if (trackingEntryBottom === candidateCoord) { + // accumulate the highest possible stackCnt of the trackingEntries that butt up + stackCnt = Math.max(stackCnt, stackCnts[buildEntryKey(trackingEntry)] + 1); + } + lateralIndex += 1; + } + } + // the destination level will be after touchingEntry's level. find it + var destLevel = 0; + if (touchingEntry) { + destLevel = touchingLevel + 1; + while (destLevel < levelCnt && levelCoords[destLevel] < candidateCoord) { + destLevel += 1; + } + } + // if adding to an existing level, find where to insert + var destLateral = -1; + if (destLevel < levelCnt && levelCoords[destLevel] === candidateCoord) { + destLateral = binarySearch(entriesByLevel[destLevel], newEntry.span.end, getEntrySpanEnd)[0]; + } + return { + touchingLevel: touchingLevel, + touchingLateral: touchingLateral, + touchingEntry: touchingEntry, + stackCnt: stackCnt, + levelCoord: candidateCoord, + level: destLevel, + lateral: destLateral, + }; + }; + // sorted by levelCoord (lowest to highest) + SegHierarchy.prototype.toRects = function () { + var _a = this, entriesByLevel = _a.entriesByLevel, levelCoords = _a.levelCoords; + var levelCnt = entriesByLevel.length; + var rects = []; + for (var level = 0; level < levelCnt; level += 1) { + var entries = entriesByLevel[level]; + var levelCoord = levelCoords[level]; + for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) { + var entry = entries_1[_i]; + rects.push(__assign(__assign({}, entry), { levelCoord: levelCoord })); + } + } + return rects; + }; + return SegHierarchy; + }()); + function getEntrySpanEnd(entry) { + return entry.span.end; + } + function buildEntryKey(entry) { + return entry.index + ':' + entry.span.start; + } + // returns groups with entries sorted by input order + function groupIntersectingEntries(entries) { + var merges = []; + for (var _i = 0, entries_2 = entries; _i < entries_2.length; _i++) { + var entry = entries_2[_i]; + var filteredMerges = []; + var hungryMerge = { + span: entry.span, + entries: [entry], + }; + for (var _a = 0, merges_1 = merges; _a < merges_1.length; _a++) { + var merge = merges_1[_a]; + if (intersectSpans(merge.span, hungryMerge.span)) { + hungryMerge = { + entries: merge.entries.concat(hungryMerge.entries), + span: joinSpans(merge.span, hungryMerge.span), + }; + } + else { + filteredMerges.push(merge); + } + } + filteredMerges.push(hungryMerge); + merges = filteredMerges; + } + return merges; + } + function joinSpans(span0, span1) { + return { + start: Math.min(span0.start, span1.start), + end: Math.max(span0.end, span1.end), + }; + } + function intersectSpans(span0, span1) { + var start = Math.max(span0.start, span1.start); + var end = Math.min(span0.end, span1.end); + if (start < end) { + return { start: start, end: end }; + } + return null; + } + // general util + // --------------------------------------------------------------------------------------------------------------------- + function insertAt(arr, index, item) { + arr.splice(index, 0, item); + } + function binarySearch(a, searchVal, getItemVal) { + var startIndex = 0; + var endIndex = a.length; // exclusive + if (!endIndex || searchVal < getItemVal(a[startIndex])) { // no items OR before first item + return [0, 0]; + } + if (searchVal > getItemVal(a[endIndex - 1])) { // after last item + return [endIndex, 0]; + } + while (startIndex < endIndex) { + var middleIndex = Math.floor(startIndex + (endIndex - startIndex) / 2); + var middleVal = getItemVal(a[middleIndex]); + if (searchVal < middleVal) { + endIndex = middleIndex; + } + else if (searchVal > middleVal) { + startIndex = middleIndex + 1; + } + else { // equal! + return [middleIndex, 1]; + } + } + return [startIndex, 0]; + } + + var Interaction = /** @class */ (function () { + function Interaction(settings) { + this.component = settings.component; + this.isHitComboAllowed = settings.isHitComboAllowed || null; + } + Interaction.prototype.destroy = function () { + }; + return Interaction; + }()); + function parseInteractionSettings(component, input) { + return { + component: component, + el: input.el, + useEventCenter: input.useEventCenter != null ? input.useEventCenter : true, + isHitComboAllowed: input.isHitComboAllowed || null, + }; + } + function interactionSettingsToStore(settings) { + var _a; + return _a = {}, + _a[settings.component.uid] = settings, + _a; + } + // global state + var interactionSettingsStore = {}; + + /* + An abstraction for a dragging interaction originating on an event. + Does higher-level things than PointerDragger, such as possibly: + - a "mirror" that moves with the pointer + - a minimum number of pixels or other criteria for a true drag to begin + + subclasses must emit: + - pointerdown + - dragstart + - dragmove + - pointerup + - dragend + */ + var ElementDragging = /** @class */ (function () { + function ElementDragging(el, selector) { + this.emitter = new Emitter(); + } + ElementDragging.prototype.destroy = function () { + }; + ElementDragging.prototype.setMirrorIsVisible = function (bool) { + // optional if subclass doesn't want to support a mirror + }; + ElementDragging.prototype.setMirrorNeedsRevert = function (bool) { + // optional if subclass doesn't want to support a mirror + }; + ElementDragging.prototype.setAutoScrollEnabled = function (bool) { + // optional + }; + return ElementDragging; + }()); + + // TODO: get rid of this in favor of options system, + // tho it's really easy to access this globally rather than pass thru options. + var config = {}; + + /* + Information about what will happen when an external element is dragged-and-dropped + onto a calendar. Contains information for creating an event. + */ + var DRAG_META_REFINERS = { + startTime: createDuration, + duration: createDuration, + create: Boolean, + sourceId: String, + }; + function parseDragMeta(raw) { + var _a = refineProps(raw, DRAG_META_REFINERS), refined = _a.refined, extra = _a.extra; + return { + startTime: refined.startTime || null, + duration: refined.duration || null, + create: refined.create != null ? refined.create : true, + sourceId: refined.sourceId, + leftoverProps: extra, + }; + } + + var ToolbarSection = /** @class */ (function (_super) { + __extends(ToolbarSection, _super); + function ToolbarSection() { + return _super !== null && _super.apply(this, arguments) || this; + } + ToolbarSection.prototype.render = function () { + var _this = this; + var children = this.props.widgetGroups.map(function (widgetGroup) { return _this.renderWidgetGroup(widgetGroup); }); + return createElement.apply(void 0, __spreadArray(['div', { className: 'fc-toolbar-chunk' }], children)); + }; + ToolbarSection.prototype.renderWidgetGroup = function (widgetGroup) { + var props = this.props; + var theme = this.context.theme; + var children = []; + var isOnlyButtons = true; + for (var _i = 0, widgetGroup_1 = widgetGroup; _i < widgetGroup_1.length; _i++) { + var widget = widgetGroup_1[_i]; + var buttonName = widget.buttonName, buttonClick = widget.buttonClick, buttonText = widget.buttonText, buttonIcon = widget.buttonIcon; + if (buttonName === 'title') { + isOnlyButtons = false; + children.push(createElement("h2", { className: "fc-toolbar-title" }, props.title)); + } + else { + var ariaAttrs = buttonIcon ? { 'aria-label': buttonName } : {}; + var buttonClasses = ["fc-" + buttonName + "-button", theme.getClass('button')]; + if (buttonName === props.activeButton) { + buttonClasses.push(theme.getClass('buttonActive')); + } + var isDisabled = (!props.isTodayEnabled && buttonName === 'today') || + (!props.isPrevEnabled && buttonName === 'prev') || + (!props.isNextEnabled && buttonName === 'next'); + children.push(createElement("button", __assign({ disabled: isDisabled, className: buttonClasses.join(' '), onClick: buttonClick, type: "button" }, ariaAttrs), buttonText || (buttonIcon ? createElement("span", { className: buttonIcon }) : ''))); + } + } + if (children.length > 1) { + var groupClassName = (isOnlyButtons && theme.getClass('buttonGroup')) || ''; + return createElement.apply(void 0, __spreadArray(['div', { className: groupClassName }], children)); + } + return children[0]; + }; + return ToolbarSection; + }(BaseComponent)); + + var Toolbar = /** @class */ (function (_super) { + __extends(Toolbar, _super); + function Toolbar() { + return _super !== null && _super.apply(this, arguments) || this; + } + Toolbar.prototype.render = function () { + var _a = this.props, model = _a.model, extraClassName = _a.extraClassName; + var forceLtr = false; + var startContent; + var endContent; + var centerContent = model.center; + if (model.left) { + forceLtr = true; + startContent = model.left; + } + else { + startContent = model.start; + } + if (model.right) { + forceLtr = true; + endContent = model.right; + } + else { + endContent = model.end; + } + var classNames = [ + extraClassName || '', + 'fc-toolbar', + forceLtr ? 'fc-toolbar-ltr' : '', + ]; + return (createElement("div", { className: classNames.join(' ') }, + this.renderSection('start', startContent || []), + this.renderSection('center', centerContent || []), + this.renderSection('end', endContent || []))); + }; + Toolbar.prototype.renderSection = function (key, widgetGroups) { + var props = this.props; + return (createElement(ToolbarSection, { key: key, widgetGroups: widgetGroups, title: props.title, activeButton: props.activeButton, isTodayEnabled: props.isTodayEnabled, isPrevEnabled: props.isPrevEnabled, isNextEnabled: props.isNextEnabled })); + }; + return Toolbar; + }(BaseComponent)); + + // TODO: do function component? + var ViewContainer = /** @class */ (function (_super) { + __extends(ViewContainer, _super); + function ViewContainer() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.state = { + availableWidth: null, + }; + _this.handleEl = function (el) { + _this.el = el; + setRef(_this.props.elRef, el); + _this.updateAvailableWidth(); + }; + _this.handleResize = function () { + _this.updateAvailableWidth(); + }; + return _this; + } + ViewContainer.prototype.render = function () { + var _a = this, props = _a.props, state = _a.state; + var aspectRatio = props.aspectRatio; + var classNames = [ + 'fc-view-harness', + (aspectRatio || props.liquid || props.height) + ? 'fc-view-harness-active' // harness controls the height + : 'fc-view-harness-passive', // let the view do the height + ]; + var height = ''; + var paddingBottom = ''; + if (aspectRatio) { + if (state.availableWidth !== null) { + height = state.availableWidth / aspectRatio; + } + else { + // while waiting to know availableWidth, we can't set height to *zero* + // because will cause lots of unnecessary scrollbars within scrollgrid. + // BETTER: don't start rendering ANYTHING yet until we know container width + // NOTE: why not always use paddingBottom? Causes height oscillation (issue 5606) + paddingBottom = (1 / aspectRatio) * 100 + "%"; + } + } + else { + height = props.height || ''; + } + return (createElement("div", { ref: this.handleEl, onClick: props.onClick, className: classNames.join(' '), style: { height: height, paddingBottom: paddingBottom } }, props.children)); + }; + ViewContainer.prototype.componentDidMount = function () { + this.context.addResizeHandler(this.handleResize); + }; + ViewContainer.prototype.componentWillUnmount = function () { + this.context.removeResizeHandler(this.handleResize); + }; + ViewContainer.prototype.updateAvailableWidth = function () { + if (this.el && // needed. but why? + this.props.aspectRatio // aspectRatio is the only height setting that needs availableWidth + ) { + this.setState({ availableWidth: this.el.offsetWidth }); + } + }; + return ViewContainer; + }(BaseComponent)); + + /* + Detects when the user clicks on an event within a DateComponent + */ + var EventClicking = /** @class */ (function (_super) { + __extends(EventClicking, _super); + function EventClicking(settings) { + var _this = _super.call(this, settings) || this; + _this.handleSegClick = function (ev, segEl) { + var component = _this.component; + var context = component.context; + var seg = getElSeg(segEl); + if (seg && // might be the
surrounding the more link + component.isValidSegDownEl(ev.target)) { + // our way to simulate a link click for elements that can't be tags + // grab before trigger fired in case trigger trashes DOM thru rerendering + var hasUrlContainer = elementClosest(ev.target, '.fc-event-forced-url'); + var url = hasUrlContainer ? hasUrlContainer.querySelector('a[href]').href : ''; + context.emitter.trigger('eventClick', { + el: segEl, + event: new EventApi(component.context, seg.eventRange.def, seg.eventRange.instance), + jsEvent: ev, + view: context.viewApi, + }); + if (url && !ev.defaultPrevented) { + window.location.href = url; + } + } + }; + _this.destroy = listenBySelector(settings.el, 'click', '.fc-event', // on both fg and bg events + _this.handleSegClick); + return _this; + } + return EventClicking; + }(Interaction)); + + /* + Triggers events and adds/removes core classNames when the user's pointer + enters/leaves event-elements of a component. + */ + var EventHovering = /** @class */ (function (_super) { + __extends(EventHovering, _super); + function EventHovering(settings) { + var _this = _super.call(this, settings) || this; + // for simulating an eventMouseLeave when the event el is destroyed while mouse is over it + _this.handleEventElRemove = function (el) { + if (el === _this.currentSegEl) { + _this.handleSegLeave(null, _this.currentSegEl); + } + }; + _this.handleSegEnter = function (ev, segEl) { + if (getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper + _this.currentSegEl = segEl; + _this.triggerEvent('eventMouseEnter', ev, segEl); + } + }; + _this.handleSegLeave = function (ev, segEl) { + if (_this.currentSegEl) { + _this.currentSegEl = null; + _this.triggerEvent('eventMouseLeave', ev, segEl); + } + }; + _this.removeHoverListeners = listenToHoverBySelector(settings.el, '.fc-event', // on both fg and bg events + _this.handleSegEnter, _this.handleSegLeave); + return _this; + } + EventHovering.prototype.destroy = function () { + this.removeHoverListeners(); + }; + EventHovering.prototype.triggerEvent = function (publicEvName, ev, segEl) { + var component = this.component; + var context = component.context; + var seg = getElSeg(segEl); + if (!ev || component.isValidSegDownEl(ev.target)) { + context.emitter.trigger(publicEvName, { + el: segEl, + event: new EventApi(context, seg.eventRange.def, seg.eventRange.instance), + jsEvent: ev, + view: context.viewApi, + }); + } + }; + return EventHovering; + }(Interaction)); + + var CalendarContent = /** @class */ (function (_super) { + __extends(CalendarContent, _super); + function CalendarContent() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.buildViewContext = memoize(buildViewContext); + _this.buildViewPropTransformers = memoize(buildViewPropTransformers); + _this.buildToolbarProps = memoize(buildToolbarProps); + _this.handleNavLinkClick = buildDelegationHandler('a[data-navlink]', _this._handleNavLinkClick.bind(_this)); + _this.headerRef = createRef(); + _this.footerRef = createRef(); + _this.interactionsStore = {}; + // Component Registration + // ----------------------------------------------------------------------------------------------------------------- + _this.registerInteractiveComponent = function (component, settingsInput) { + var settings = parseInteractionSettings(component, settingsInput); + var DEFAULT_INTERACTIONS = [ + EventClicking, + EventHovering, + ]; + var interactionClasses = DEFAULT_INTERACTIONS.concat(_this.props.pluginHooks.componentInteractions); + var interactions = interactionClasses.map(function (TheInteractionClass) { return new TheInteractionClass(settings); }); + _this.interactionsStore[component.uid] = interactions; + interactionSettingsStore[component.uid] = settings; + }; + _this.unregisterInteractiveComponent = function (component) { + for (var _i = 0, _a = _this.interactionsStore[component.uid]; _i < _a.length; _i++) { + var listener = _a[_i]; + listener.destroy(); + } + delete _this.interactionsStore[component.uid]; + delete interactionSettingsStore[component.uid]; + }; + // Resizing + // ----------------------------------------------------------------------------------------------------------------- + _this.resizeRunner = new DelayedRunner(function () { + _this.props.emitter.trigger('_resize', true); // should window resizes be considered "forced" ? + _this.props.emitter.trigger('windowResize', { view: _this.props.viewApi }); + }); + _this.handleWindowResize = function (ev) { + var options = _this.props.options; + if (options.handleWindowResize && + ev.target === window // avoid jqui events + ) { + _this.resizeRunner.request(options.windowResizeDelay); + } + }; + return _this; + } + /* + renders INSIDE of an outer div + */ + CalendarContent.prototype.render = function () { + var props = this.props; + var toolbarConfig = props.toolbarConfig, options = props.options; + var toolbarProps = this.buildToolbarProps(props.viewSpec, props.dateProfile, props.dateProfileGenerator, props.currentDate, getNow(props.options.now, props.dateEnv), // TODO: use NowTimer???? + props.viewTitle); + var viewVGrow = false; + var viewHeight = ''; + var viewAspectRatio; + if (props.isHeightAuto || props.forPrint) { + viewHeight = ''; + } + else if (options.height != null) { + viewVGrow = true; + } + else if (options.contentHeight != null) { + viewHeight = options.contentHeight; + } + else { + viewAspectRatio = Math.max(options.aspectRatio, 0.5); // prevent from getting too tall + } + var viewContext = this.buildViewContext(props.viewSpec, props.viewApi, props.options, props.dateProfileGenerator, props.dateEnv, props.theme, props.pluginHooks, props.dispatch, props.getCurrentData, props.emitter, props.calendarApi, this.registerInteractiveComponent, this.unregisterInteractiveComponent); + return (createElement(ViewContextType.Provider, { value: viewContext }, + toolbarConfig.headerToolbar && (createElement(Toolbar, __assign({ ref: this.headerRef, extraClassName: "fc-header-toolbar", model: toolbarConfig.headerToolbar }, toolbarProps))), + createElement(ViewContainer, { liquid: viewVGrow, height: viewHeight, aspectRatio: viewAspectRatio, onClick: this.handleNavLinkClick }, + this.renderView(props), + this.buildAppendContent()), + toolbarConfig.footerToolbar && (createElement(Toolbar, __assign({ ref: this.footerRef, extraClassName: "fc-footer-toolbar", model: toolbarConfig.footerToolbar }, toolbarProps))))); + }; + CalendarContent.prototype.componentDidMount = function () { + var props = this.props; + this.calendarInteractions = props.pluginHooks.calendarInteractions + .map(function (CalendarInteractionClass) { return new CalendarInteractionClass(props); }); + window.addEventListener('resize', this.handleWindowResize); + var propSetHandlers = props.pluginHooks.propSetHandlers; + for (var propName in propSetHandlers) { + propSetHandlers[propName](props[propName], props); + } + }; + CalendarContent.prototype.componentDidUpdate = function (prevProps) { + var props = this.props; + var propSetHandlers = props.pluginHooks.propSetHandlers; + for (var propName in propSetHandlers) { + if (props[propName] !== prevProps[propName]) { + propSetHandlers[propName](props[propName], props); + } + } + }; + CalendarContent.prototype.componentWillUnmount = function () { + window.removeEventListener('resize', this.handleWindowResize); + this.resizeRunner.clear(); + for (var _i = 0, _a = this.calendarInteractions; _i < _a.length; _i++) { + var interaction = _a[_i]; + interaction.destroy(); + } + this.props.emitter.trigger('_unmount'); + }; + CalendarContent.prototype._handleNavLinkClick = function (ev, anchorEl) { + var _a = this.props, dateEnv = _a.dateEnv, options = _a.options, calendarApi = _a.calendarApi; + var navLinkOptions = anchorEl.getAttribute('data-navlink'); + navLinkOptions = navLinkOptions ? JSON.parse(navLinkOptions) : {}; + var dateMarker = dateEnv.createMarker(navLinkOptions.date); + var viewType = navLinkOptions.type; + var customAction = viewType === 'day' ? options.navLinkDayClick : + viewType === 'week' ? options.navLinkWeekClick : null; + if (typeof customAction === 'function') { + customAction.call(calendarApi, dateEnv.toDate(dateMarker), ev); + } + else { + if (typeof customAction === 'string') { + viewType = customAction; + } + calendarApi.zoomTo(dateMarker, viewType); + } + }; + CalendarContent.prototype.buildAppendContent = function () { + var props = this.props; + var children = props.pluginHooks.viewContainerAppends.map(function (buildAppendContent) { return buildAppendContent(props); }); + return createElement.apply(void 0, __spreadArray([Fragment, {}], children)); + }; + CalendarContent.prototype.renderView = function (props) { + var pluginHooks = props.pluginHooks; + var viewSpec = props.viewSpec; + var viewProps = { + dateProfile: props.dateProfile, + businessHours: props.businessHours, + eventStore: props.renderableEventStore, + eventUiBases: props.eventUiBases, + dateSelection: props.dateSelection, + eventSelection: props.eventSelection, + eventDrag: props.eventDrag, + eventResize: props.eventResize, + isHeightAuto: props.isHeightAuto, + forPrint: props.forPrint, + }; + var transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers); + for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) { + var transformer = transformers_1[_i]; + __assign(viewProps, transformer.transform(viewProps, props)); + } + var ViewComponent = viewSpec.component; + return (createElement(ViewComponent, __assign({}, viewProps))); + }; + return CalendarContent; + }(PureComponent)); + function buildToolbarProps(viewSpec, dateProfile, dateProfileGenerator, currentDate, now, title) { + // don't force any date-profiles to valid date profiles (the `false`) so that we can tell if it's invalid + var todayInfo = dateProfileGenerator.build(now, undefined, false); // TODO: need `undefined` or else INFINITE LOOP for some reason + var prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, false); + var nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, false); + return { + title: title, + activeButton: viewSpec.type, + isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now), + isPrevEnabled: prevInfo.isValid, + isNextEnabled: nextInfo.isValid, + }; + } + // Plugin + // ----------------------------------------------------------------------------------------------------------------- + function buildViewPropTransformers(theClasses) { + return theClasses.map(function (TheClass) { return new TheClass(); }); + } + + var CalendarRoot = /** @class */ (function (_super) { + __extends(CalendarRoot, _super); + function CalendarRoot() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.state = { + forPrint: false, + }; + _this.handleBeforePrint = function () { + _this.setState({ forPrint: true }); + }; + _this.handleAfterPrint = function () { + _this.setState({ forPrint: false }); + }; + return _this; + } + CalendarRoot.prototype.render = function () { + var props = this.props; + var options = props.options; + var forPrint = this.state.forPrint; + var isHeightAuto = forPrint || options.height === 'auto' || options.contentHeight === 'auto'; + var height = (!isHeightAuto && options.height != null) ? options.height : ''; + var classNames = [ + 'fc', + forPrint ? 'fc-media-print' : 'fc-media-screen', + "fc-direction-" + options.direction, + props.theme.getClass('root'), + ]; + if (!getCanVGrowWithinCell()) { + classNames.push('fc-liquid-hack'); + } + return props.children(classNames, height, isHeightAuto, forPrint); + }; + CalendarRoot.prototype.componentDidMount = function () { + var emitter = this.props.emitter; + emitter.on('_beforeprint', this.handleBeforePrint); + emitter.on('_afterprint', this.handleAfterPrint); + }; + CalendarRoot.prototype.componentWillUnmount = function () { + var emitter = this.props.emitter; + emitter.off('_beforeprint', this.handleBeforePrint); + emitter.off('_afterprint', this.handleAfterPrint); + }; + return CalendarRoot; + }(BaseComponent)); + + // Computes a default column header formatting string if `colFormat` is not explicitly defined + function computeFallbackHeaderFormat(datesRepDistinctDays, dayCnt) { + // if more than one week row, or if there are a lot of columns with not much space, + // put just the day numbers will be in each cell + if (!datesRepDistinctDays || dayCnt > 10) { + return createFormatter({ weekday: 'short' }); // "Sat" + } + if (dayCnt > 1) { + return createFormatter({ weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true }); // "Sat 11/12" + } + return createFormatter({ weekday: 'long' }); // "Saturday" + } + + var CLASS_NAME = 'fc-col-header-cell'; // do the cushion too? no + function renderInner$1(hookProps) { + return hookProps.text; + } + + var TableDateCell = /** @class */ (function (_super) { + __extends(TableDateCell, _super); + function TableDateCell() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableDateCell.prototype.render = function () { + var _a = this.context, dateEnv = _a.dateEnv, options = _a.options, theme = _a.theme, viewApi = _a.viewApi; + var props = this.props; + var date = props.date, dateProfile = props.dateProfile; + var dayMeta = getDateMeta(date, props.todayRange, null, dateProfile); + var classNames = [CLASS_NAME].concat(getDayClassNames(dayMeta, theme)); + var text = dateEnv.format(date, props.dayHeaderFormat); + // if colCnt is 1, we are already in a day-view and don't need a navlink + var navLinkAttrs = (options.navLinks && !dayMeta.isDisabled && props.colCnt > 1) + ? { 'data-navlink': buildNavLinkData(date), tabIndex: 0 } + : {}; + var hookProps = __assign(__assign(__assign({ date: dateEnv.toDate(date), view: viewApi }, props.extraHookProps), { text: text }), dayMeta); + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.dayHeaderClassNames, content: options.dayHeaderContent, defaultContent: renderInner$1, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("th", __assign({ ref: rootElRef, className: classNames.concat(customClassNames).join(' '), "data-date": !dayMeta.isDisabled ? formatDayString(date) : undefined, colSpan: props.colSpan }, props.extraDataAttrs), + createElement("div", { className: "fc-scrollgrid-sync-inner" }, !dayMeta.isDisabled && (createElement("a", __assign({ ref: innerElRef, className: [ + 'fc-col-header-cell-cushion', + props.isSticky ? 'fc-sticky' : '', + ].join(' ') }, navLinkAttrs), innerContent))))); })); + }; + return TableDateCell; + }(BaseComponent)); + + var TableDowCell = /** @class */ (function (_super) { + __extends(TableDowCell, _super); + function TableDowCell() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableDowCell.prototype.render = function () { + var props = this.props; + var _a = this.context, dateEnv = _a.dateEnv, theme = _a.theme, viewApi = _a.viewApi, options = _a.options; + var date = addDays(new Date(259200000), props.dow); // start with Sun, 04 Jan 1970 00:00:00 GMT + var dateMeta = { + dow: props.dow, + isDisabled: false, + isFuture: false, + isPast: false, + isToday: false, + isOther: false, + }; + var classNames = [CLASS_NAME].concat(getDayClassNames(dateMeta, theme), props.extraClassNames || []); + var text = dateEnv.format(date, props.dayHeaderFormat); + var hookProps = __assign(__assign(__assign(__assign({ // TODO: make this public? + date: date }, dateMeta), { view: viewApi }), props.extraHookProps), { text: text }); + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.dayHeaderClassNames, content: options.dayHeaderContent, defaultContent: renderInner$1, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("th", __assign({ ref: rootElRef, className: classNames.concat(customClassNames).join(' '), colSpan: props.colSpan }, props.extraDataAttrs), + createElement("div", { className: "fc-scrollgrid-sync-inner" }, + createElement("a", { className: [ + 'fc-col-header-cell-cushion', + props.isSticky ? 'fc-sticky' : '', + ].join(' '), ref: innerElRef }, innerContent)))); })); + }; + return TableDowCell; + }(BaseComponent)); + + var NowTimer = /** @class */ (function (_super) { + __extends(NowTimer, _super); + function NowTimer(props, context) { + var _this = _super.call(this, props, context) || this; + _this.initialNowDate = getNow(context.options.now, context.dateEnv); + _this.initialNowQueriedMs = new Date().valueOf(); + _this.state = _this.computeTiming().currentState; + return _this; + } + NowTimer.prototype.render = function () { + var _a = this, props = _a.props, state = _a.state; + return props.children(state.nowDate, state.todayRange); + }; + NowTimer.prototype.componentDidMount = function () { + this.setTimeout(); + }; + NowTimer.prototype.componentDidUpdate = function (prevProps) { + if (prevProps.unit !== this.props.unit) { + this.clearTimeout(); + this.setTimeout(); + } + }; + NowTimer.prototype.componentWillUnmount = function () { + this.clearTimeout(); + }; + NowTimer.prototype.computeTiming = function () { + var _a = this, props = _a.props, context = _a.context; + var unroundedNow = addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs); + var currentUnitStart = context.dateEnv.startOf(unroundedNow, props.unit); + var nextUnitStart = context.dateEnv.add(currentUnitStart, createDuration(1, props.unit)); + var waitMs = nextUnitStart.valueOf() - unroundedNow.valueOf(); + // there is a max setTimeout ms value (https://stackoverflow.com/a/3468650/96342) + // ensure no longer than a day + waitMs = Math.min(1000 * 60 * 60 * 24, waitMs); + return { + currentState: { nowDate: currentUnitStart, todayRange: buildDayRange(currentUnitStart) }, + nextState: { nowDate: nextUnitStart, todayRange: buildDayRange(nextUnitStart) }, + waitMs: waitMs, + }; + }; + NowTimer.prototype.setTimeout = function () { + var _this = this; + var _a = this.computeTiming(), nextState = _a.nextState, waitMs = _a.waitMs; + this.timeoutId = setTimeout(function () { + _this.setState(nextState, function () { + _this.setTimeout(); + }); + }, waitMs); + }; + NowTimer.prototype.clearTimeout = function () { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + } + }; + NowTimer.contextType = ViewContextType; + return NowTimer; + }(Component)); + function buildDayRange(date) { + var start = startOfDay(date); + var end = addDays(start, 1); + return { start: start, end: end }; + } + + var DayHeader = /** @class */ (function (_super) { + __extends(DayHeader, _super); + function DayHeader() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.createDayHeaderFormatter = memoize(createDayHeaderFormatter); + return _this; + } + DayHeader.prototype.render = function () { + var context = this.context; + var _a = this.props, dates = _a.dates, dateProfile = _a.dateProfile, datesRepDistinctDays = _a.datesRepDistinctDays, renderIntro = _a.renderIntro; + var dayHeaderFormat = this.createDayHeaderFormatter(context.options.dayHeaderFormat, datesRepDistinctDays, dates.length); + return (createElement(NowTimer, { unit: "day" }, function (nowDate, todayRange) { return (createElement("tr", null, + renderIntro && renderIntro('day'), + dates.map(function (date) { return (datesRepDistinctDays ? (createElement(TableDateCell, { key: date.toISOString(), date: date, dateProfile: dateProfile, todayRange: todayRange, colCnt: dates.length, dayHeaderFormat: dayHeaderFormat })) : (createElement(TableDowCell, { key: date.getUTCDay(), dow: date.getUTCDay(), dayHeaderFormat: dayHeaderFormat }))); }))); })); + }; + return DayHeader; + }(BaseComponent)); + function createDayHeaderFormatter(explicitFormat, datesRepDistinctDays, dateCnt) { + return explicitFormat || computeFallbackHeaderFormat(datesRepDistinctDays, dateCnt); + } + + var DaySeriesModel = /** @class */ (function () { + function DaySeriesModel(range, dateProfileGenerator) { + var date = range.start; + var end = range.end; + var indices = []; + var dates = []; + var dayIndex = -1; + while (date < end) { // loop each day from start to end + if (dateProfileGenerator.isHiddenDay(date)) { + indices.push(dayIndex + 0.5); // mark that it's between indices + } + else { + dayIndex += 1; + indices.push(dayIndex); + dates.push(date); + } + date = addDays(date, 1); + } + this.dates = dates; + this.indices = indices; + this.cnt = dates.length; + } + DaySeriesModel.prototype.sliceRange = function (range) { + var firstIndex = this.getDateDayIndex(range.start); // inclusive first index + var lastIndex = this.getDateDayIndex(addDays(range.end, -1)); // inclusive last index + var clippedFirstIndex = Math.max(0, firstIndex); + var clippedLastIndex = Math.min(this.cnt - 1, lastIndex); + // deal with in-between indices + clippedFirstIndex = Math.ceil(clippedFirstIndex); // in-between starts round to next cell + clippedLastIndex = Math.floor(clippedLastIndex); // in-between ends round to prev cell + if (clippedFirstIndex <= clippedLastIndex) { + return { + firstIndex: clippedFirstIndex, + lastIndex: clippedLastIndex, + isStart: firstIndex === clippedFirstIndex, + isEnd: lastIndex === clippedLastIndex, + }; + } + return null; + }; + // Given a date, returns its chronolocial cell-index from the first cell of the grid. + // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. + // If before the first offset, returns a negative number. + // If after the last offset, returns an offset past the last cell offset. + // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. + DaySeriesModel.prototype.getDateDayIndex = function (date) { + var indices = this.indices; + var dayOffset = Math.floor(diffDays(this.dates[0], date)); + if (dayOffset < 0) { + return indices[0] - 1; + } + if (dayOffset >= indices.length) { + return indices[indices.length - 1] + 1; + } + return indices[dayOffset]; + }; + return DaySeriesModel; + }()); + + var DayTableModel = /** @class */ (function () { + function DayTableModel(daySeries, breakOnWeeks) { + var dates = daySeries.dates; + var daysPerRow; + var firstDay; + var rowCnt; + if (breakOnWeeks) { + // count columns until the day-of-week repeats + firstDay = dates[0].getUTCDay(); + for (daysPerRow = 1; daysPerRow < dates.length; daysPerRow += 1) { + if (dates[daysPerRow].getUTCDay() === firstDay) { + break; + } + } + rowCnt = Math.ceil(dates.length / daysPerRow); + } + else { + rowCnt = 1; + daysPerRow = dates.length; + } + this.rowCnt = rowCnt; + this.colCnt = daysPerRow; + this.daySeries = daySeries; + this.cells = this.buildCells(); + this.headerDates = this.buildHeaderDates(); + } + DayTableModel.prototype.buildCells = function () { + var rows = []; + for (var row = 0; row < this.rowCnt; row += 1) { + var cells = []; + for (var col = 0; col < this.colCnt; col += 1) { + cells.push(this.buildCell(row, col)); + } + rows.push(cells); + } + return rows; + }; + DayTableModel.prototype.buildCell = function (row, col) { + var date = this.daySeries.dates[row * this.colCnt + col]; + return { + key: date.toISOString(), + date: date, + }; + }; + DayTableModel.prototype.buildHeaderDates = function () { + var dates = []; + for (var col = 0; col < this.colCnt; col += 1) { + dates.push(this.cells[0][col].date); + } + return dates; + }; + DayTableModel.prototype.sliceRange = function (range) { + var colCnt = this.colCnt; + var seriesSeg = this.daySeries.sliceRange(range); + var segs = []; + if (seriesSeg) { + var firstIndex = seriesSeg.firstIndex, lastIndex = seriesSeg.lastIndex; + var index = firstIndex; + while (index <= lastIndex) { + var row = Math.floor(index / colCnt); + var nextIndex = Math.min((row + 1) * colCnt, lastIndex + 1); + segs.push({ + row: row, + firstCol: index % colCnt, + lastCol: (nextIndex - 1) % colCnt, + isStart: seriesSeg.isStart && index === firstIndex, + isEnd: seriesSeg.isEnd && (nextIndex - 1) === lastIndex, + }); + index = nextIndex; + } + } + return segs; + }; + return DayTableModel; + }()); + + var Slicer = /** @class */ (function () { + function Slicer() { + this.sliceBusinessHours = memoize(this._sliceBusinessHours); + this.sliceDateSelection = memoize(this._sliceDateSpan); + this.sliceEventStore = memoize(this._sliceEventStore); + this.sliceEventDrag = memoize(this._sliceInteraction); + this.sliceEventResize = memoize(this._sliceInteraction); + this.forceDayIfListItem = false; // hack + } + Slicer.prototype.sliceProps = function (props, dateProfile, nextDayThreshold, context) { + var extraArgs = []; + for (var _i = 4; _i < arguments.length; _i++) { + extraArgs[_i - 4] = arguments[_i]; + } + var eventUiBases = props.eventUiBases; + var eventSegs = this.sliceEventStore.apply(this, __spreadArray([props.eventStore, eventUiBases, dateProfile, nextDayThreshold], extraArgs)); + return { + dateSelectionSegs: this.sliceDateSelection.apply(this, __spreadArray([props.dateSelection, eventUiBases, context], extraArgs)), + businessHourSegs: this.sliceBusinessHours.apply(this, __spreadArray([props.businessHours, dateProfile, nextDayThreshold, context], extraArgs)), + fgEventSegs: eventSegs.fg, + bgEventSegs: eventSegs.bg, + eventDrag: this.sliceEventDrag.apply(this, __spreadArray([props.eventDrag, eventUiBases, dateProfile, nextDayThreshold], extraArgs)), + eventResize: this.sliceEventResize.apply(this, __spreadArray([props.eventResize, eventUiBases, dateProfile, nextDayThreshold], extraArgs)), + eventSelection: props.eventSelection, + }; // TODO: give interactionSegs? + }; + Slicer.prototype.sliceNowDate = function (// does not memoize + date, context) { + var extraArgs = []; + for (var _i = 2; _i < arguments.length; _i++) { + extraArgs[_i - 2] = arguments[_i]; + } + return this._sliceDateSpan.apply(this, __spreadArray([{ range: { start: date, end: addMs(date, 1) }, allDay: false }, + {}, + context], extraArgs)); + }; + Slicer.prototype._sliceBusinessHours = function (businessHours, dateProfile, nextDayThreshold, context) { + var extraArgs = []; + for (var _i = 4; _i < arguments.length; _i++) { + extraArgs[_i - 4] = arguments[_i]; + } + if (!businessHours) { + return []; + } + return this._sliceEventStore.apply(this, __spreadArray([expandRecurring(businessHours, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), context), + {}, + dateProfile, + nextDayThreshold], extraArgs)).bg; + }; + Slicer.prototype._sliceEventStore = function (eventStore, eventUiBases, dateProfile, nextDayThreshold) { + var extraArgs = []; + for (var _i = 4; _i < arguments.length; _i++) { + extraArgs[_i - 4] = arguments[_i]; + } + if (eventStore) { + var rangeRes = sliceEventStore(eventStore, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold); + return { + bg: this.sliceEventRanges(rangeRes.bg, extraArgs), + fg: this.sliceEventRanges(rangeRes.fg, extraArgs), + }; + } + return { bg: [], fg: [] }; + }; + Slicer.prototype._sliceInteraction = function (interaction, eventUiBases, dateProfile, nextDayThreshold) { + var extraArgs = []; + for (var _i = 4; _i < arguments.length; _i++) { + extraArgs[_i - 4] = arguments[_i]; + } + if (!interaction) { + return null; + } + var rangeRes = sliceEventStore(interaction.mutatedEvents, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold); + return { + segs: this.sliceEventRanges(rangeRes.fg, extraArgs), + affectedInstances: interaction.affectedEvents.instances, + isEvent: interaction.isEvent, + }; + }; + Slicer.prototype._sliceDateSpan = function (dateSpan, eventUiBases, context) { + var extraArgs = []; + for (var _i = 3; _i < arguments.length; _i++) { + extraArgs[_i - 3] = arguments[_i]; + } + if (!dateSpan) { + return []; + } + var eventRange = fabricateEventRange(dateSpan, eventUiBases, context); + var segs = this.sliceRange.apply(this, __spreadArray([dateSpan.range], extraArgs)); + for (var _a = 0, segs_1 = segs; _a < segs_1.length; _a++) { + var seg = segs_1[_a]; + seg.eventRange = eventRange; + } + return segs; + }; + /* + "complete" seg means it has component and eventRange + */ + Slicer.prototype.sliceEventRanges = function (eventRanges, extraArgs) { + var segs = []; + for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) { + var eventRange = eventRanges_1[_i]; + segs.push.apply(segs, this.sliceEventRange(eventRange, extraArgs)); + } + return segs; + }; + /* + "complete" seg means it has component and eventRange + */ + Slicer.prototype.sliceEventRange = function (eventRange, extraArgs) { + var dateRange = eventRange.range; + // hack to make multi-day events that are being force-displayed as list-items to take up only one day + if (this.forceDayIfListItem && eventRange.ui.display === 'list-item') { + dateRange = { + start: dateRange.start, + end: addDays(dateRange.start, 1), + }; + } + var segs = this.sliceRange.apply(this, __spreadArray([dateRange], extraArgs)); + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + seg.eventRange = eventRange; + seg.isStart = eventRange.isStart && seg.isStart; + seg.isEnd = eventRange.isEnd && seg.isEnd; + } + return segs; + }; + return Slicer; + }()); + /* + for incorporating slotMinTime/slotMaxTime if appropriate + TODO: should be part of DateProfile! + TimelineDateProfile already does this btw + */ + function computeActiveRange(dateProfile, isComponentAllDay) { + var range = dateProfile.activeRange; + if (isComponentAllDay) { + return range; + } + return { + start: addMs(range.start, dateProfile.slotMinTime.milliseconds), + end: addMs(range.end, dateProfile.slotMaxTime.milliseconds - 864e5), // 864e5 = ms in a day + }; + } + + // high-level segmenting-aware tester functions + // ------------------------------------------------------------------------------------------------------------------------ + function isInteractionValid(interaction, dateProfile, context) { + var instances = interaction.mutatedEvents.instances; + for (var instanceId in instances) { + if (!rangeContainsRange(dateProfile.validRange, instances[instanceId].range)) { + return false; + } + } + return isNewPropsValid({ eventDrag: interaction }, context); // HACK: the eventDrag props is used for ALL interactions + } + function isDateSelectionValid(dateSelection, dateProfile, context) { + if (!rangeContainsRange(dateProfile.validRange, dateSelection.range)) { + return false; + } + return isNewPropsValid({ dateSelection: dateSelection }, context); + } + function isNewPropsValid(newProps, context) { + var calendarState = context.getCurrentData(); + var props = __assign({ businessHours: calendarState.businessHours, dateSelection: '', eventStore: calendarState.eventStore, eventUiBases: calendarState.eventUiBases, eventSelection: '', eventDrag: null, eventResize: null }, newProps); + return (context.pluginHooks.isPropsValid || isPropsValid)(props, context); + } + function isPropsValid(state, context, dateSpanMeta, filterConfig) { + if (dateSpanMeta === void 0) { dateSpanMeta = {}; } + if (state.eventDrag && !isInteractionPropsValid(state, context, dateSpanMeta, filterConfig)) { + return false; + } + if (state.dateSelection && !isDateSelectionPropsValid(state, context, dateSpanMeta, filterConfig)) { + return false; + } + return true; + } + // Moving Event Validation + // ------------------------------------------------------------------------------------------------------------------------ + function isInteractionPropsValid(state, context, dateSpanMeta, filterConfig) { + var currentState = context.getCurrentData(); + var interaction = state.eventDrag; // HACK: the eventDrag props is used for ALL interactions + var subjectEventStore = interaction.mutatedEvents; + var subjectDefs = subjectEventStore.defs; + var subjectInstances = subjectEventStore.instances; + var subjectConfigs = compileEventUis(subjectDefs, interaction.isEvent ? + state.eventUiBases : + { '': currentState.selectionConfig }); + if (filterConfig) { + subjectConfigs = mapHash(subjectConfigs, filterConfig); + } + // exclude the subject events. TODO: exclude defs too? + var otherEventStore = excludeInstances(state.eventStore, interaction.affectedEvents.instances); + var otherDefs = otherEventStore.defs; + var otherInstances = otherEventStore.instances; + var otherConfigs = compileEventUis(otherDefs, state.eventUiBases); + for (var subjectInstanceId in subjectInstances) { + var subjectInstance = subjectInstances[subjectInstanceId]; + var subjectRange = subjectInstance.range; + var subjectConfig = subjectConfigs[subjectInstance.defId]; + var subjectDef = subjectDefs[subjectInstance.defId]; + // constraint + if (!allConstraintsPass(subjectConfig.constraints, subjectRange, otherEventStore, state.businessHours, context)) { + return false; + } + // overlap + var eventOverlap = context.options.eventOverlap; + var eventOverlapFunc = typeof eventOverlap === 'function' ? eventOverlap : null; + for (var otherInstanceId in otherInstances) { + var otherInstance = otherInstances[otherInstanceId]; + // intersect! evaluate + if (rangesIntersect(subjectRange, otherInstance.range)) { + var otherOverlap = otherConfigs[otherInstance.defId].overlap; + // consider the other event's overlap. only do this if the subject event is a "real" event + if (otherOverlap === false && interaction.isEvent) { + return false; + } + if (subjectConfig.overlap === false) { + return false; + } + if (eventOverlapFunc && !eventOverlapFunc(new EventApi(context, otherDefs[otherInstance.defId], otherInstance), // still event + new EventApi(context, subjectDef, subjectInstance))) { + return false; + } + } + } + // allow (a function) + var calendarEventStore = currentState.eventStore; // need global-to-calendar, not local to component (splittable)state + for (var _i = 0, _a = subjectConfig.allows; _i < _a.length; _i++) { + var subjectAllow = _a[_i]; + var subjectDateSpan = __assign(__assign({}, dateSpanMeta), { range: subjectInstance.range, allDay: subjectDef.allDay }); + var origDef = calendarEventStore.defs[subjectDef.defId]; + var origInstance = calendarEventStore.instances[subjectInstanceId]; + var eventApi = void 0; + if (origDef) { // was previously in the calendar + eventApi = new EventApi(context, origDef, origInstance); + } + else { // was an external event + eventApi = new EventApi(context, subjectDef); // no instance, because had no dates + } + if (!subjectAllow(buildDateSpanApiWithContext(subjectDateSpan, context), eventApi)) { + return false; + } + } + } + return true; + } + // Date Selection Validation + // ------------------------------------------------------------------------------------------------------------------------ + function isDateSelectionPropsValid(state, context, dateSpanMeta, filterConfig) { + var relevantEventStore = state.eventStore; + var relevantDefs = relevantEventStore.defs; + var relevantInstances = relevantEventStore.instances; + var selection = state.dateSelection; + var selectionRange = selection.range; + var selectionConfig = context.getCurrentData().selectionConfig; + if (filterConfig) { + selectionConfig = filterConfig(selectionConfig); + } + // constraint + if (!allConstraintsPass(selectionConfig.constraints, selectionRange, relevantEventStore, state.businessHours, context)) { + return false; + } + // overlap + var selectOverlap = context.options.selectOverlap; + var selectOverlapFunc = typeof selectOverlap === 'function' ? selectOverlap : null; + for (var relevantInstanceId in relevantInstances) { + var relevantInstance = relevantInstances[relevantInstanceId]; + // intersect! evaluate + if (rangesIntersect(selectionRange, relevantInstance.range)) { + if (selectionConfig.overlap === false) { + return false; + } + if (selectOverlapFunc && !selectOverlapFunc(new EventApi(context, relevantDefs[relevantInstance.defId], relevantInstance), null)) { + return false; + } + } + } + // allow (a function) + for (var _i = 0, _a = selectionConfig.allows; _i < _a.length; _i++) { + var selectionAllow = _a[_i]; + var fullDateSpan = __assign(__assign({}, dateSpanMeta), selection); + if (!selectionAllow(buildDateSpanApiWithContext(fullDateSpan, context), null)) { + return false; + } + } + return true; + } + // Constraint Utils + // ------------------------------------------------------------------------------------------------------------------------ + function allConstraintsPass(constraints, subjectRange, otherEventStore, businessHoursUnexpanded, context) { + for (var _i = 0, constraints_1 = constraints; _i < constraints_1.length; _i++) { + var constraint = constraints_1[_i]; + if (!anyRangesContainRange(constraintToRanges(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, context), subjectRange)) { + return false; + } + } + return true; + } + function constraintToRanges(constraint, subjectRange, // for expanding a recurring constraint, or expanding business hours + otherEventStore, // for if constraint is an even group ID + businessHoursUnexpanded, // for if constraint is 'businessHours' + context) { + if (constraint === 'businessHours') { + return eventStoreToRanges(expandRecurring(businessHoursUnexpanded, subjectRange, context)); + } + if (typeof constraint === 'string') { // an group ID + return eventStoreToRanges(filterEventStoreDefs(otherEventStore, function (eventDef) { return eventDef.groupId === constraint; })); + } + if (typeof constraint === 'object' && constraint) { // non-null object + return eventStoreToRanges(expandRecurring(constraint, subjectRange, context)); + } + return []; // if it's false + } + // TODO: move to event-store file? + function eventStoreToRanges(eventStore) { + var instances = eventStore.instances; + var ranges = []; + for (var instanceId in instances) { + ranges.push(instances[instanceId].range); + } + return ranges; + } + // TODO: move to geom file? + function anyRangesContainRange(outerRanges, innerRange) { + for (var _i = 0, outerRanges_1 = outerRanges; _i < outerRanges_1.length; _i++) { + var outerRange = outerRanges_1[_i]; + if (rangeContainsRange(outerRange, innerRange)) { + return true; + } + } + return false; + } + + var VISIBLE_HIDDEN_RE = /^(visible|hidden)$/; + var Scroller = /** @class */ (function (_super) { + __extends(Scroller, _super); + function Scroller() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.handleEl = function (el) { + _this.el = el; + setRef(_this.props.elRef, el); + }; + return _this; + } + Scroller.prototype.render = function () { + var props = this.props; + var liquid = props.liquid, liquidIsAbsolute = props.liquidIsAbsolute; + var isAbsolute = liquid && liquidIsAbsolute; + var className = ['fc-scroller']; + if (liquid) { + if (liquidIsAbsolute) { + className.push('fc-scroller-liquid-absolute'); + } + else { + className.push('fc-scroller-liquid'); + } + } + return (createElement("div", { ref: this.handleEl, className: className.join(' '), style: { + overflowX: props.overflowX, + overflowY: props.overflowY, + left: (isAbsolute && -(props.overcomeLeft || 0)) || '', + right: (isAbsolute && -(props.overcomeRight || 0)) || '', + bottom: (isAbsolute && -(props.overcomeBottom || 0)) || '', + marginLeft: (!isAbsolute && -(props.overcomeLeft || 0)) || '', + marginRight: (!isAbsolute && -(props.overcomeRight || 0)) || '', + marginBottom: (!isAbsolute && -(props.overcomeBottom || 0)) || '', + maxHeight: props.maxHeight || '', + } }, props.children)); + }; + Scroller.prototype.needsXScrolling = function () { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowX)) { + return false; + } + // testing scrollWidth>clientWidth is unreliable cross-browser when pixel heights aren't integers. + // much more reliable to see if children are taller than the scroller, even tho doesn't account for + // inner-child margins and absolute positioning + var el = this.el; + var realClientWidth = this.el.getBoundingClientRect().width - this.getYScrollbarWidth(); + var children = el.children; + for (var i = 0; i < children.length; i += 1) { + var childEl = children[i]; + if (childEl.getBoundingClientRect().width > realClientWidth) { + return true; + } + } + return false; + }; + Scroller.prototype.needsYScrolling = function () { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowY)) { + return false; + } + // testing scrollHeight>clientHeight is unreliable cross-browser when pixel heights aren't integers. + // much more reliable to see if children are taller than the scroller, even tho doesn't account for + // inner-child margins and absolute positioning + var el = this.el; + var realClientHeight = this.el.getBoundingClientRect().height - this.getXScrollbarWidth(); + var children = el.children; + for (var i = 0; i < children.length; i += 1) { + var childEl = children[i]; + if (childEl.getBoundingClientRect().height > realClientHeight) { + return true; + } + } + return false; + }; + Scroller.prototype.getXScrollbarWidth = function () { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowX)) { + return 0; + } + return this.el.offsetHeight - this.el.clientHeight; // only works because we guarantee no borders. TODO: add to CSS with important? + }; + Scroller.prototype.getYScrollbarWidth = function () { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowY)) { + return 0; + } + return this.el.offsetWidth - this.el.clientWidth; // only works because we guarantee no borders. TODO: add to CSS with important? + }; + return Scroller; + }(BaseComponent)); + + /* + TODO: somehow infer OtherArgs from masterCallback? + TODO: infer RefType from masterCallback if provided + */ + var RefMap = /** @class */ (function () { + function RefMap(masterCallback) { + var _this = this; + this.masterCallback = masterCallback; + this.currentMap = {}; + this.depths = {}; + this.callbackMap = {}; + this.handleValue = function (val, key) { + var _a = _this, depths = _a.depths, currentMap = _a.currentMap; + var removed = false; + var added = false; + if (val !== null) { + // for bug... ACTUALLY: can probably do away with this now that callers don't share numeric indices anymore + removed = (key in currentMap); + currentMap[key] = val; + depths[key] = (depths[key] || 0) + 1; + added = true; + } + else { + depths[key] -= 1; + if (!depths[key]) { + delete currentMap[key]; + delete _this.callbackMap[key]; + removed = true; + } + } + if (_this.masterCallback) { + if (removed) { + _this.masterCallback(null, String(key)); + } + if (added) { + _this.masterCallback(val, String(key)); + } + } + }; + } + RefMap.prototype.createRef = function (key) { + var _this = this; + var refCallback = this.callbackMap[key]; + if (!refCallback) { + refCallback = this.callbackMap[key] = function (val) { + _this.handleValue(val, String(key)); + }; + } + return refCallback; + }; + // TODO: check callers that don't care about order. should use getAll instead + // NOTE: this method has become less valuable now that we are encouraged to map order by some other index + // TODO: provide ONE array-export function, buildArray, which fails on non-numeric indexes. caller can manipulate and "collect" + RefMap.prototype.collect = function (startIndex, endIndex, step) { + return collectFromHash(this.currentMap, startIndex, endIndex, step); + }; + RefMap.prototype.getAll = function () { + return hashValuesToArray(this.currentMap); + }; + return RefMap; + }()); + + function computeShrinkWidth(chunkEls) { + var shrinkCells = findElements(chunkEls, '.fc-scrollgrid-shrink'); + var largestWidth = 0; + for (var _i = 0, shrinkCells_1 = shrinkCells; _i < shrinkCells_1.length; _i++) { + var shrinkCell = shrinkCells_1[_i]; + largestWidth = Math.max(largestWidth, computeSmallestCellWidth(shrinkCell)); + } + return Math.ceil(largestWidth); // 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 + HOWEVER, Chrome was getting messing up distributing the width to elements makes Chrome behave. + */ + for (var _i = 0, cols_1 = cols; _i < cols_1.length; _i++) { + var colProps = cols_1[_i]; + var span = colProps.span || 1; + for (var i = 0; i < span; i += 1) { + colNodes.push(createElement("col", { style: { + width: colProps.width === 'shrink' ? sanitizeShrinkWidth(shrinkWidth) : (colProps.width || ''), + minWidth: colProps.minWidth || '', + } })); + } + } + return createElement.apply(void 0, __spreadArray(['colgroup', {}], colNodes)); + } + function sanitizeShrinkWidth(shrinkWidth) { + /* why 4? if we do 0, it will kill any border, which are needed for computeSmallestCellWidth + 4 accounts for 2 2-pixel borders. TODO: better solution? */ + return shrinkWidth == null ? 4 : shrinkWidth; + } + function hasShrinkWidth(cols) { + for (var _i = 0, cols_2 = cols; _i < cols_2.length; _i++) { + var col = cols_2[_i]; + if (col.width === 'shrink') { + return true; + } + } + return false; + } + function getScrollGridClassNames(liquid, context) { + var classNames = [ + 'fc-scrollgrid', + context.theme.getClass('table'), + ]; + if (liquid) { + classNames.push('fc-scrollgrid-liquid'); + } + return classNames; + } + function getSectionClassNames(sectionConfig, wholeTableVGrow) { + var classNames = [ + 'fc-scrollgrid-section', + "fc-scrollgrid-section-" + sectionConfig.type, + sectionConfig.className, // used? + ]; + if (wholeTableVGrow && sectionConfig.liquid && sectionConfig.maxHeight == null) { + classNames.push('fc-scrollgrid-section-liquid'); + } + if (sectionConfig.isSticky) { + classNames.push('fc-scrollgrid-section-sticky'); + } + return classNames; + } + function renderScrollShim(arg) { + return (createElement("div", { className: "fc-scrollgrid-sticky-shim", style: { + width: arg.clientWidth, + minWidth: arg.tableMinWidth, + } })); + } + function getStickyHeaderDates(options) { + var stickyHeaderDates = options.stickyHeaderDates; + if (stickyHeaderDates == null || stickyHeaderDates === 'auto') { + stickyHeaderDates = options.height === 'auto' || options.viewHeight === 'auto'; + } + return stickyHeaderDates; + } + function getStickyFooterScrollbar(options) { + var stickyFooterScrollbar = options.stickyFooterScrollbar; + if (stickyFooterScrollbar == null || stickyFooterScrollbar === 'auto') { + stickyFooterScrollbar = options.height === 'auto' || options.viewHeight === 'auto'; + } + return stickyFooterScrollbar; + } + + var SimpleScrollGrid = /** @class */ (function (_super) { + __extends(SimpleScrollGrid, _super); + function SimpleScrollGrid() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.processCols = memoize(function (a) { return a; }, isColPropsEqual); // so we get same `cols` props every time + // yucky to memoize VNodes, but much more efficient for consumers + _this.renderMicroColGroup = memoize(renderMicroColGroup); + _this.scrollerRefs = new RefMap(); + _this.scrollerElRefs = new RefMap(_this._handleScrollerEl.bind(_this)); + _this.state = { + shrinkWidth: null, + forceYScrollbars: false, + scrollerClientWidths: {}, + scrollerClientHeights: {}, + }; + // TODO: can do a really simple print-view. dont need to join rows + _this.handleSizing = function () { + _this.setState(__assign({ shrinkWidth: _this.computeShrinkWidth() }, _this.computeScrollerDims())); + }; + return _this; + } + SimpleScrollGrid.prototype.render = function () { + var _a = this, props = _a.props, state = _a.state, context = _a.context; + var sectionConfigs = props.sections || []; + var cols = this.processCols(props.cols); + var microColGroupNode = this.renderMicroColGroup(cols, state.shrinkWidth); + var classNames = getScrollGridClassNames(props.liquid, context); + if (props.collapsibleWidth) { + classNames.push('fc-scrollgrid-collapsible'); + } + // TODO: make DRY + var configCnt = sectionConfigs.length; + var configI = 0; + var currentConfig; + var headSectionNodes = []; + var bodySectionNodes = []; + var footSectionNodes = []; + while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'header') { + headSectionNodes.push(this.renderSection(currentConfig, microColGroupNode)); + configI += 1; + } + while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'body') { + bodySectionNodes.push(this.renderSection(currentConfig, microColGroupNode)); + configI += 1; + } + while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'footer') { + footSectionNodes.push(this.renderSection(currentConfig, microColGroupNode)); + configI += 1; + } + // firefox bug: when setting height on table and there is a thead or tfoot, + // the necessary height:100% on the liquid-height body section forces the *whole* table to be taller. (bug #5524) + // use getCanVGrowWithinCell as a way to detect table-stupid firefox. + // if so, use a simpler dom structure, jam everything into a lone tbody. + var isBuggy = !getCanVGrowWithinCell(); + return createElement('table', { + className: classNames.join(' '), + style: { height: props.height }, + }, Boolean(!isBuggy && headSectionNodes.length) && createElement.apply(void 0, __spreadArray(['thead', {}], headSectionNodes)), Boolean(!isBuggy && bodySectionNodes.length) && createElement.apply(void 0, __spreadArray(['tbody', {}], bodySectionNodes)), Boolean(!isBuggy && footSectionNodes.length) && createElement.apply(void 0, __spreadArray(['tfoot', {}], footSectionNodes)), isBuggy && createElement.apply(void 0, __spreadArray(__spreadArray(__spreadArray(['tbody', {}], headSectionNodes), bodySectionNodes), footSectionNodes))); + }; + SimpleScrollGrid.prototype.renderSection = function (sectionConfig, microColGroupNode) { + if ('outerContent' in sectionConfig) { + return (createElement(Fragment, { key: sectionConfig.key }, sectionConfig.outerContent)); + } + return (createElement("tr", { key: sectionConfig.key, className: getSectionClassNames(sectionConfig, this.props.liquid).join(' ') }, this.renderChunkTd(sectionConfig, microColGroupNode, sectionConfig.chunk))); + }; + SimpleScrollGrid.prototype.renderChunkTd = function (sectionConfig, microColGroupNode, chunkConfig) { + if ('outerContent' in chunkConfig) { + return chunkConfig.outerContent; + } + var props = this.props; + var _a = this.state, forceYScrollbars = _a.forceYScrollbars, scrollerClientWidths = _a.scrollerClientWidths, scrollerClientHeights = _a.scrollerClientHeights; + var needsYScrolling = getAllowYScrolling(props, sectionConfig); // TODO: do lazily. do in section config? + var isLiquid = getSectionHasLiquidHeight(props, sectionConfig); + // for `!props.liquid` - is WHOLE scrollgrid natural height? + // TODO: do same thing in advanced scrollgrid? prolly not b/c always has horizontal scrollbars + var overflowY = !props.liquid ? 'visible' : + forceYScrollbars ? 'scroll' : + !needsYScrolling ? 'hidden' : + 'auto'; + var sectionKey = sectionConfig.key; + var content = renderChunkContent(sectionConfig, chunkConfig, { + tableColGroupNode: microColGroupNode, + tableMinWidth: '', + clientWidth: (!props.collapsibleWidth && scrollerClientWidths[sectionKey] !== undefined) ? scrollerClientWidths[sectionKey] : null, + clientHeight: scrollerClientHeights[sectionKey] !== undefined ? scrollerClientHeights[sectionKey] : null, + expandRows: sectionConfig.expandRows, + syncRowHeights: false, + rowSyncHeights: [], + reportRowHeightChange: function () { }, + }); + return (createElement("td", { ref: chunkConfig.elRef }, + createElement("div", { className: "fc-scroller-harness" + (isLiquid ? ' fc-scroller-harness-liquid' : '') }, + createElement(Scroller, { ref: this.scrollerRefs.createRef(sectionKey), elRef: this.scrollerElRefs.createRef(sectionKey), overflowY: overflowY, overflowX: !props.liquid ? 'visible' : 'hidden' /* natural height? */, maxHeight: sectionConfig.maxHeight, liquid: isLiquid, liquidIsAbsolute // because its within a harness + : true }, content)))); + }; + SimpleScrollGrid.prototype._handleScrollerEl = function (scrollerEl, key) { + var section = getSectionByKey(this.props.sections, key); + if (section) { + setRef(section.chunk.scrollerElRef, scrollerEl); + } + }; + SimpleScrollGrid.prototype.componentDidMount = function () { + this.handleSizing(); + this.context.addResizeHandler(this.handleSizing); + }; + SimpleScrollGrid.prototype.componentDidUpdate = function () { + // TODO: need better solution when state contains non-sizing things + this.handleSizing(); + }; + SimpleScrollGrid.prototype.componentWillUnmount = function () { + this.context.removeResizeHandler(this.handleSizing); + }; + SimpleScrollGrid.prototype.computeShrinkWidth = function () { + return hasShrinkWidth(this.props.cols) + ? computeShrinkWidth(this.scrollerElRefs.getAll()) + : 0; + }; + SimpleScrollGrid.prototype.computeScrollerDims = function () { + var scrollbarWidth = getScrollbarWidths(); + var _a = this, scrollerRefs = _a.scrollerRefs, scrollerElRefs = _a.scrollerElRefs; + var forceYScrollbars = false; + var scrollerClientWidths = {}; + var scrollerClientHeights = {}; + for (var sectionKey in scrollerRefs.currentMap) { + var scroller = scrollerRefs.currentMap[sectionKey]; + if (scroller && scroller.needsYScrolling()) { + forceYScrollbars = true; + break; + } + } + for (var _i = 0, _b = this.props.sections; _i < _b.length; _i++) { + var section = _b[_i]; + var sectionKey = section.key; + var scrollerEl = scrollerElRefs.currentMap[sectionKey]; + if (scrollerEl) { + var harnessEl = scrollerEl.parentNode; // TODO: weird way to get this. need harness b/c doesn't include table borders + scrollerClientWidths[sectionKey] = Math.floor(harnessEl.getBoundingClientRect().width - (forceYScrollbars + ? scrollbarWidth.y // use global because scroller might not have scrollbars yet but will need them in future + : 0)); + scrollerClientHeights[sectionKey] = Math.floor(harnessEl.getBoundingClientRect().height); + } + } + return { forceYScrollbars: forceYScrollbars, scrollerClientWidths: scrollerClientWidths, scrollerClientHeights: scrollerClientHeights }; + }; + return SimpleScrollGrid; + }(BaseComponent)); + SimpleScrollGrid.addStateEquality({ + scrollerClientWidths: isPropsEqual, + scrollerClientHeights: isPropsEqual, + }); + function getSectionByKey(sections, key) { + for (var _i = 0, sections_1 = sections; _i < sections_1.length; _i++) { + var section = sections_1[_i]; + if (section.key === key) { + return section; + } + } + return null; + } + + var EventRoot = /** @class */ (function (_super) { + __extends(EventRoot, _super); + function EventRoot() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.elRef = createRef(); + return _this; + } + EventRoot.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var options = context.options; + var seg = props.seg; + var eventRange = seg.eventRange; + var ui = eventRange.ui; + var hookProps = { + event: new EventApi(context, eventRange.def, eventRange.instance), + view: context.viewApi, + timeText: props.timeText, + textColor: ui.textColor, + backgroundColor: ui.backgroundColor, + borderColor: ui.borderColor, + isDraggable: !props.disableDragging && computeSegDraggable(seg, context), + isStartResizable: !props.disableResizing && computeSegStartResizable(seg, context), + isEndResizable: !props.disableResizing && computeSegEndResizable(seg), + isMirror: Boolean(props.isDragging || props.isResizing || props.isDateSelecting), + isStart: Boolean(seg.isStart), + isEnd: Boolean(seg.isEnd), + isPast: Boolean(props.isPast), + isFuture: Boolean(props.isFuture), + isToday: Boolean(props.isToday), + isSelected: Boolean(props.isSelected), + isDragging: Boolean(props.isDragging), + isResizing: Boolean(props.isResizing), + }; + var standardClassNames = getEventClassNames(hookProps).concat(ui.classNames); + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.eventClassNames, content: options.eventContent, defaultContent: props.defaultContent, didMount: options.eventDidMount, willUnmount: options.eventWillUnmount, elRef: this.elRef }, function (rootElRef, customClassNames, innerElRef, innerContent) { return props.children(rootElRef, standardClassNames.concat(customClassNames), innerElRef, innerContent, hookProps); })); + }; + EventRoot.prototype.componentDidMount = function () { + setElSeg(this.elRef.current, this.props.seg); + }; + /* + need to re-assign seg to the element if seg changes, even if the element is the same + */ + EventRoot.prototype.componentDidUpdate = function (prevProps) { + var seg = this.props.seg; + if (seg !== prevProps.seg) { + setElSeg(this.elRef.current, seg); + } + }; + return EventRoot; + }(BaseComponent)); + + // should not be a purecomponent + var StandardEvent = /** @class */ (function (_super) { + __extends(StandardEvent, _super); + function StandardEvent() { + return _super !== null && _super.apply(this, arguments) || this; + } + StandardEvent.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var seg = props.seg; + var timeFormat = context.options.eventTimeFormat || props.defaultTimeFormat; + var timeText = buildSegTimeText(seg, timeFormat, context, props.defaultDisplayEventTime, props.defaultDisplayEventEnd); + return (createElement(EventRoot, { seg: seg, timeText: timeText, disableDragging: props.disableDragging, disableResizing: props.disableResizing, defaultContent: props.defaultContent || renderInnerContent$4, isDragging: props.isDragging, isResizing: props.isResizing, isDateSelecting: props.isDateSelecting, isSelected: props.isSelected, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday }, function (rootElRef, classNames, innerElRef, innerContent, hookProps) { return (createElement("a", __assign({ className: props.extraClassNames.concat(classNames).join(' '), style: { + borderColor: hookProps.borderColor, + backgroundColor: hookProps.backgroundColor, + }, ref: rootElRef }, getSegAnchorAttrs$1(seg)), + createElement("div", { className: "fc-event-main", ref: innerElRef, style: { color: hookProps.textColor } }, innerContent), + hookProps.isStartResizable && + createElement("div", { className: "fc-event-resizer fc-event-resizer-start" }), + hookProps.isEndResizable && + createElement("div", { className: "fc-event-resizer fc-event-resizer-end" }))); })); + }; + return StandardEvent; + }(BaseComponent)); + function renderInnerContent$4(innerProps) { + return (createElement("div", { className: "fc-event-main-frame" }, + innerProps.timeText && (createElement("div", { className: "fc-event-time" }, innerProps.timeText)), + createElement("div", { className: "fc-event-title-container" }, + createElement("div", { className: "fc-event-title fc-sticky" }, innerProps.event.title || createElement(Fragment, null, "\u00A0"))))); + } + function getSegAnchorAttrs$1(seg) { + var url = seg.eventRange.def.url; + return url ? { href: url } : {}; + } + + var NowIndicatorRoot = function (props) { return (createElement(ViewContextType.Consumer, null, function (context) { + var options = context.options; + var hookProps = { + isAxis: props.isAxis, + date: context.dateEnv.toDate(props.date), + view: context.viewApi, + }; + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.nowIndicatorClassNames, content: options.nowIndicatorContent, didMount: options.nowIndicatorDidMount, willUnmount: options.nowIndicatorWillUnmount }, props.children)); + })); }; + + var DAY_NUM_FORMAT = createFormatter({ day: 'numeric' }); + var DayCellContent = /** @class */ (function (_super) { + __extends(DayCellContent, _super); + function DayCellContent() { + return _super !== null && _super.apply(this, arguments) || this; + } + DayCellContent.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var options = context.options; + var hookProps = refineDayCellHookProps({ + date: props.date, + dateProfile: props.dateProfile, + todayRange: props.todayRange, + showDayNumber: props.showDayNumber, + extraProps: props.extraHookProps, + viewApi: context.viewApi, + dateEnv: context.dateEnv, + }); + return (createElement(ContentHook, { hookProps: hookProps, content: options.dayCellContent, defaultContent: props.defaultContent }, props.children)); + }; + return DayCellContent; + }(BaseComponent)); + function refineDayCellHookProps(raw) { + var date = raw.date, dateEnv = raw.dateEnv; + var dayMeta = getDateMeta(date, raw.todayRange, null, raw.dateProfile); + return __assign(__assign(__assign({ date: dateEnv.toDate(date), view: raw.viewApi }, dayMeta), { dayNumberText: raw.showDayNumber ? dateEnv.format(date, DAY_NUM_FORMAT) : '' }), raw.extraProps); + } + + var DayCellRoot = /** @class */ (function (_super) { + __extends(DayCellRoot, _super); + function DayCellRoot() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.refineHookProps = memoizeObjArg(refineDayCellHookProps); + _this.normalizeClassNames = buildClassNameNormalizer(); + return _this; + } + DayCellRoot.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var options = context.options; + var hookProps = this.refineHookProps({ + date: props.date, + dateProfile: props.dateProfile, + todayRange: props.todayRange, + showDayNumber: props.showDayNumber, + extraProps: props.extraHookProps, + viewApi: context.viewApi, + dateEnv: context.dateEnv, + }); + var classNames = getDayClassNames(hookProps, context.theme).concat(hookProps.isDisabled + ? [] // don't use custom classNames if disabled + : this.normalizeClassNames(options.dayCellClassNames, hookProps)); + var dataAttrs = hookProps.isDisabled ? {} : { + 'data-date': formatDayString(props.date), + }; + return (createElement(MountHook, { hookProps: hookProps, didMount: options.dayCellDidMount, willUnmount: options.dayCellWillUnmount, elRef: props.elRef }, function (rootElRef) { return props.children(rootElRef, classNames, dataAttrs, hookProps.isDisabled); })); + }; + return DayCellRoot; + }(BaseComponent)); + + function renderFill(fillType) { + return (createElement("div", { className: "fc-" + fillType })); + } + var BgEvent = function (props) { return (createElement(EventRoot, { defaultContent: renderInnerContent$3, seg: props.seg /* uselesss i think */, timeText: "", disableDragging: true, disableResizing: true, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: false, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday }, function (rootElRef, classNames, innerElRef, innerContent, hookProps) { return (createElement("div", { ref: rootElRef, className: ['fc-bg-event'].concat(classNames).join(' '), style: { + backgroundColor: hookProps.backgroundColor, + } }, innerContent)); })); }; + function renderInnerContent$3(props) { + var title = props.event.title; + return title && (createElement("div", { className: "fc-event-title" }, props.event.title)); + } + + var WeekNumberRoot = function (props) { return (createElement(ViewContextType.Consumer, null, function (context) { + var dateEnv = context.dateEnv, options = context.options; + var date = props.date; + var format = options.weekNumberFormat || props.defaultFormat; + var num = dateEnv.computeWeekNumber(date); // TODO: somehow use for formatting as well? + var text = dateEnv.format(date, format); + var hookProps = { num: num, text: text, date: date }; + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.weekNumberClassNames, content: options.weekNumberContent, defaultContent: renderInner, didMount: options.weekNumberDidMount, willUnmount: options.weekNumberWillUnmount }, props.children)); + })); }; + function renderInner(innerProps) { + return innerProps.text; + } + + var PADDING_FROM_VIEWPORT = 10; + var Popover = /** @class */ (function (_super) { + __extends(Popover, _super); + function Popover() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.handleRootEl = function (el) { + _this.rootEl = el; + if (_this.props.elRef) { + setRef(_this.props.elRef, el); + } + }; + // Triggered when the user clicks *anywhere* in the document, for the autoHide feature + _this.handleDocumentMousedown = function (ev) { + // only hide the popover if the click happened outside the popover + var target = getEventTargetViaRoot(ev); + if (!_this.rootEl.contains(target)) { + _this.handleCloseClick(); + } + }; + _this.handleCloseClick = function () { + var onClose = _this.props.onClose; + if (onClose) { + onClose(); + } + }; + return _this; + } + Popover.prototype.render = function () { + var theme = this.context.theme; + var props = this.props; + var classNames = [ + 'fc-popover', + theme.getClass('popover'), + ].concat(props.extraClassNames || []); + return createPortal(createElement("div", __assign({ className: classNames.join(' ') }, props.extraAttrs, { ref: this.handleRootEl }), + createElement("div", { className: 'fc-popover-header ' + theme.getClass('popoverHeader') }, + createElement("span", { className: "fc-popover-title" }, props.title), + createElement("span", { className: 'fc-popover-close ' + theme.getIconClass('close'), onClick: this.handleCloseClick })), + createElement("div", { className: 'fc-popover-body ' + theme.getClass('popoverContent') }, props.children)), props.parentEl); + }; + Popover.prototype.componentDidMount = function () { + document.addEventListener('mousedown', this.handleDocumentMousedown); + this.updateSize(); + }; + Popover.prototype.componentWillUnmount = function () { + document.removeEventListener('mousedown', this.handleDocumentMousedown); + }; + Popover.prototype.updateSize = function () { + var isRtl = this.context.isRtl; + var _a = this.props, alignmentEl = _a.alignmentEl, alignGridTop = _a.alignGridTop; + var rootEl = this.rootEl; + var alignmentRect = computeClippedClientRect(alignmentEl); + if (alignmentRect) { + var popoverDims = rootEl.getBoundingClientRect(); + // position relative to viewport + var popoverTop = alignGridTop + ? elementClosest(alignmentEl, '.fc-scrollgrid').getBoundingClientRect().top + : alignmentRect.top; + var popoverLeft = isRtl ? alignmentRect.right - popoverDims.width : alignmentRect.left; + // constrain + popoverTop = Math.max(popoverTop, PADDING_FROM_VIEWPORT); + popoverLeft = Math.min(popoverLeft, document.documentElement.clientWidth - PADDING_FROM_VIEWPORT - popoverDims.width); + popoverLeft = Math.max(popoverLeft, PADDING_FROM_VIEWPORT); + var origin_1 = rootEl.offsetParent.getBoundingClientRect(); + applyStyle(rootEl, { + top: popoverTop - origin_1.top, + left: popoverLeft - origin_1.left, + }); + } + }; + return Popover; + }(BaseComponent)); + + var MorePopover = /** @class */ (function (_super) { + __extends(MorePopover, _super); + function MorePopover() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.handleRootEl = function (rootEl) { + _this.rootEl = rootEl; + if (rootEl) { + _this.context.registerInteractiveComponent(_this, { + el: rootEl, + useEventCenter: false, + }); + } + else { + _this.context.unregisterInteractiveComponent(_this); + } + }; + return _this; + } + MorePopover.prototype.render = function () { + var _a = this.context, options = _a.options, dateEnv = _a.dateEnv; + var props = this.props; + var startDate = props.startDate, todayRange = props.todayRange, dateProfile = props.dateProfile; + var title = dateEnv.format(startDate, options.dayPopoverFormat); + return (createElement(DayCellRoot, { date: startDate, dateProfile: dateProfile, todayRange: todayRange, elRef: this.handleRootEl }, function (rootElRef, dayClassNames, dataAttrs) { return (createElement(Popover, { elRef: rootElRef, title: title, extraClassNames: ['fc-more-popover'].concat(dayClassNames), extraAttrs: dataAttrs /* TODO: make these time-based when not whole-day? */, parentEl: props.parentEl, alignmentEl: props.alignmentEl, alignGridTop: props.alignGridTop, onClose: props.onClose }, + createElement(DayCellContent, { date: startDate, dateProfile: dateProfile, todayRange: todayRange }, function (innerElRef, innerContent) { return (innerContent && + createElement("div", { className: "fc-more-popover-misc", ref: innerElRef }, innerContent)); }), + props.children)); })); + }; + MorePopover.prototype.queryHit = function (positionLeft, positionTop, elWidth, elHeight) { + var _a = this, rootEl = _a.rootEl, props = _a.props; + if (positionLeft >= 0 && positionLeft < elWidth && + positionTop >= 0 && positionTop < elHeight) { + return { + dateProfile: props.dateProfile, + dateSpan: __assign({ allDay: true, range: { + start: props.startDate, + end: props.endDate, + } }, props.extraDateSpan), + dayEl: rootEl, + rect: { + left: 0, + top: 0, + right: elWidth, + bottom: elHeight, + }, + layer: 1, // important when comparing with hits from other components + }; + } + return null; + }; + return MorePopover; + }(DateComponent)); + + var MoreLinkRoot = /** @class */ (function (_super) { + __extends(MoreLinkRoot, _super); + function MoreLinkRoot() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.linkElRef = createRef(); + _this.state = { + isPopoverOpen: false, + }; + _this.handleClick = function (ev) { + var _a = _this, props = _a.props, context = _a.context; + var moreLinkClick = context.options.moreLinkClick; + var date = computeRange(props).start; + function buildPublicSeg(seg) { + var _a = seg.eventRange, def = _a.def, instance = _a.instance, range = _a.range; + return { + event: new EventApi(context, def, instance), + start: context.dateEnv.toDate(range.start), + end: context.dateEnv.toDate(range.end), + isStart: seg.isStart, + isEnd: seg.isEnd, + }; + } + if (typeof moreLinkClick === 'function') { + moreLinkClick = moreLinkClick({ + date: date, + allDay: Boolean(props.allDayDate), + allSegs: props.allSegs.map(buildPublicSeg), + hiddenSegs: props.hiddenSegs.map(buildPublicSeg), + jsEvent: ev, + view: context.viewApi, + }); + } + if (!moreLinkClick || moreLinkClick === 'popover') { + _this.setState({ isPopoverOpen: true }); + } + else if (typeof moreLinkClick === 'string') { // a view name + context.calendarApi.zoomTo(date, moreLinkClick); + } + }; + _this.handlePopoverClose = function () { + _this.setState({ isPopoverOpen: false }); + }; + return _this; + } + MoreLinkRoot.prototype.render = function () { + var _this = this; + var props = this.props; + return (createElement(ViewContextType.Consumer, null, function (context) { + var viewApi = context.viewApi, options = context.options, calendarApi = context.calendarApi; + var moreLinkText = options.moreLinkText; + var moreCnt = props.moreCnt; + var range = computeRange(props); + var hookProps = { + num: moreCnt, + shortText: "+" + moreCnt, + text: typeof moreLinkText === 'function' + ? moreLinkText.call(calendarApi, moreCnt) + : "+" + moreCnt + " " + moreLinkText, + view: viewApi, + }; + return (createElement(Fragment, null, + Boolean(props.moreCnt) && (createElement(RenderHook, { elRef: _this.linkElRef, hookProps: hookProps, classNames: options.moreLinkClassNames, content: options.moreLinkContent, defaultContent: props.defaultContent || renderMoreLinkInner$1, didMount: options.moreLinkDidMount, willUnmount: options.moreLinkWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return props.children(rootElRef, ['fc-more-link'].concat(customClassNames), innerElRef, innerContent, _this.handleClick); })), + _this.state.isPopoverOpen && (createElement(MorePopover, { startDate: range.start, endDate: range.end, dateProfile: props.dateProfile, todayRange: props.todayRange, extraDateSpan: props.extraDateSpan, parentEl: _this.parentEl, alignmentEl: props.alignmentElRef.current, alignGridTop: props.alignGridTop, onClose: _this.handlePopoverClose }, props.popoverContent())))); + })); + }; + MoreLinkRoot.prototype.componentDidMount = function () { + this.updateParentEl(); + }; + MoreLinkRoot.prototype.componentDidUpdate = function () { + this.updateParentEl(); + }; + MoreLinkRoot.prototype.updateParentEl = function () { + if (this.linkElRef.current) { + this.parentEl = elementClosest(this.linkElRef.current, '.fc-view-harness'); + } + }; + return MoreLinkRoot; + }(BaseComponent)); + function renderMoreLinkInner$1(props) { + return props.text; + } + function computeRange(props) { + if (props.allDayDate) { + return { + start: props.allDayDate, + end: addDays(props.allDayDate, 1), + }; + } + var hiddenSegs = props.hiddenSegs; + return { + start: computeEarliestSegStart(hiddenSegs), + end: computeLatestSegEnd(hiddenSegs), + }; + } + function computeEarliestSegStart(segs) { + return segs.reduce(pickEarliestStart).eventRange.range.start; + } + function pickEarliestStart(seg0, seg1) { + return seg0.eventRange.range.start < seg1.eventRange.range.start ? seg0 : seg1; + } + function computeLatestSegEnd(segs) { + return segs.reduce(pickLatestEnd).eventRange.range.end; + } + function pickLatestEnd(seg0, seg1) { + return seg0.eventRange.range.end > seg1.eventRange.range.end ? seg0 : seg1; + } + + // exports + // -------------------------------------------------------------------------------------------------- + var version = '5.9.0'; // important to type it, so .d.ts has generic string + + var Calendar = /** @class */ (function (_super) { + __extends(Calendar, _super); + function Calendar(el, optionOverrides) { + if (optionOverrides === void 0) { optionOverrides = {}; } + var _this = _super.call(this) || this; + _this.isRendering = false; + _this.isRendered = false; + _this.currentClassNames = []; + _this.customContentRenderId = 0; // will affect custom generated classNames? + _this.handleAction = function (action) { + // actions we know we want to render immediately + switch (action.type) { + case 'SET_EVENT_DRAG': + case 'SET_EVENT_RESIZE': + _this.renderRunner.tryDrain(); + } + }; + _this.handleData = function (data) { + _this.currentData = data; + _this.renderRunner.request(data.calendarOptions.rerenderDelay); + }; + _this.handleRenderRequest = function () { + if (_this.isRendering) { + _this.isRendered = true; + var currentData_1 = _this.currentData; + render(createElement(CalendarRoot, { options: currentData_1.calendarOptions, theme: currentData_1.theme, emitter: currentData_1.emitter }, function (classNames, height, isHeightAuto, forPrint) { + _this.setClassNames(classNames); + _this.setHeight(height); + return (createElement(CustomContentRenderContext.Provider, { value: _this.customContentRenderId }, + createElement(CalendarContent, __assign({ isHeightAuto: isHeightAuto, forPrint: forPrint }, currentData_1)))); + }), _this.el); + } + else if (_this.isRendered) { + _this.isRendered = false; + unmountComponentAtNode(_this.el); + _this.setClassNames([]); + _this.setHeight(''); + } + flushToDom(); + }; + _this.el = el; + _this.renderRunner = new DelayedRunner(_this.handleRenderRequest); + new CalendarDataManager({ + optionOverrides: optionOverrides, + calendarApi: _this, + onAction: _this.handleAction, + onData: _this.handleData, + }); + return _this; + } + Object.defineProperty(Calendar.prototype, "view", { + get: function () { return this.currentData.viewApi; } // for public API + , + enumerable: false, + configurable: true + }); + Calendar.prototype.render = function () { + var wasRendering = this.isRendering; + if (!wasRendering) { + this.isRendering = true; + } + else { + this.customContentRenderId += 1; + } + this.renderRunner.request(); + if (wasRendering) { + this.updateSize(); + } + }; + Calendar.prototype.destroy = function () { + if (this.isRendering) { + this.isRendering = false; + this.renderRunner.request(); + } + }; + Calendar.prototype.updateSize = function () { + _super.prototype.updateSize.call(this); + flushToDom(); + }; + Calendar.prototype.batchRendering = function (func) { + this.renderRunner.pause('batchRendering'); + func(); + this.renderRunner.resume('batchRendering'); + }; + Calendar.prototype.pauseRendering = function () { + this.renderRunner.pause('pauseRendering'); + }; + Calendar.prototype.resumeRendering = function () { + this.renderRunner.resume('pauseRendering', true); + }; + Calendar.prototype.resetOptions = function (optionOverrides, append) { + this.currentDataManager.resetOptions(optionOverrides, append); + }; + Calendar.prototype.setClassNames = function (classNames) { + if (!isArraysEqual(classNames, this.currentClassNames)) { + var classList = this.el.classList; + for (var _i = 0, _a = this.currentClassNames; _i < _a.length; _i++) { + var className = _a[_i]; + classList.remove(className); + } + for (var _b = 0, classNames_1 = classNames; _b < classNames_1.length; _b++) { + var className = classNames_1[_b]; + classList.add(className); + } + this.currentClassNames = classNames; + } + }; + Calendar.prototype.setHeight = function (height) { + applyStyleProp(this.el, 'height', height); + }; + return Calendar; + }(CalendarApi)); + + config.touchMouseIgnoreWait = 500; + var ignoreMouseDepth = 0; + var listenerCnt = 0; + var isWindowTouchMoveCancelled = false; + /* + Uses a "pointer" abstraction, which monitors UI events for both mouse and touch. + Tracks when the pointer "drags" on a certain element, meaning down+move+up. + + Also, tracks if there was touch-scrolling. + Also, can prevent touch-scrolling from happening. + Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement. + + emits: + - pointerdown + - pointermove + - pointerup + */ + var PointerDragging = /** @class */ (function () { + function PointerDragging(containerEl) { + var _this = this; + this.subjectEl = null; + // options that can be directly assigned by caller + this.selector = ''; // will cause subjectEl in all emitted events to be this element + this.handleSelector = ''; + this.shouldIgnoreMove = false; + this.shouldWatchScroll = true; // for simulating pointermove on scroll + // internal states + this.isDragging = false; + this.isTouchDragging = false; + this.wasTouchScroll = false; + // Mouse + // ---------------------------------------------------------------------------------------------------- + this.handleMouseDown = function (ev) { + if (!_this.shouldIgnoreMouse() && + isPrimaryMouseButton(ev) && + _this.tryStart(ev)) { + var pev = _this.createEventFromMouse(ev, true); + _this.emitter.trigger('pointerdown', pev); + _this.initScrollWatch(pev); + if (!_this.shouldIgnoreMove) { + document.addEventListener('mousemove', _this.handleMouseMove); + } + document.addEventListener('mouseup', _this.handleMouseUp); + } + }; + this.handleMouseMove = function (ev) { + var pev = _this.createEventFromMouse(ev); + _this.recordCoords(pev); + _this.emitter.trigger('pointermove', pev); + }; + this.handleMouseUp = function (ev) { + document.removeEventListener('mousemove', _this.handleMouseMove); + document.removeEventListener('mouseup', _this.handleMouseUp); + _this.emitter.trigger('pointerup', _this.createEventFromMouse(ev)); + _this.cleanup(); // call last so that pointerup has access to props + }; + // Touch + // ---------------------------------------------------------------------------------------------------- + this.handleTouchStart = function (ev) { + if (_this.tryStart(ev)) { + _this.isTouchDragging = true; + var pev = _this.createEventFromTouch(ev, true); + _this.emitter.trigger('pointerdown', pev); + _this.initScrollWatch(pev); + // unlike mouse, need to attach to target, not document + // https://stackoverflow.com/a/45760014 + var targetEl = ev.target; + if (!_this.shouldIgnoreMove) { + targetEl.addEventListener('touchmove', _this.handleTouchMove); + } + targetEl.addEventListener('touchend', _this.handleTouchEnd); + targetEl.addEventListener('touchcancel', _this.handleTouchEnd); // treat it as a touch end + // attach a handler to get called when ANY scroll action happens on the page. + // this was impossible to do with normal on/off because 'scroll' doesn't bubble. + // http://stackoverflow.com/a/32954565/96342 + window.addEventListener('scroll', _this.handleTouchScroll, true); + } + }; + this.handleTouchMove = function (ev) { + var pev = _this.createEventFromTouch(ev); + _this.recordCoords(pev); + _this.emitter.trigger('pointermove', pev); + }; + this.handleTouchEnd = function (ev) { + if (_this.isDragging) { // done to guard against touchend followed by touchcancel + var targetEl = ev.target; + targetEl.removeEventListener('touchmove', _this.handleTouchMove); + targetEl.removeEventListener('touchend', _this.handleTouchEnd); + targetEl.removeEventListener('touchcancel', _this.handleTouchEnd); + window.removeEventListener('scroll', _this.handleTouchScroll, true); // useCaptured=true + _this.emitter.trigger('pointerup', _this.createEventFromTouch(ev)); + _this.cleanup(); // call last so that pointerup has access to props + _this.isTouchDragging = false; + startIgnoringMouse(); + } + }; + this.handleTouchScroll = function () { + _this.wasTouchScroll = true; + }; + this.handleScroll = function (ev) { + if (!_this.shouldIgnoreMove) { + var pageX = (window.pageXOffset - _this.prevScrollX) + _this.prevPageX; + var pageY = (window.pageYOffset - _this.prevScrollY) + _this.prevPageY; + _this.emitter.trigger('pointermove', { + origEvent: ev, + isTouch: _this.isTouchDragging, + subjectEl: _this.subjectEl, + pageX: pageX, + pageY: pageY, + deltaX: pageX - _this.origPageX, + deltaY: pageY - _this.origPageY, + }); + } + }; + this.containerEl = containerEl; + this.emitter = new Emitter(); + containerEl.addEventListener('mousedown', this.handleMouseDown); + containerEl.addEventListener('touchstart', this.handleTouchStart, { passive: true }); + listenerCreated(); + } + PointerDragging.prototype.destroy = function () { + this.containerEl.removeEventListener('mousedown', this.handleMouseDown); + this.containerEl.removeEventListener('touchstart', this.handleTouchStart, { passive: true }); + listenerDestroyed(); + }; + PointerDragging.prototype.tryStart = function (ev) { + var subjectEl = this.querySubjectEl(ev); + var downEl = ev.target; + if (subjectEl && + (!this.handleSelector || elementClosest(downEl, this.handleSelector))) { + this.subjectEl = subjectEl; + this.isDragging = true; // do this first so cancelTouchScroll will work + this.wasTouchScroll = false; + return true; + } + return false; + }; + PointerDragging.prototype.cleanup = function () { + isWindowTouchMoveCancelled = false; + this.isDragging = false; + this.subjectEl = null; + // keep wasTouchScroll around for later access + this.destroyScrollWatch(); + }; + PointerDragging.prototype.querySubjectEl = function (ev) { + if (this.selector) { + return elementClosest(ev.target, this.selector); + } + return this.containerEl; + }; + PointerDragging.prototype.shouldIgnoreMouse = function () { + return ignoreMouseDepth || this.isTouchDragging; + }; + // can be called by user of this class, to cancel touch-based scrolling for the current drag + PointerDragging.prototype.cancelTouchScroll = function () { + if (this.isDragging) { + isWindowTouchMoveCancelled = true; + } + }; + // Scrolling that simulates pointermoves + // ---------------------------------------------------------------------------------------------------- + PointerDragging.prototype.initScrollWatch = function (ev) { + if (this.shouldWatchScroll) { + this.recordCoords(ev); + window.addEventListener('scroll', this.handleScroll, true); // useCapture=true + } + }; + PointerDragging.prototype.recordCoords = function (ev) { + if (this.shouldWatchScroll) { + this.prevPageX = ev.pageX; + this.prevPageY = ev.pageY; + this.prevScrollX = window.pageXOffset; + this.prevScrollY = window.pageYOffset; + } + }; + PointerDragging.prototype.destroyScrollWatch = function () { + if (this.shouldWatchScroll) { + window.removeEventListener('scroll', this.handleScroll, true); // useCaptured=true + } + }; + // Event Normalization + // ---------------------------------------------------------------------------------------------------- + PointerDragging.prototype.createEventFromMouse = function (ev, isFirst) { + var deltaX = 0; + var deltaY = 0; + // TODO: repeat code + if (isFirst) { + this.origPageX = ev.pageX; + this.origPageY = ev.pageY; + } + else { + deltaX = ev.pageX - this.origPageX; + deltaY = ev.pageY - this.origPageY; + } + return { + origEvent: ev, + isTouch: false, + subjectEl: this.subjectEl, + pageX: ev.pageX, + pageY: ev.pageY, + deltaX: deltaX, + deltaY: deltaY, + }; + }; + PointerDragging.prototype.createEventFromTouch = function (ev, isFirst) { + var touches = ev.touches; + var pageX; + var pageY; + var deltaX = 0; + var deltaY = 0; + // if touch coords available, prefer, + // because FF would give bad ev.pageX ev.pageY + if (touches && touches.length) { + pageX = touches[0].pageX; + pageY = touches[0].pageY; + } + else { + pageX = ev.pageX; + pageY = ev.pageY; + } + // TODO: repeat code + if (isFirst) { + this.origPageX = pageX; + this.origPageY = pageY; + } + else { + deltaX = pageX - this.origPageX; + deltaY = pageY - this.origPageY; + } + return { + origEvent: ev, + isTouch: true, + subjectEl: this.subjectEl, + pageX: pageX, + pageY: pageY, + deltaX: deltaX, + deltaY: deltaY, + }; + }; + return PointerDragging; + }()); + // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) + function isPrimaryMouseButton(ev) { + return ev.button === 0 && !ev.ctrlKey; + } + // Ignoring fake mouse events generated by touch + // ---------------------------------------------------------------------------------------------------- + function startIgnoringMouse() { + ignoreMouseDepth += 1; + setTimeout(function () { + ignoreMouseDepth -= 1; + }, config.touchMouseIgnoreWait); + } + // We want to attach touchmove as early as possible for Safari + // ---------------------------------------------------------------------------------------------------- + function listenerCreated() { + listenerCnt += 1; + if (listenerCnt === 1) { + window.addEventListener('touchmove', onWindowTouchMove, { passive: false }); + } + } + function listenerDestroyed() { + listenerCnt -= 1; + if (!listenerCnt) { + window.removeEventListener('touchmove', onWindowTouchMove, { passive: false }); + } + } + function onWindowTouchMove(ev) { + if (isWindowTouchMoveCancelled) { + ev.preventDefault(); + } + } + + /* + An effect in which an element follows the movement of a pointer across the screen. + The moving element is a clone of some other element. + Must call start + handleMove + stop. + */ + var ElementMirror = /** @class */ (function () { + function ElementMirror() { + this.isVisible = false; // must be explicitly enabled + this.sourceEl = null; + this.mirrorEl = null; + this.sourceElRect = null; // screen coords relative to viewport + // options that can be set directly by caller + this.parentNode = document.body; // HIGHLY SUGGESTED to set this to sidestep ShadowDOM issues + this.zIndex = 9999; + this.revertDuration = 0; + } + ElementMirror.prototype.start = function (sourceEl, pageX, pageY) { + this.sourceEl = sourceEl; + this.sourceElRect = this.sourceEl.getBoundingClientRect(); + this.origScreenX = pageX - window.pageXOffset; + this.origScreenY = pageY - window.pageYOffset; + this.deltaX = 0; + this.deltaY = 0; + this.updateElPosition(); + }; + ElementMirror.prototype.handleMove = function (pageX, pageY) { + this.deltaX = (pageX - window.pageXOffset) - this.origScreenX; + this.deltaY = (pageY - window.pageYOffset) - this.origScreenY; + this.updateElPosition(); + }; + // can be called before start + ElementMirror.prototype.setIsVisible = function (bool) { + if (bool) { + if (!this.isVisible) { + if (this.mirrorEl) { + this.mirrorEl.style.display = ''; + } + this.isVisible = bool; // needs to happen before updateElPosition + this.updateElPosition(); // because was not updating the position while invisible + } + } + else if (this.isVisible) { + if (this.mirrorEl) { + this.mirrorEl.style.display = 'none'; + } + this.isVisible = bool; + } + }; + // always async + ElementMirror.prototype.stop = function (needsRevertAnimation, callback) { + var _this = this; + var done = function () { + _this.cleanup(); + callback(); + }; + if (needsRevertAnimation && + this.mirrorEl && + this.isVisible && + this.revertDuration && // if 0, transition won't work + (this.deltaX || this.deltaY) // if same coords, transition won't work + ) { + this.doRevertAnimation(done, this.revertDuration); + } + else { + setTimeout(done, 0); + } + }; + ElementMirror.prototype.doRevertAnimation = function (callback, revertDuration) { + var mirrorEl = this.mirrorEl; + var finalSourceElRect = this.sourceEl.getBoundingClientRect(); // because autoscrolling might have happened + mirrorEl.style.transition = + 'top ' + revertDuration + 'ms,' + + 'left ' + revertDuration + 'ms'; + applyStyle(mirrorEl, { + left: finalSourceElRect.left, + top: finalSourceElRect.top, + }); + whenTransitionDone(mirrorEl, function () { + mirrorEl.style.transition = ''; + callback(); + }); + }; + ElementMirror.prototype.cleanup = function () { + if (this.mirrorEl) { + removeElement(this.mirrorEl); + this.mirrorEl = null; + } + this.sourceEl = null; + }; + ElementMirror.prototype.updateElPosition = function () { + if (this.sourceEl && this.isVisible) { + applyStyle(this.getMirrorEl(), { + left: this.sourceElRect.left + this.deltaX, + top: this.sourceElRect.top + this.deltaY, + }); + } + }; + ElementMirror.prototype.getMirrorEl = function () { + var sourceElRect = this.sourceElRect; + var mirrorEl = this.mirrorEl; + if (!mirrorEl) { + mirrorEl = this.mirrorEl = this.sourceEl.cloneNode(true); // cloneChildren=true + // we don't want long taps or any mouse interaction causing selection/menus. + // would use preventSelection(), but that prevents selectstart, causing problems. + mirrorEl.classList.add('fc-unselectable'); + mirrorEl.classList.add('fc-event-dragging'); + applyStyle(mirrorEl, { + position: 'fixed', + zIndex: this.zIndex, + visibility: '', + boxSizing: 'border-box', + width: sourceElRect.right - sourceElRect.left, + height: sourceElRect.bottom - sourceElRect.top, + right: 'auto', + bottom: 'auto', + margin: 0, + }); + this.parentNode.appendChild(mirrorEl); + } + return mirrorEl; + }; + return ElementMirror; + }()); + + /* + Is a cache for a given element's scroll information (all the info that ScrollController stores) + in addition the "client rectangle" of the element.. the area within the scrollbars. + + The cache can be in one of two modes: + - doesListening:false - ignores when the container is scrolled by someone else + - doesListening:true - watch for scrolling and update the cache + */ + var ScrollGeomCache = /** @class */ (function (_super) { + __extends(ScrollGeomCache, _super); + function ScrollGeomCache(scrollController, doesListening) { + var _this = _super.call(this) || this; + _this.handleScroll = function () { + _this.scrollTop = _this.scrollController.getScrollTop(); + _this.scrollLeft = _this.scrollController.getScrollLeft(); + _this.handleScrollChange(); + }; + _this.scrollController = scrollController; + _this.doesListening = doesListening; + _this.scrollTop = _this.origScrollTop = scrollController.getScrollTop(); + _this.scrollLeft = _this.origScrollLeft = scrollController.getScrollLeft(); + _this.scrollWidth = scrollController.getScrollWidth(); + _this.scrollHeight = scrollController.getScrollHeight(); + _this.clientWidth = scrollController.getClientWidth(); + _this.clientHeight = scrollController.getClientHeight(); + _this.clientRect = _this.computeClientRect(); // do last in case it needs cached values + if (_this.doesListening) { + _this.getEventTarget().addEventListener('scroll', _this.handleScroll); + } + return _this; + } + ScrollGeomCache.prototype.destroy = function () { + if (this.doesListening) { + this.getEventTarget().removeEventListener('scroll', this.handleScroll); + } + }; + ScrollGeomCache.prototype.getScrollTop = function () { + return this.scrollTop; + }; + ScrollGeomCache.prototype.getScrollLeft = function () { + return this.scrollLeft; + }; + ScrollGeomCache.prototype.setScrollTop = function (top) { + this.scrollController.setScrollTop(top); + if (!this.doesListening) { + // we are not relying on the element to normalize out-of-bounds scroll values + // so we need to sanitize ourselves + this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0); + this.handleScrollChange(); + } + }; + ScrollGeomCache.prototype.setScrollLeft = function (top) { + this.scrollController.setScrollLeft(top); + if (!this.doesListening) { + // we are not relying on the element to normalize out-of-bounds scroll values + // so we need to sanitize ourselves + this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0); + this.handleScrollChange(); + } + }; + ScrollGeomCache.prototype.getClientWidth = function () { + return this.clientWidth; + }; + ScrollGeomCache.prototype.getClientHeight = function () { + return this.clientHeight; + }; + ScrollGeomCache.prototype.getScrollWidth = function () { + return this.scrollWidth; + }; + ScrollGeomCache.prototype.getScrollHeight = function () { + return this.scrollHeight; + }; + ScrollGeomCache.prototype.handleScrollChange = function () { + }; + return ScrollGeomCache; + }(ScrollController)); + + var ElementScrollGeomCache = /** @class */ (function (_super) { + __extends(ElementScrollGeomCache, _super); + function ElementScrollGeomCache(el, doesListening) { + return _super.call(this, new ElementScrollController(el), doesListening) || this; + } + ElementScrollGeomCache.prototype.getEventTarget = function () { + return this.scrollController.el; + }; + ElementScrollGeomCache.prototype.computeClientRect = function () { + return computeInnerRect(this.scrollController.el); + }; + return ElementScrollGeomCache; + }(ScrollGeomCache)); + + var WindowScrollGeomCache = /** @class */ (function (_super) { + __extends(WindowScrollGeomCache, _super); + function WindowScrollGeomCache(doesListening) { + return _super.call(this, new WindowScrollController(), doesListening) || this; + } + WindowScrollGeomCache.prototype.getEventTarget = function () { + return window; + }; + WindowScrollGeomCache.prototype.computeClientRect = function () { + return { + left: this.scrollLeft, + right: this.scrollLeft + this.clientWidth, + top: this.scrollTop, + bottom: this.scrollTop + this.clientHeight, + }; + }; + // the window is the only scroll object that changes it's rectangle relative + // to the document's topleft as it scrolls + WindowScrollGeomCache.prototype.handleScrollChange = function () { + this.clientRect = this.computeClientRect(); + }; + return WindowScrollGeomCache; + }(ScrollGeomCache)); + + // If available we are using native "performance" API instead of "Date" + // Read more about it on MDN: + // https://developer.mozilla.org/en-US/docs/Web/API/Performance + var getTime = typeof performance === 'function' ? performance.now : Date.now; + /* + For a pointer interaction, automatically scrolls certain scroll containers when the pointer + approaches the edge. + + The caller must call start + handleMove + stop. + */ + var AutoScroller = /** @class */ (function () { + function AutoScroller() { + var _this = this; + // options that can be set by caller + this.isEnabled = true; + this.scrollQuery = [window, '.fc-scroller']; + this.edgeThreshold = 50; // pixels + this.maxVelocity = 300; // pixels per second + // internal state + this.pointerScreenX = null; + this.pointerScreenY = null; + this.isAnimating = false; + this.scrollCaches = null; + // protect against the initial pointerdown being too close to an edge and starting the scroll + this.everMovedUp = false; + this.everMovedDown = false; + this.everMovedLeft = false; + this.everMovedRight = false; + this.animate = function () { + if (_this.isAnimating) { // wasn't cancelled between animation calls + var edge = _this.computeBestEdge(_this.pointerScreenX + window.pageXOffset, _this.pointerScreenY + window.pageYOffset); + if (edge) { + var now = getTime(); + _this.handleSide(edge, (now - _this.msSinceRequest) / 1000); + _this.requestAnimation(now); + } + else { + _this.isAnimating = false; // will stop animation + } + } + }; + } + AutoScroller.prototype.start = function (pageX, pageY, scrollStartEl) { + if (this.isEnabled) { + this.scrollCaches = this.buildCaches(scrollStartEl); + this.pointerScreenX = null; + this.pointerScreenY = null; + this.everMovedUp = false; + this.everMovedDown = false; + this.everMovedLeft = false; + this.everMovedRight = false; + this.handleMove(pageX, pageY); + } + }; + AutoScroller.prototype.handleMove = function (pageX, pageY) { + if (this.isEnabled) { + var pointerScreenX = pageX - window.pageXOffset; + var pointerScreenY = pageY - window.pageYOffset; + var yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY; + var xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX; + if (yDelta < 0) { + this.everMovedUp = true; + } + else if (yDelta > 0) { + this.everMovedDown = true; + } + if (xDelta < 0) { + this.everMovedLeft = true; + } + else if (xDelta > 0) { + this.everMovedRight = true; + } + this.pointerScreenX = pointerScreenX; + this.pointerScreenY = pointerScreenY; + if (!this.isAnimating) { + this.isAnimating = true; + this.requestAnimation(getTime()); + } + } + }; + AutoScroller.prototype.stop = function () { + if (this.isEnabled) { + this.isAnimating = false; // will stop animation + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + scrollCache.destroy(); + } + this.scrollCaches = null; + } + }; + AutoScroller.prototype.requestAnimation = function (now) { + this.msSinceRequest = now; + requestAnimationFrame(this.animate); + }; + AutoScroller.prototype.handleSide = function (edge, seconds) { + var scrollCache = edge.scrollCache; + var edgeThreshold = this.edgeThreshold; + var invDistance = edgeThreshold - edge.distance; + var velocity = // the closer to the edge, the faster we scroll + ((invDistance * invDistance) / (edgeThreshold * edgeThreshold)) * // quadratic + this.maxVelocity * seconds; + var sign = 1; + switch (edge.name) { + case 'left': + sign = -1; + // falls through + case 'right': + scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign); + break; + case 'top': + sign = -1; + // falls through + case 'bottom': + scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign); + break; + } + }; + // left/top are relative to document topleft + AutoScroller.prototype.computeBestEdge = function (left, top) { + var edgeThreshold = this.edgeThreshold; + var bestSide = null; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + var rect = scrollCache.clientRect; + var leftDist = left - rect.left; + var rightDist = rect.right - left; + var topDist = top - rect.top; + var bottomDist = rect.bottom - top; + // completely within the rect? + if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) { + if (topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() && + (!bestSide || bestSide.distance > topDist)) { + bestSide = { scrollCache: scrollCache, name: 'top', distance: topDist }; + } + if (bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() && + (!bestSide || bestSide.distance > bottomDist)) { + bestSide = { scrollCache: scrollCache, name: 'bottom', distance: bottomDist }; + } + if (leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() && + (!bestSide || bestSide.distance > leftDist)) { + bestSide = { scrollCache: scrollCache, name: 'left', distance: leftDist }; + } + if (rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() && + (!bestSide || bestSide.distance > rightDist)) { + bestSide = { scrollCache: scrollCache, name: 'right', distance: rightDist }; + } + } + } + return bestSide; + }; + AutoScroller.prototype.buildCaches = function (scrollStartEl) { + return this.queryScrollEls(scrollStartEl).map(function (el) { + if (el === window) { + return new WindowScrollGeomCache(false); // false = don't listen to user-generated scrolls + } + return new ElementScrollGeomCache(el, false); // false = don't listen to user-generated scrolls + }); + }; + AutoScroller.prototype.queryScrollEls = function (scrollStartEl) { + var els = []; + for (var _i = 0, _a = this.scrollQuery; _i < _a.length; _i++) { + var query = _a[_i]; + if (typeof query === 'object') { + els.push(query); + } + else { + els.push.apply(els, Array.prototype.slice.call(getElRoot(scrollStartEl).querySelectorAll(query))); + } + } + return els; + }; + return AutoScroller; + }()); + + /* + Monitors dragging on an element. Has a number of high-level features: + - minimum distance required before dragging + - minimum wait time ("delay") before dragging + - a mirror element that follows the pointer + */ + var FeaturefulElementDragging = /** @class */ (function (_super) { + __extends(FeaturefulElementDragging, _super); + function FeaturefulElementDragging(containerEl, selector) { + var _this = _super.call(this, containerEl) || this; + _this.containerEl = containerEl; + // options that can be directly set by caller + // the caller can also set the PointerDragging's options as well + _this.delay = null; + _this.minDistance = 0; + _this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag + _this.mirrorNeedsRevert = false; + _this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup + _this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation + _this.isDelayEnded = false; + _this.isDistanceSurpassed = false; + _this.delayTimeoutId = null; + _this.onPointerDown = function (ev) { + if (!_this.isDragging) { // so new drag doesn't happen while revert animation is going + _this.isInteracting = true; + _this.isDelayEnded = false; + _this.isDistanceSurpassed = false; + preventSelection(document.body); + preventContextMenu(document.body); + // prevent links from being visited if there's an eventual drag. + // also prevents selection in older browsers (maybe?). + // not necessary for touch, besides, browser would complain about passiveness. + if (!ev.isTouch) { + ev.origEvent.preventDefault(); + } + _this.emitter.trigger('pointerdown', ev); + if (_this.isInteracting && // not destroyed via pointerdown handler + !_this.pointer.shouldIgnoreMove) { + // actions related to initiating dragstart+dragmove+dragend... + _this.mirror.setIsVisible(false); // reset. caller must set-visible + _this.mirror.start(ev.subjectEl, ev.pageX, ev.pageY); // must happen on first pointer down + _this.startDelay(ev); + if (!_this.minDistance) { + _this.handleDistanceSurpassed(ev); + } + } + } + }; + _this.onPointerMove = function (ev) { + if (_this.isInteracting) { + _this.emitter.trigger('pointermove', ev); + if (!_this.isDistanceSurpassed) { + var minDistance = _this.minDistance; + var distanceSq = void 0; // current distance from the origin, squared + var deltaX = ev.deltaX, deltaY = ev.deltaY; + distanceSq = deltaX * deltaX + deltaY * deltaY; + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem + _this.handleDistanceSurpassed(ev); + } + } + if (_this.isDragging) { + // a real pointer move? (not one simulated by scrolling) + if (ev.origEvent.type !== 'scroll') { + _this.mirror.handleMove(ev.pageX, ev.pageY); + _this.autoScroller.handleMove(ev.pageX, ev.pageY); + } + _this.emitter.trigger('dragmove', ev); + } + } + }; + _this.onPointerUp = function (ev) { + if (_this.isInteracting) { + _this.isInteracting = false; + allowSelection(document.body); + allowContextMenu(document.body); + _this.emitter.trigger('pointerup', ev); // can potentially set mirrorNeedsRevert + if (_this.isDragging) { + _this.autoScroller.stop(); + _this.tryStopDrag(ev); // which will stop the mirror + } + if (_this.delayTimeoutId) { + clearTimeout(_this.delayTimeoutId); + _this.delayTimeoutId = null; + } + } + }; + var pointer = _this.pointer = new PointerDragging(containerEl); + pointer.emitter.on('pointerdown', _this.onPointerDown); + pointer.emitter.on('pointermove', _this.onPointerMove); + pointer.emitter.on('pointerup', _this.onPointerUp); + if (selector) { + pointer.selector = selector; + } + _this.mirror = new ElementMirror(); + _this.autoScroller = new AutoScroller(); + return _this; + } + FeaturefulElementDragging.prototype.destroy = function () { + this.pointer.destroy(); + // HACK: simulate a pointer-up to end the current drag + // TODO: fire 'dragend' directly and stop interaction. discourage use of pointerup event (b/c might not fire) + this.onPointerUp({}); + }; + FeaturefulElementDragging.prototype.startDelay = function (ev) { + var _this = this; + if (typeof this.delay === 'number') { + this.delayTimeoutId = setTimeout(function () { + _this.delayTimeoutId = null; + _this.handleDelayEnd(ev); + }, this.delay); // not assignable to number! + } + else { + this.handleDelayEnd(ev); + } + }; + FeaturefulElementDragging.prototype.handleDelayEnd = function (ev) { + this.isDelayEnded = true; + this.tryStartDrag(ev); + }; + FeaturefulElementDragging.prototype.handleDistanceSurpassed = function (ev) { + this.isDistanceSurpassed = true; + this.tryStartDrag(ev); + }; + FeaturefulElementDragging.prototype.tryStartDrag = function (ev) { + if (this.isDelayEnded && this.isDistanceSurpassed) { + if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) { + this.isDragging = true; + this.mirrorNeedsRevert = false; + this.autoScroller.start(ev.pageX, ev.pageY, this.containerEl); + this.emitter.trigger('dragstart', ev); + if (this.touchScrollAllowed === false) { + this.pointer.cancelTouchScroll(); + } + } + } + }; + FeaturefulElementDragging.prototype.tryStopDrag = function (ev) { + // .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events + // that come from the document to fire beforehand. much more convenient this way. + this.mirror.stop(this.mirrorNeedsRevert, this.stopDrag.bind(this, ev)); + }; + FeaturefulElementDragging.prototype.stopDrag = function (ev) { + this.isDragging = false; + this.emitter.trigger('dragend', ev); + }; + // fill in the implementations... + FeaturefulElementDragging.prototype.setIgnoreMove = function (bool) { + this.pointer.shouldIgnoreMove = bool; + }; + FeaturefulElementDragging.prototype.setMirrorIsVisible = function (bool) { + this.mirror.setIsVisible(bool); + }; + FeaturefulElementDragging.prototype.setMirrorNeedsRevert = function (bool) { + this.mirrorNeedsRevert = bool; + }; + FeaturefulElementDragging.prototype.setAutoScrollEnabled = function (bool) { + this.autoScroller.isEnabled = bool; + }; + return FeaturefulElementDragging; + }(ElementDragging)); + + /* + When this class is instantiated, it records the offset of an element (relative to the document topleft), + and continues to monitor scrolling, updating the cached coordinates if it needs to. + Does not access the DOM after instantiation, so highly performant. + + Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element + and an determine if a given point is inside the combined clipping rectangle. + */ + var OffsetTracker = /** @class */ (function () { + function OffsetTracker(el) { + this.origRect = computeRect(el); + // will work fine for divs that have overflow:hidden + this.scrollCaches = getClippingParents(el).map(function (scrollEl) { return new ElementScrollGeomCache(scrollEl, true); }); + } + OffsetTracker.prototype.destroy = function () { + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + scrollCache.destroy(); + } + }; + OffsetTracker.prototype.computeLeft = function () { + var left = this.origRect.left; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + left += scrollCache.origScrollLeft - scrollCache.getScrollLeft(); + } + return left; + }; + OffsetTracker.prototype.computeTop = function () { + var top = this.origRect.top; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + top += scrollCache.origScrollTop - scrollCache.getScrollTop(); + } + return top; + }; + OffsetTracker.prototype.isWithinClipping = function (pageX, pageY) { + var point = { left: pageX, top: pageY }; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + if (!isIgnoredClipping(scrollCache.getEventTarget()) && + !pointInsideRect(point, scrollCache.clientRect)) { + return false; + } + } + return true; + }; + return OffsetTracker; + }()); + // certain clipping containers should never constrain interactions, like and + // https://github.com/fullcalendar/fullcalendar/issues/3615 + function isIgnoredClipping(node) { + var tagName = node.tagName; + return tagName === 'HTML' || tagName === 'BODY'; + } + + /* + Tracks movement over multiple droppable areas (aka "hits") + that exist in one or more DateComponents. + Relies on an existing draggable. + + emits: + - pointerdown + - dragstart + - hitchange - fires initially, even if not over a hit + - pointerup + - (hitchange - again, to null, if ended over a hit) + - dragend + */ + var HitDragging = /** @class */ (function () { + function HitDragging(dragging, droppableStore) { + var _this = this; + // options that can be set by caller + this.useSubjectCenter = false; + this.requireInitial = true; // if doesn't start out on a hit, won't emit any events + this.initialHit = null; + this.movingHit = null; + this.finalHit = null; // won't ever be populated if shouldIgnoreMove + this.handlePointerDown = function (ev) { + var dragging = _this.dragging; + _this.initialHit = null; + _this.movingHit = null; + _this.finalHit = null; + _this.prepareHits(); + _this.processFirstCoord(ev); + if (_this.initialHit || !_this.requireInitial) { + dragging.setIgnoreMove(false); + // TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :( + _this.emitter.trigger('pointerdown', ev); + } + else { + dragging.setIgnoreMove(true); + } + }; + this.handleDragStart = function (ev) { + _this.emitter.trigger('dragstart', ev); + _this.handleMove(ev, true); // force = fire even if initially null + }; + this.handleDragMove = function (ev) { + _this.emitter.trigger('dragmove', ev); + _this.handleMove(ev); + }; + this.handlePointerUp = function (ev) { + _this.releaseHits(); + _this.emitter.trigger('pointerup', ev); + }; + this.handleDragEnd = function (ev) { + if (_this.movingHit) { + _this.emitter.trigger('hitupdate', null, true, ev); + } + _this.finalHit = _this.movingHit; + _this.movingHit = null; + _this.emitter.trigger('dragend', ev); + }; + this.droppableStore = droppableStore; + dragging.emitter.on('pointerdown', this.handlePointerDown); + dragging.emitter.on('dragstart', this.handleDragStart); + dragging.emitter.on('dragmove', this.handleDragMove); + dragging.emitter.on('pointerup', this.handlePointerUp); + dragging.emitter.on('dragend', this.handleDragEnd); + this.dragging = dragging; + this.emitter = new Emitter(); + } + // sets initialHit + // sets coordAdjust + HitDragging.prototype.processFirstCoord = function (ev) { + var origPoint = { left: ev.pageX, top: ev.pageY }; + var adjustedPoint = origPoint; + var subjectEl = ev.subjectEl; + var subjectRect; + if (subjectEl instanceof HTMLElement) { // i.e. not a Document/ShadowRoot + subjectRect = computeRect(subjectEl); + adjustedPoint = constrainPoint(adjustedPoint, subjectRect); + } + var initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top); + if (initialHit) { + if (this.useSubjectCenter && subjectRect) { + var slicedSubjectRect = intersectRects(subjectRect, initialHit.rect); + if (slicedSubjectRect) { + adjustedPoint = getRectCenter(slicedSubjectRect); + } + } + this.coordAdjust = diffPoints(adjustedPoint, origPoint); + } + else { + this.coordAdjust = { left: 0, top: 0 }; + } + }; + HitDragging.prototype.handleMove = function (ev, forceHandle) { + var hit = this.queryHitForOffset(ev.pageX + this.coordAdjust.left, ev.pageY + this.coordAdjust.top); + if (forceHandle || !isHitsEqual(this.movingHit, hit)) { + this.movingHit = hit; + this.emitter.trigger('hitupdate', hit, false, ev); + } + }; + HitDragging.prototype.prepareHits = function () { + this.offsetTrackers = mapHash(this.droppableStore, function (interactionSettings) { + interactionSettings.component.prepareHits(); + return new OffsetTracker(interactionSettings.el); + }); + }; + HitDragging.prototype.releaseHits = function () { + var offsetTrackers = this.offsetTrackers; + for (var id in offsetTrackers) { + offsetTrackers[id].destroy(); + } + this.offsetTrackers = {}; + }; + HitDragging.prototype.queryHitForOffset = function (offsetLeft, offsetTop) { + var _a = this, droppableStore = _a.droppableStore, offsetTrackers = _a.offsetTrackers; + var bestHit = null; + for (var id in droppableStore) { + var component = droppableStore[id].component; + var offsetTracker = offsetTrackers[id]; + if (offsetTracker && // wasn't destroyed mid-drag + offsetTracker.isWithinClipping(offsetLeft, offsetTop)) { + var originLeft = offsetTracker.computeLeft(); + var originTop = offsetTracker.computeTop(); + var positionLeft = offsetLeft - originLeft; + var positionTop = offsetTop - originTop; + var origRect = offsetTracker.origRect; + var width = origRect.right - origRect.left; + var height = origRect.bottom - origRect.top; + if ( + // must be within the element's bounds + positionLeft >= 0 && positionLeft < width && + positionTop >= 0 && positionTop < height) { + var hit = component.queryHit(positionLeft, positionTop, width, height); + if (hit && ( + // make sure the hit is within activeRange, meaning it's not a dead cell + rangeContainsRange(hit.dateProfile.activeRange, hit.dateSpan.range)) && + (!bestHit || hit.layer > bestHit.layer)) { + hit.componentId = id; + hit.context = component.context; + // TODO: better way to re-orient rectangle + hit.rect.left += originLeft; + hit.rect.right += originLeft; + hit.rect.top += originTop; + hit.rect.bottom += originTop; + bestHit = hit; + } + } + } + } + return bestHit; + }; + return HitDragging; + }()); + function isHitsEqual(hit0, hit1) { + if (!hit0 && !hit1) { + return true; + } + if (Boolean(hit0) !== Boolean(hit1)) { + return false; + } + return isDateSpansEqual(hit0.dateSpan, hit1.dateSpan); + } + + function buildDatePointApiWithContext(dateSpan, context) { + var props = {}; + for (var _i = 0, _a = context.pluginHooks.datePointTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(props, transform(dateSpan, context)); + } + __assign(props, buildDatePointApi(dateSpan, context.dateEnv)); + return props; + } + function buildDatePointApi(span, dateEnv) { + return { + date: dateEnv.toDate(span.range.start), + dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }), + allDay: span.allDay, + }; + } + + /* + Monitors when the user clicks on a specific date/time of a component. + A pointerdown+pointerup on the same "hit" constitutes a click. + */ + var DateClicking = /** @class */ (function (_super) { + __extends(DateClicking, _super); + function DateClicking(settings) { + var _this = _super.call(this, settings) || this; + _this.handlePointerDown = function (pev) { + var dragging = _this.dragging; + var downEl = pev.origEvent.target; + // do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired + dragging.setIgnoreMove(!_this.component.isValidDateDownEl(downEl)); + }; + // won't even fire if moving was ignored + _this.handleDragEnd = function (ev) { + var component = _this.component; + var pointer = _this.dragging.pointer; + if (!pointer.wasTouchScroll) { + var _a = _this.hitDragging, initialHit = _a.initialHit, finalHit = _a.finalHit; + if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) { + var context = component.context; + var arg = __assign(__assign({}, buildDatePointApiWithContext(initialHit.dateSpan, context)), { dayEl: initialHit.dayEl, jsEvent: ev.origEvent, view: context.viewApi || context.calendarApi.view }); + context.emitter.trigger('dateClick', arg); + } + } + }; + // we DO want to watch pointer moves because otherwise finalHit won't get populated + _this.dragging = new FeaturefulElementDragging(settings.el); + _this.dragging.autoScroller.isEnabled = false; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + DateClicking.prototype.destroy = function () { + this.dragging.destroy(); + }; + return DateClicking; + }(Interaction)); + + /* + Tracks when the user selects a portion of time of a component, + constituted by a drag over date cells, with a possible delay at the beginning of the drag. + */ + var DateSelecting = /** @class */ (function (_super) { + __extends(DateSelecting, _super); + function DateSelecting(settings) { + var _this = _super.call(this, settings) || this; + _this.dragSelection = null; + _this.handlePointerDown = function (ev) { + var _a = _this, component = _a.component, dragging = _a.dragging; + var options = component.context.options; + var canSelect = options.selectable && + component.isValidDateDownEl(ev.origEvent.target); + // don't bother to watch expensive moves if component won't do selection + dragging.setIgnoreMove(!canSelect); + // if touch, require user to hold down + dragging.delay = ev.isTouch ? getComponentTouchDelay$1(component) : null; + }; + _this.handleDragStart = function (ev) { + _this.component.context.calendarApi.unselect(ev); // unselect previous selections + }; + _this.handleHitUpdate = function (hit, isFinal) { + var context = _this.component.context; + var dragSelection = null; + var isInvalid = false; + if (hit) { + var initialHit = _this.hitDragging.initialHit; + var disallowed = hit.componentId === initialHit.componentId + && _this.isHitComboAllowed + && !_this.isHitComboAllowed(initialHit, hit); + if (!disallowed) { + dragSelection = joinHitsIntoSelection(initialHit, hit, context.pluginHooks.dateSelectionTransformers); + } + if (!dragSelection || !isDateSelectionValid(dragSelection, hit.dateProfile, context)) { + isInvalid = true; + dragSelection = null; + } + } + if (dragSelection) { + context.dispatch({ type: 'SELECT_DATES', selection: dragSelection }); + } + else if (!isFinal) { // only unselect if moved away while dragging + context.dispatch({ type: 'UNSELECT_DATES' }); + } + if (!isInvalid) { + enableCursor(); + } + else { + disableCursor(); + } + if (!isFinal) { + _this.dragSelection = dragSelection; // only clear if moved away from all hits while dragging + } + }; + _this.handlePointerUp = function (pev) { + if (_this.dragSelection) { + // selection is already rendered, so just need to report selection + triggerDateSelect(_this.dragSelection, pev, _this.component.context); + _this.dragSelection = null; + } + }; + var component = settings.component; + var options = component.context.options; + var dragging = _this.dragging = new FeaturefulElementDragging(settings.el); + dragging.touchScrollAllowed = false; + dragging.minDistance = options.selectMinDistance || 0; + dragging.autoScroller.isEnabled = options.dragScroll; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('pointerup', _this.handlePointerUp); + return _this; + } + DateSelecting.prototype.destroy = function () { + this.dragging.destroy(); + }; + return DateSelecting; + }(Interaction)); + function getComponentTouchDelay$1(component) { + var options = component.context.options; + var delay = options.selectLongPressDelay; + if (delay == null) { + delay = options.longPressDelay; + } + return delay; + } + function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) { + var dateSpan0 = hit0.dateSpan; + var dateSpan1 = hit1.dateSpan; + var ms = [ + dateSpan0.range.start, + dateSpan0.range.end, + dateSpan1.range.start, + dateSpan1.range.end, + ]; + ms.sort(compareNumbers); + var props = {}; + for (var _i = 0, dateSelectionTransformers_1 = dateSelectionTransformers; _i < dateSelectionTransformers_1.length; _i++) { + var transformer = dateSelectionTransformers_1[_i]; + var res = transformer(hit0, hit1); + if (res === false) { + return null; + } + if (res) { + __assign(props, res); + } + } + props.range = { start: ms[0], end: ms[3] }; + props.allDay = dateSpan0.allDay; + return props; + } + + var EventDragging = /** @class */ (function (_super) { + __extends(EventDragging, _super); + function EventDragging(settings) { + var _this = _super.call(this, settings) || this; + // internal state + _this.subjectEl = null; + _this.subjectSeg = null; // the seg being selected/dragged + _this.isDragging = false; + _this.eventRange = null; + _this.relevantEvents = null; // the events being dragged + _this.receivingContext = null; + _this.validMutation = null; + _this.mutatedRelevantEvents = null; + _this.handlePointerDown = function (ev) { + var origTarget = ev.origEvent.target; + var _a = _this, component = _a.component, dragging = _a.dragging; + var mirror = dragging.mirror; + var options = component.context.options; + var initialContext = component.context; + _this.subjectEl = ev.subjectEl; + var subjectSeg = _this.subjectSeg = getElSeg(ev.subjectEl); + var eventRange = _this.eventRange = subjectSeg.eventRange; + var eventInstanceId = eventRange.instance.instanceId; + _this.relevantEvents = getRelevantEvents(initialContext.getCurrentData().eventStore, eventInstanceId); + dragging.minDistance = ev.isTouch ? 0 : options.eventDragMinDistance; + dragging.delay = + // only do a touch delay if touch and this event hasn't been selected yet + (ev.isTouch && eventInstanceId !== component.props.eventSelection) ? + getComponentTouchDelay(component) : + null; + if (options.fixedMirrorParent) { + mirror.parentNode = options.fixedMirrorParent; + } + else { + mirror.parentNode = elementClosest(origTarget, '.fc'); + } + mirror.revertDuration = options.dragRevertDuration; + var isValid = component.isValidSegDownEl(origTarget) && + !elementClosest(origTarget, '.fc-event-resizer'); // NOT on a resizer + dragging.setIgnoreMove(!isValid); + // disable dragging for elements that are resizable (ie, selectable) + // but are not draggable + _this.isDragging = isValid && + ev.subjectEl.classList.contains('fc-event-draggable'); + }; + _this.handleDragStart = function (ev) { + var initialContext = _this.component.context; + var eventRange = _this.eventRange; + var eventInstanceId = eventRange.instance.instanceId; + if (ev.isTouch) { + // need to select a different event? + if (eventInstanceId !== _this.component.props.eventSelection) { + initialContext.dispatch({ type: 'SELECT_EVENT', eventInstanceId: eventInstanceId }); + } + } + else { + // if now using mouse, but was previous touch interaction, clear selected event + initialContext.dispatch({ type: 'UNSELECT_EVENT' }); + } + if (_this.isDragging) { + initialContext.calendarApi.unselect(ev); // unselect *date* selection + initialContext.emitter.trigger('eventDragStart', { + el: _this.subjectEl, + event: new EventApi(initialContext, eventRange.def, eventRange.instance), + jsEvent: ev.origEvent, + view: initialContext.viewApi, + }); + } + }; + _this.handleHitUpdate = function (hit, isFinal) { + if (!_this.isDragging) { + return; + } + var relevantEvents = _this.relevantEvents; + var initialHit = _this.hitDragging.initialHit; + var initialContext = _this.component.context; + // states based on new hit + var receivingContext = null; + var mutation = null; + var mutatedRelevantEvents = null; + var isInvalid = false; + var interaction = { + affectedEvents: relevantEvents, + mutatedEvents: createEmptyEventStore(), + isEvent: true, + }; + if (hit) { + receivingContext = hit.context; + var receivingOptions = receivingContext.options; + if (initialContext === receivingContext || + (receivingOptions.editable && receivingOptions.droppable)) { + mutation = computeEventMutation(initialHit, hit, receivingContext.getCurrentData().pluginHooks.eventDragMutationMassagers); + if (mutation) { + mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, receivingContext.getCurrentData().eventUiBases, mutation, receivingContext); + interaction.mutatedEvents = mutatedRelevantEvents; + if (!isInteractionValid(interaction, hit.dateProfile, receivingContext)) { + isInvalid = true; + mutation = null; + mutatedRelevantEvents = null; + interaction.mutatedEvents = createEmptyEventStore(); + } + } + } + else { + receivingContext = null; + } + } + _this.displayDrag(receivingContext, interaction); + if (!isInvalid) { + enableCursor(); + } + else { + disableCursor(); + } + if (!isFinal) { + if (initialContext === receivingContext && // TODO: write test for this + isHitsEqual(initialHit, hit)) { + mutation = null; + } + _this.dragging.setMirrorNeedsRevert(!mutation); + // render the mirror if no already-rendered mirror + // TODO: wish we could somehow wait for dispatch to guarantee render + _this.dragging.setMirrorIsVisible(!hit || !getElRoot(_this.subjectEl).querySelector('.fc-event-mirror')); + // assign states based on new hit + _this.receivingContext = receivingContext; + _this.validMutation = mutation; + _this.mutatedRelevantEvents = mutatedRelevantEvents; + } + }; + _this.handlePointerUp = function () { + if (!_this.isDragging) { + _this.cleanup(); // because handleDragEnd won't fire + } + }; + _this.handleDragEnd = function (ev) { + if (_this.isDragging) { + var initialContext_1 = _this.component.context; + var initialView = initialContext_1.viewApi; + var _a = _this, receivingContext_1 = _a.receivingContext, validMutation = _a.validMutation; + var eventDef = _this.eventRange.def; + var eventInstance = _this.eventRange.instance; + var eventApi = new EventApi(initialContext_1, eventDef, eventInstance); + var relevantEvents_1 = _this.relevantEvents; + var mutatedRelevantEvents_1 = _this.mutatedRelevantEvents; + var finalHit = _this.hitDragging.finalHit; + _this.clearDrag(); // must happen after revert animation + initialContext_1.emitter.trigger('eventDragStop', { + el: _this.subjectEl, + event: eventApi, + jsEvent: ev.origEvent, + view: initialView, + }); + if (validMutation) { + // dropped within same calendar + if (receivingContext_1 === initialContext_1) { + var updatedEventApi = new EventApi(initialContext_1, mutatedRelevantEvents_1.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents_1.instances[eventInstance.instanceId] : null); + initialContext_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents_1, + }); + var eventChangeArg = { + oldEvent: eventApi, + event: updatedEventApi, + relatedEvents: buildEventApis(mutatedRelevantEvents_1, initialContext_1, eventInstance), + revert: function () { + initialContext_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents_1, // the pre-change data + }); + }, + }; + var transformed = {}; + for (var _i = 0, _b = initialContext_1.getCurrentData().pluginHooks.eventDropTransformers; _i < _b.length; _i++) { + var transformer = _b[_i]; + __assign(transformed, transformer(validMutation, initialContext_1)); + } + initialContext_1.emitter.trigger('eventDrop', __assign(__assign(__assign({}, eventChangeArg), transformed), { el: ev.subjectEl, delta: validMutation.datesDelta, jsEvent: ev.origEvent, view: initialView })); + initialContext_1.emitter.trigger('eventChange', eventChangeArg); + // dropped in different calendar + } + else if (receivingContext_1) { + var eventRemoveArg = { + event: eventApi, + relatedEvents: buildEventApis(relevantEvents_1, initialContext_1, eventInstance), + revert: function () { + initialContext_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents_1, + }); + }, + }; + initialContext_1.emitter.trigger('eventLeave', __assign(__assign({}, eventRemoveArg), { draggedEl: ev.subjectEl, view: initialView })); + initialContext_1.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: relevantEvents_1, + }); + initialContext_1.emitter.trigger('eventRemove', eventRemoveArg); + var addedEventDef = mutatedRelevantEvents_1.defs[eventDef.defId]; + var addedEventInstance = mutatedRelevantEvents_1.instances[eventInstance.instanceId]; + var addedEventApi = new EventApi(receivingContext_1, addedEventDef, addedEventInstance); + receivingContext_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents_1, + }); + var eventAddArg = { + event: addedEventApi, + relatedEvents: buildEventApis(mutatedRelevantEvents_1, receivingContext_1, addedEventInstance), + revert: function () { + receivingContext_1.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: mutatedRelevantEvents_1, + }); + }, + }; + receivingContext_1.emitter.trigger('eventAdd', eventAddArg); + if (ev.isTouch) { + receivingContext_1.dispatch({ + type: 'SELECT_EVENT', + eventInstanceId: eventInstance.instanceId, + }); + } + receivingContext_1.emitter.trigger('drop', __assign(__assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext_1)), { draggedEl: ev.subjectEl, jsEvent: ev.origEvent, view: finalHit.context.viewApi })); + receivingContext_1.emitter.trigger('eventReceive', __assign(__assign({}, eventAddArg), { draggedEl: ev.subjectEl, view: finalHit.context.viewApi })); + } + } + else { + initialContext_1.emitter.trigger('_noEventDrop'); + } + } + _this.cleanup(); + }; + var component = _this.component; + var options = component.context.options; + var dragging = _this.dragging = new FeaturefulElementDragging(settings.el); + dragging.pointer.selector = EventDragging.SELECTOR; + dragging.touchScrollAllowed = false; + dragging.autoScroller.isEnabled = options.dragScroll; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsStore); + hitDragging.useSubjectCenter = settings.useEventCenter; + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('pointerup', _this.handlePointerUp); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + EventDragging.prototype.destroy = function () { + this.dragging.destroy(); + }; + // render a drag state on the next receivingCalendar + EventDragging.prototype.displayDrag = function (nextContext, state) { + var initialContext = this.component.context; + var prevContext = this.receivingContext; + // does the previous calendar need to be cleared? + if (prevContext && prevContext !== nextContext) { + // does the initial calendar need to be cleared? + // if so, don't clear all the way. we still need to to hide the affectedEvents + if (prevContext === initialContext) { + prevContext.dispatch({ + type: 'SET_EVENT_DRAG', + state: { + affectedEvents: state.affectedEvents, + mutatedEvents: createEmptyEventStore(), + isEvent: true, + }, + }); + // completely clear the old calendar if it wasn't the initial + } + else { + prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + } + if (nextContext) { + nextContext.dispatch({ type: 'SET_EVENT_DRAG', state: state }); + } + }; + EventDragging.prototype.clearDrag = function () { + var initialCalendar = this.component.context; + var receivingContext = this.receivingContext; + if (receivingContext) { + receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + // the initial calendar might have an dummy drag state from displayDrag + if (initialCalendar !== receivingContext) { + initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + }; + EventDragging.prototype.cleanup = function () { + this.subjectSeg = null; + this.isDragging = false; + this.eventRange = null; + this.relevantEvents = null; + this.receivingContext = null; + this.validMutation = null; + this.mutatedRelevantEvents = null; + }; + // TODO: test this in IE11 + // QUESTION: why do we need it on the resizable??? + EventDragging.SELECTOR = '.fc-event-draggable, .fc-event-resizable'; + return EventDragging; + }(Interaction)); + function computeEventMutation(hit0, hit1, massagers) { + var dateSpan0 = hit0.dateSpan; + var dateSpan1 = hit1.dateSpan; + var date0 = dateSpan0.range.start; + var date1 = dateSpan1.range.start; + var standardProps = {}; + if (dateSpan0.allDay !== dateSpan1.allDay) { + standardProps.allDay = dateSpan1.allDay; + standardProps.hasEnd = hit1.context.options.allDayMaintainDuration; + if (dateSpan1.allDay) { + // means date1 is already start-of-day, + // but date0 needs to be converted + date0 = startOfDay(date0); + } + } + var delta = diffDates(date0, date1, hit0.context.dateEnv, hit0.componentId === hit1.componentId ? + hit0.largeUnit : + null); + if (delta.milliseconds) { // has hours/minutes/seconds + standardProps.allDay = false; + } + var mutation = { + datesDelta: delta, + standardProps: standardProps, + }; + for (var _i = 0, massagers_1 = massagers; _i < massagers_1.length; _i++) { + var massager = massagers_1[_i]; + massager(mutation, hit0, hit1); + } + return mutation; + } + function getComponentTouchDelay(component) { + var options = component.context.options; + var delay = options.eventLongPressDelay; + if (delay == null) { + delay = options.longPressDelay; + } + return delay; + } + + var EventResizing = /** @class */ (function (_super) { + __extends(EventResizing, _super); + function EventResizing(settings) { + var _this = _super.call(this, settings) || this; + // internal state + _this.draggingSegEl = null; + _this.draggingSeg = null; // TODO: rename to resizingSeg? subjectSeg? + _this.eventRange = null; + _this.relevantEvents = null; + _this.validMutation = null; + _this.mutatedRelevantEvents = null; + _this.handlePointerDown = function (ev) { + var component = _this.component; + var segEl = _this.querySegEl(ev); + var seg = getElSeg(segEl); + var eventRange = _this.eventRange = seg.eventRange; + _this.dragging.minDistance = component.context.options.eventDragMinDistance; + // if touch, need to be working with a selected event + _this.dragging.setIgnoreMove(!_this.component.isValidSegDownEl(ev.origEvent.target) || + (ev.isTouch && _this.component.props.eventSelection !== eventRange.instance.instanceId)); + }; + _this.handleDragStart = function (ev) { + var context = _this.component.context; + var eventRange = _this.eventRange; + _this.relevantEvents = getRelevantEvents(context.getCurrentData().eventStore, _this.eventRange.instance.instanceId); + var segEl = _this.querySegEl(ev); + _this.draggingSegEl = segEl; + _this.draggingSeg = getElSeg(segEl); + context.calendarApi.unselect(); + context.emitter.trigger('eventResizeStart', { + el: segEl, + event: new EventApi(context, eventRange.def, eventRange.instance), + jsEvent: ev.origEvent, + view: context.viewApi, + }); + }; + _this.handleHitUpdate = function (hit, isFinal, ev) { + var context = _this.component.context; + var relevantEvents = _this.relevantEvents; + var initialHit = _this.hitDragging.initialHit; + var eventInstance = _this.eventRange.instance; + var mutation = null; + var mutatedRelevantEvents = null; + var isInvalid = false; + var interaction = { + affectedEvents: relevantEvents, + mutatedEvents: createEmptyEventStore(), + isEvent: true, + }; + if (hit) { + var disallowed = hit.componentId === initialHit.componentId + && _this.isHitComboAllowed + && !_this.isHitComboAllowed(initialHit, hit); + if (!disallowed) { + mutation = computeMutation(initialHit, hit, ev.subjectEl.classList.contains('fc-event-resizer-start'), eventInstance.range); + } + } + if (mutation) { + mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, context.getCurrentData().eventUiBases, mutation, context); + interaction.mutatedEvents = mutatedRelevantEvents; + if (!isInteractionValid(interaction, hit.dateProfile, context)) { + isInvalid = true; + mutation = null; + mutatedRelevantEvents = null; + interaction.mutatedEvents = null; + } + } + if (mutatedRelevantEvents) { + context.dispatch({ + type: 'SET_EVENT_RESIZE', + state: interaction, + }); + } + else { + context.dispatch({ type: 'UNSET_EVENT_RESIZE' }); + } + if (!isInvalid) { + enableCursor(); + } + else { + disableCursor(); + } + if (!isFinal) { + if (mutation && isHitsEqual(initialHit, hit)) { + mutation = null; + } + _this.validMutation = mutation; + _this.mutatedRelevantEvents = mutatedRelevantEvents; + } + }; + _this.handleDragEnd = function (ev) { + var context = _this.component.context; + var eventDef = _this.eventRange.def; + var eventInstance = _this.eventRange.instance; + var eventApi = new EventApi(context, eventDef, eventInstance); + var relevantEvents = _this.relevantEvents; + var mutatedRelevantEvents = _this.mutatedRelevantEvents; + context.emitter.trigger('eventResizeStop', { + el: _this.draggingSegEl, + event: eventApi, + jsEvent: ev.origEvent, + view: context.viewApi, + }); + if (_this.validMutation) { + var updatedEventApi = new EventApi(context, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null); + context.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents, + }); + var eventChangeArg = { + oldEvent: eventApi, + event: updatedEventApi, + relatedEvents: buildEventApis(mutatedRelevantEvents, context, eventInstance), + revert: function () { + context.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents, // the pre-change events + }); + }, + }; + context.emitter.trigger('eventResize', __assign(__assign({}, eventChangeArg), { el: _this.draggingSegEl, startDelta: _this.validMutation.startDelta || createDuration(0), endDelta: _this.validMutation.endDelta || createDuration(0), jsEvent: ev.origEvent, view: context.viewApi })); + context.emitter.trigger('eventChange', eventChangeArg); + } + else { + context.emitter.trigger('_noEventResize'); + } + // reset all internal state + _this.draggingSeg = null; + _this.relevantEvents = null; + _this.validMutation = null; + // okay to keep eventInstance around. useful to set it in handlePointerDown + }; + var component = settings.component; + var dragging = _this.dragging = new FeaturefulElementDragging(settings.el); + dragging.pointer.selector = '.fc-event-resizer'; + dragging.touchScrollAllowed = false; + dragging.autoScroller.isEnabled = component.context.options.dragScroll; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + EventResizing.prototype.destroy = function () { + this.dragging.destroy(); + }; + EventResizing.prototype.querySegEl = function (ev) { + return elementClosest(ev.subjectEl, '.fc-event'); + }; + return EventResizing; + }(Interaction)); + function computeMutation(hit0, hit1, isFromStart, instanceRange) { + var dateEnv = hit0.context.dateEnv; + var date0 = hit0.dateSpan.range.start; + var date1 = hit1.dateSpan.range.start; + var delta = diffDates(date0, date1, dateEnv, hit0.largeUnit); + if (isFromStart) { + if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) { + return { startDelta: delta }; + } + } + else if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) { + return { endDelta: delta }; + } + return null; + } + + var UnselectAuto = /** @class */ (function () { + function UnselectAuto(context) { + var _this = this; + this.context = context; + this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system + this.matchesCancel = false; + this.matchesEvent = false; + this.onSelect = function (selectInfo) { + if (selectInfo.jsEvent) { + _this.isRecentPointerDateSelect = true; + } + }; + this.onDocumentPointerDown = function (pev) { + var unselectCancel = _this.context.options.unselectCancel; + var downEl = getEventTargetViaRoot(pev.origEvent); + _this.matchesCancel = !!elementClosest(downEl, unselectCancel); + _this.matchesEvent = !!elementClosest(downEl, EventDragging.SELECTOR); // interaction started on an event? + }; + this.onDocumentPointerUp = function (pev) { + var context = _this.context; + var documentPointer = _this.documentPointer; + var calendarState = context.getCurrentData(); + // touch-scrolling should never unfocus any type of selection + if (!documentPointer.wasTouchScroll) { + if (calendarState.dateSelection && // an existing date selection? + !_this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp? + ) { + var unselectAuto = context.options.unselectAuto; + if (unselectAuto && (!unselectAuto || !_this.matchesCancel)) { + context.calendarApi.unselect(pev); + } + } + if (calendarState.eventSelection && // an existing event selected? + !_this.matchesEvent // interaction DIDN'T start on an event + ) { + context.dispatch({ type: 'UNSELECT_EVENT' }); + } + } + _this.isRecentPointerDateSelect = false; + }; + var documentPointer = this.documentPointer = new PointerDragging(document); + documentPointer.shouldIgnoreMove = true; + documentPointer.shouldWatchScroll = false; + documentPointer.emitter.on('pointerdown', this.onDocumentPointerDown); + documentPointer.emitter.on('pointerup', this.onDocumentPointerUp); + /* + TODO: better way to know about whether there was a selection with the pointer + */ + context.emitter.on('select', this.onSelect); + } + UnselectAuto.prototype.destroy = function () { + this.context.emitter.off('select', this.onSelect); + this.documentPointer.destroy(); + }; + return UnselectAuto; + }()); + + var OPTION_REFINERS$3 = { + fixedMirrorParent: identity, + }; + var LISTENER_REFINERS = { + dateClick: identity, + eventDragStart: identity, + eventDragStop: identity, + eventDrop: identity, + eventResizeStart: identity, + eventResizeStop: identity, + eventResize: identity, + drop: identity, + eventReceive: identity, + eventLeave: identity, + }; + + /* + Given an already instantiated draggable object for one-or-more elements, + Interprets any dragging as an attempt to drag an events that lives outside + of a calendar onto a calendar. + */ + var ExternalElementDragging = /** @class */ (function () { + function ExternalElementDragging(dragging, suppliedDragMeta) { + var _this = this; + this.receivingContext = null; + this.droppableEvent = null; // will exist for all drags, even if create:false + this.suppliedDragMeta = null; + this.dragMeta = null; + this.handleDragStart = function (ev) { + _this.dragMeta = _this.buildDragMeta(ev.subjectEl); + }; + this.handleHitUpdate = function (hit, isFinal, ev) { + var dragging = _this.hitDragging.dragging; + var receivingContext = null; + var droppableEvent = null; + var isInvalid = false; + var interaction = { + affectedEvents: createEmptyEventStore(), + mutatedEvents: createEmptyEventStore(), + isEvent: _this.dragMeta.create, + }; + if (hit) { + receivingContext = hit.context; + if (_this.canDropElOnCalendar(ev.subjectEl, receivingContext)) { + droppableEvent = computeEventForDateSpan(hit.dateSpan, _this.dragMeta, receivingContext); + interaction.mutatedEvents = eventTupleToStore(droppableEvent); + isInvalid = !isInteractionValid(interaction, hit.dateProfile, receivingContext); + if (isInvalid) { + interaction.mutatedEvents = createEmptyEventStore(); + droppableEvent = null; + } + } + } + _this.displayDrag(receivingContext, interaction); + // show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?) + // TODO: wish we could somehow wait for dispatch to guarantee render + dragging.setMirrorIsVisible(isFinal || !droppableEvent || !document.querySelector('.fc-event-mirror')); + if (!isInvalid) { + enableCursor(); + } + else { + disableCursor(); + } + if (!isFinal) { + dragging.setMirrorNeedsRevert(!droppableEvent); + _this.receivingContext = receivingContext; + _this.droppableEvent = droppableEvent; + } + }; + this.handleDragEnd = function (pev) { + var _a = _this, receivingContext = _a.receivingContext, droppableEvent = _a.droppableEvent; + _this.clearDrag(); + if (receivingContext && droppableEvent) { + var finalHit = _this.hitDragging.finalHit; + var finalView = finalHit.context.viewApi; + var dragMeta = _this.dragMeta; + receivingContext.emitter.trigger('drop', __assign(__assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext)), { draggedEl: pev.subjectEl, jsEvent: pev.origEvent, view: finalView })); + if (dragMeta.create) { + var addingEvents_1 = eventTupleToStore(droppableEvent); + receivingContext.dispatch({ + type: 'MERGE_EVENTS', + eventStore: addingEvents_1, + }); + if (pev.isTouch) { + receivingContext.dispatch({ + type: 'SELECT_EVENT', + eventInstanceId: droppableEvent.instance.instanceId, + }); + } + // signal that an external event landed + receivingContext.emitter.trigger('eventReceive', { + event: new EventApi(receivingContext, droppableEvent.def, droppableEvent.instance), + relatedEvents: [], + revert: function () { + receivingContext.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: addingEvents_1, + }); + }, + draggedEl: pev.subjectEl, + view: finalView, + }); + } + } + _this.receivingContext = null; + _this.droppableEvent = null; + }; + var hitDragging = this.hitDragging = new HitDragging(dragging, interactionSettingsStore); + hitDragging.requireInitial = false; // will start outside of a component + hitDragging.emitter.on('dragstart', this.handleDragStart); + hitDragging.emitter.on('hitupdate', this.handleHitUpdate); + hitDragging.emitter.on('dragend', this.handleDragEnd); + this.suppliedDragMeta = suppliedDragMeta; + } + ExternalElementDragging.prototype.buildDragMeta = function (subjectEl) { + if (typeof this.suppliedDragMeta === 'object') { + return parseDragMeta(this.suppliedDragMeta); + } + if (typeof this.suppliedDragMeta === 'function') { + return parseDragMeta(this.suppliedDragMeta(subjectEl)); + } + return getDragMetaFromEl(subjectEl); + }; + ExternalElementDragging.prototype.displayDrag = function (nextContext, state) { + var prevContext = this.receivingContext; + if (prevContext && prevContext !== nextContext) { + prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + if (nextContext) { + nextContext.dispatch({ type: 'SET_EVENT_DRAG', state: state }); + } + }; + ExternalElementDragging.prototype.clearDrag = function () { + if (this.receivingContext) { + this.receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + }; + ExternalElementDragging.prototype.canDropElOnCalendar = function (el, receivingContext) { + var dropAccept = receivingContext.options.dropAccept; + if (typeof dropAccept === 'function') { + return dropAccept.call(receivingContext.calendarApi, el); + } + if (typeof dropAccept === 'string' && dropAccept) { + return Boolean(elementMatches(el, dropAccept)); + } + return true; + }; + return ExternalElementDragging; + }()); + // Utils for computing event store from the DragMeta + // ---------------------------------------------------------------------------------------------------- + function computeEventForDateSpan(dateSpan, dragMeta, context) { + var defProps = __assign({}, dragMeta.leftoverProps); + for (var _i = 0, _a = context.pluginHooks.externalDefTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(defProps, transform(dateSpan, dragMeta)); + } + var _b = refineEventDef(defProps, context), refined = _b.refined, extra = _b.extra; + var def = parseEventDef(refined, extra, dragMeta.sourceId, dateSpan.allDay, context.options.forceEventDuration || Boolean(dragMeta.duration), // hasEnd + context); + var start = dateSpan.range.start; + // only rely on time info if drop zone is all-day, + // otherwise, we already know the time + if (dateSpan.allDay && dragMeta.startTime) { + start = context.dateEnv.add(start, dragMeta.startTime); + } + var end = dragMeta.duration ? + context.dateEnv.add(start, dragMeta.duration) : + getDefaultEventEnd(dateSpan.allDay, start, context); + var instance = createEventInstance(def.defId, { start: start, end: end }); + return { def: def, instance: instance }; + } + // Utils for extracting data from element + // ---------------------------------------------------------------------------------------------------- + function getDragMetaFromEl(el) { + var str = getEmbeddedElData(el, 'event'); + var obj = str ? + JSON.parse(str) : + { create: false }; // if no embedded data, assume no event creation + return parseDragMeta(obj); + } + config.dataAttrPrefix = ''; + function getEmbeddedElData(el, name) { + var prefix = config.dataAttrPrefix; + var prefixedName = (prefix ? prefix + '-' : '') + name; + return el.getAttribute('data-' + prefixedName) || ''; + } + + /* + Makes an element (that is *external* to any calendar) draggable. + Can pass in data that determines how an event will be created when dropped onto a calendar. + Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system. + */ + var ExternalDraggable = /** @class */ (function () { + function ExternalDraggable(el, settings) { + var _this = this; + if (settings === void 0) { settings = {}; } + this.handlePointerDown = function (ev) { + var dragging = _this.dragging; + var _a = _this.settings, minDistance = _a.minDistance, longPressDelay = _a.longPressDelay; + dragging.minDistance = + minDistance != null ? + minDistance : + (ev.isTouch ? 0 : BASE_OPTION_DEFAULTS.eventDragMinDistance); + dragging.delay = + ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv + (longPressDelay != null ? longPressDelay : BASE_OPTION_DEFAULTS.longPressDelay) : + 0; + }; + this.handleDragStart = function (ev) { + if (ev.isTouch && + _this.dragging.delay && + ev.subjectEl.classList.contains('fc-event')) { + _this.dragging.mirror.getMirrorEl().classList.add('fc-event-selected'); + } + }; + this.settings = settings; + var dragging = this.dragging = new FeaturefulElementDragging(el); + dragging.touchScrollAllowed = false; + if (settings.itemSelector != null) { + dragging.pointer.selector = settings.itemSelector; + } + if (settings.appendTo != null) { + dragging.mirror.parentNode = settings.appendTo; // TODO: write tests + } + dragging.emitter.on('pointerdown', this.handlePointerDown); + dragging.emitter.on('dragstart', this.handleDragStart); + new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new + } + ExternalDraggable.prototype.destroy = function () { + this.dragging.destroy(); + }; + return ExternalDraggable; + }()); + + /* + Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements. + The third-party system is responsible for drawing the visuals effects of the drag. + This class simply monitors for pointer movements and fires events. + It also has the ability to hide the moving element (the "mirror") during the drag. + */ + var InferredElementDragging = /** @class */ (function (_super) { + __extends(InferredElementDragging, _super); + function InferredElementDragging(containerEl) { + var _this = _super.call(this, containerEl) || this; + _this.shouldIgnoreMove = false; + _this.mirrorSelector = ''; + _this.currentMirrorEl = null; + _this.handlePointerDown = function (ev) { + _this.emitter.trigger('pointerdown', ev); + if (!_this.shouldIgnoreMove) { + // fire dragstart right away. does not support delay or min-distance + _this.emitter.trigger('dragstart', ev); + } + }; + _this.handlePointerMove = function (ev) { + if (!_this.shouldIgnoreMove) { + _this.emitter.trigger('dragmove', ev); + } + }; + _this.handlePointerUp = function (ev) { + _this.emitter.trigger('pointerup', ev); + if (!_this.shouldIgnoreMove) { + // fire dragend right away. does not support a revert animation + _this.emitter.trigger('dragend', ev); + } + }; + var pointer = _this.pointer = new PointerDragging(containerEl); + pointer.emitter.on('pointerdown', _this.handlePointerDown); + pointer.emitter.on('pointermove', _this.handlePointerMove); + pointer.emitter.on('pointerup', _this.handlePointerUp); + return _this; + } + InferredElementDragging.prototype.destroy = function () { + this.pointer.destroy(); + }; + InferredElementDragging.prototype.setIgnoreMove = function (bool) { + this.shouldIgnoreMove = bool; + }; + InferredElementDragging.prototype.setMirrorIsVisible = function (bool) { + if (bool) { + // restore a previously hidden element. + // use the reference in case the selector class has already been removed. + if (this.currentMirrorEl) { + this.currentMirrorEl.style.visibility = ''; + this.currentMirrorEl = null; + } + } + else { + var mirrorEl = this.mirrorSelector + // TODO: somehow query FullCalendars WITHIN shadow-roots + ? document.querySelector(this.mirrorSelector) + : null; + if (mirrorEl) { + this.currentMirrorEl = mirrorEl; + mirrorEl.style.visibility = 'hidden'; + } + } + }; + return InferredElementDragging; + }(ElementDragging)); + + /* + Bridges third-party drag-n-drop systems with FullCalendar. + Must be instantiated and destroyed by caller. + */ + var ThirdPartyDraggable = /** @class */ (function () { + function ThirdPartyDraggable(containerOrSettings, settings) { + var containerEl = document; + if ( + // wish we could just test instanceof EventTarget, but doesn't work in IE11 + containerOrSettings === document || + containerOrSettings instanceof Element) { + containerEl = containerOrSettings; + settings = settings || {}; + } + else { + settings = (containerOrSettings || {}); + } + var dragging = this.dragging = new InferredElementDragging(containerEl); + if (typeof settings.itemSelector === 'string') { + dragging.pointer.selector = settings.itemSelector; + } + else if (containerEl === document) { + dragging.pointer.selector = '[data-event]'; + } + if (typeof settings.mirrorSelector === 'string') { + dragging.mirrorSelector = settings.mirrorSelector; + } + new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new + } + ThirdPartyDraggable.prototype.destroy = function () { + this.dragging.destroy(); + }; + return ThirdPartyDraggable; + }()); + + var interactionPlugin = createPlugin({ + componentInteractions: [DateClicking, DateSelecting, EventDragging, EventResizing], + calendarInteractions: [UnselectAuto], + elementDraggingImpl: FeaturefulElementDragging, + optionRefiners: OPTION_REFINERS$3, + listenerRefiners: LISTENER_REFINERS, + }); + + /* An abstract class for the daygrid views, as well as month view. Renders one or more rows of day cells. + ----------------------------------------------------------------------------------------------------------------------*/ + // It is a manager for a Table subcomponent, which does most of the heavy lifting. + // It is responsible for managing width/height. + var TableView = /** @class */ (function (_super) { + __extends(TableView, _super); + function TableView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.headerElRef = createRef(); + return _this; + } + TableView.prototype.renderSimpleLayout = function (headerRowContent, bodyContent) { + var _a = this, props = _a.props, context = _a.context; + 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, + }, + }); + } + sections.push({ + type: 'body', + key: 'body', + liquid: true, + chunk: { content: bodyContent }, + }); + return (createElement(ViewRoot, { viewSpec: context.viewSpec }, function (rootElRef, classNames) { return (createElement("div", { ref: rootElRef, className: ['fc-daygrid'].concat(classNames).join(' ') }, + createElement(SimpleScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, collapsibleWidth: props.forPrint, cols: [] /* TODO: make optional? */, sections: sections }))); })); + }; + TableView.prototype.renderHScrollLayout = function (headerRowContent, bodyContent, colCnt, dayMinWidth) { + var ScrollGrid = this.context.pluginHooks.scrollGridImpl; + if (!ScrollGrid) { + throw new Error('No ScrollGrid implementation'); + } + var _a = this, props = _a.props, context = _a.context; + 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, + chunks: [{ + key: 'main', + elRef: this.headerElRef, + tableClassName: 'fc-col-header', + rowContent: headerRowContent, + }], + }); + } + sections.push({ + type: 'body', + key: 'body', + liquid: true, + chunks: [{ + key: 'main', + content: bodyContent, + }], + }); + if (stickyFooterScrollbar) { + sections.push({ + type: 'footer', + key: 'footer', + isSticky: true, + chunks: [{ + key: 'main', + content: renderScrollShim, + }], + }); + } + return (createElement(ViewRoot, { viewSpec: context.viewSpec }, function (rootElRef, classNames) { return (createElement("div", { ref: rootElRef, className: ['fc-daygrid'].concat(classNames).join(' ') }, + createElement(ScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, collapsibleWidth: props.forPrint, colGroups: [{ cols: [{ span: colCnt, minWidth: dayMinWidth }] }], sections: sections }))); })); + }; + return TableView; + }(DateComponent)); + + function splitSegsByRow(segs, rowCnt) { + var byRow = []; + for (var i = 0; i < rowCnt; i += 1) { + byRow[i] = []; + } + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + byRow[seg.row].push(seg); + } + return byRow; + } + function splitSegsByFirstCol(segs, colCnt) { + var byCol = []; + for (var i = 0; i < colCnt; i += 1) { + byCol[i] = []; + } + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + byCol[seg.firstCol].push(seg); + } + return byCol; + } + function splitInteractionByRow(ui, rowCnt) { + var byRow = []; + if (!ui) { + for (var i = 0; i < rowCnt; i += 1) { + byRow[i] = null; + } + } + else { + for (var i = 0; i < rowCnt; i += 1) { + byRow[i] = { + affectedInstances: ui.affectedInstances, + isEvent: ui.isEvent, + segs: [], + }; + } + for (var _i = 0, _a = ui.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + byRow[seg.row].segs.push(seg); + } + } + return byRow; + } + + var TableCellTop = /** @class */ (function (_super) { + __extends(TableCellTop, _super); + function TableCellTop() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableCellTop.prototype.render = function () { + var props = this.props; + var navLinkAttrs = this.context.options.navLinks + ? { 'data-navlink': buildNavLinkData(props.date), tabIndex: 0 } + : {}; + return (createElement(DayCellContent, { date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, showDayNumber: props.showDayNumber, extraHookProps: props.extraHookProps, defaultContent: renderTopInner }, function (innerElRef, innerContent) { return ((innerContent || props.forceDayTop) && (createElement("div", { className: "fc-daygrid-day-top", ref: innerElRef }, + createElement("a", __assign({ className: "fc-daygrid-day-number" }, navLinkAttrs), innerContent || createElement(Fragment, null, "\u00A0"))))); })); + }; + return TableCellTop; + }(BaseComponent)); + function renderTopInner(props) { + return props.dayNumberText; + } + + var DEFAULT_TABLE_EVENT_TIME_FORMAT = createFormatter({ + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true, + meridiem: 'narrow', + }); + function hasListItemDisplay(seg) { + var display = seg.eventRange.ui.display; + return display === 'list-item' || (display === 'auto' && + !seg.eventRange.def.allDay && + seg.firstCol === seg.lastCol && // can't be multi-day + seg.isStart && // " + seg.isEnd // " + ); + } + + var TableBlockEvent = /** @class */ (function (_super) { + __extends(TableBlockEvent, _super); + function TableBlockEvent() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableBlockEvent.prototype.render = function () { + var props = this.props; + return (createElement(StandardEvent, __assign({}, props, { extraClassNames: ['fc-daygrid-event', 'fc-daygrid-block-event', 'fc-h-event'], defaultTimeFormat: DEFAULT_TABLE_EVENT_TIME_FORMAT, defaultDisplayEventEnd: props.defaultDisplayEventEnd, disableResizing: !props.seg.eventRange.def.allDay }))); + }; + return TableBlockEvent; + }(BaseComponent)); + + var TableListItemEvent = /** @class */ (function (_super) { + __extends(TableListItemEvent, _super); + function TableListItemEvent() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableListItemEvent.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var timeFormat = context.options.eventTimeFormat || DEFAULT_TABLE_EVENT_TIME_FORMAT; + var timeText = buildSegTimeText(props.seg, timeFormat, context, true, props.defaultDisplayEventEnd); + return (createElement(EventRoot, { seg: props.seg, timeText: timeText, defaultContent: renderInnerContent$2, isDragging: props.isDragging, isResizing: false, isDateSelecting: false, isSelected: props.isSelected, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday }, function (rootElRef, classNames, innerElRef, innerContent) { return ( // we don't use styles! + createElement("a", __assign({ className: ['fc-daygrid-event', 'fc-daygrid-dot-event'].concat(classNames).join(' '), ref: rootElRef }, getSegAnchorAttrs(props.seg)), innerContent)); })); + }; + return TableListItemEvent; + }(BaseComponent)); + function renderInnerContent$2(innerProps) { + return (createElement(Fragment, null, + createElement("div", { className: "fc-daygrid-event-dot", style: { borderColor: innerProps.borderColor || innerProps.backgroundColor } }), + innerProps.timeText && (createElement("div", { className: "fc-event-time" }, innerProps.timeText)), + createElement("div", { className: "fc-event-title" }, innerProps.event.title || createElement(Fragment, null, "\u00A0")))); + } + function getSegAnchorAttrs(seg) { + var url = seg.eventRange.def.url; + return url ? { href: url } : {}; + } + + var TableCellMoreLink = /** @class */ (function (_super) { + __extends(TableCellMoreLink, _super); + function TableCellMoreLink() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.compileSegs = memoize(compileSegs); + return _this; + } + TableCellMoreLink.prototype.render = function () { + var props = this.props; + var _a = this.compileSegs(props.singlePlacements), allSegs = _a.allSegs, invisibleSegs = _a.invisibleSegs; + return (createElement(MoreLinkRoot, { dateProfile: props.dateProfile, todayRange: props.todayRange, allDayDate: props.allDayDate, moreCnt: props.moreCnt, allSegs: allSegs, hiddenSegs: invisibleSegs, alignmentElRef: props.alignmentElRef, alignGridTop: props.alignGridTop, extraDateSpan: props.extraDateSpan, popoverContent: function () { + var isForcedInvisible = (props.eventDrag ? props.eventDrag.affectedInstances : null) || + (props.eventResize ? props.eventResize.affectedInstances : null) || + {}; + return (createElement(Fragment, null, allSegs.map(function (seg) { + var instanceId = seg.eventRange.instance.instanceId; + return (createElement("div", { className: "fc-daygrid-event-harness", key: instanceId, style: { + visibility: isForcedInvisible[instanceId] ? 'hidden' : '', + } }, hasListItemDisplay(seg) ? (createElement(TableListItemEvent, __assign({ seg: seg, isDragging: false, isSelected: instanceId === props.eventSelection, defaultDisplayEventEnd: false }, getSegMeta(seg, props.todayRange)))) : (createElement(TableBlockEvent, __assign({ seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: instanceId === props.eventSelection, defaultDisplayEventEnd: false }, getSegMeta(seg, props.todayRange)))))); + }))); + } }, function (rootElRef, classNames, innerElRef, innerContent, handleClick) { return (createElement("a", { ref: rootElRef, className: ['fc-daygrid-more-link'].concat(classNames).join(' '), onClick: handleClick }, innerContent)); })); + }; + return TableCellMoreLink; + }(BaseComponent)); + function compileSegs(singlePlacements) { + var allSegs = []; + var invisibleSegs = []; + for (var _i = 0, singlePlacements_1 = singlePlacements; _i < singlePlacements_1.length; _i++) { + var placement = singlePlacements_1[_i]; + allSegs.push(placement.seg); + if (!placement.isVisible) { + invisibleSegs.push(placement.seg); + } + } + return { allSegs: allSegs, invisibleSegs: invisibleSegs }; + } + + var DEFAULT_WEEK_NUM_FORMAT$1 = createFormatter({ week: 'narrow' }); + var TableCell = /** @class */ (function (_super) { + __extends(TableCell, _super); + function TableCell() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.rootElRef = createRef(); + _this.handleRootEl = function (el) { + setRef(_this.rootElRef, el); + setRef(_this.props.elRef, el); + }; + return _this; + } + TableCell.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context, rootElRef = _a.rootElRef; + var options = context.options; + var date = props.date, dateProfile = props.dateProfile; + var navLinkAttrs = options.navLinks + ? { 'data-navlink': buildNavLinkData(date, 'week'), tabIndex: 0 } + : {}; + return (createElement(DayCellRoot, { date: date, dateProfile: dateProfile, todayRange: props.todayRange, showDayNumber: props.showDayNumber, extraHookProps: props.extraHookProps, elRef: this.handleRootEl }, function (dayElRef, dayClassNames, rootDataAttrs, isDisabled) { return (createElement("td", __assign({ ref: dayElRef, className: ['fc-daygrid-day'].concat(dayClassNames, props.extraClassNames || []).join(' ') }, rootDataAttrs, props.extraDataAttrs), + createElement("div", { className: "fc-daygrid-day-frame fc-scrollgrid-sync-inner", ref: props.innerElRef /* different from hook system! RENAME */ }, + props.showWeekNumber && (createElement(WeekNumberRoot, { date: date, defaultFormat: DEFAULT_WEEK_NUM_FORMAT$1 }, function (weekElRef, weekClassNames, innerElRef, innerContent) { return (createElement("a", __assign({ ref: weekElRef, className: ['fc-daygrid-week-number'].concat(weekClassNames).join(' ') }, navLinkAttrs), innerContent)); })), + !isDisabled && (createElement(TableCellTop, { date: date, dateProfile: dateProfile, showDayNumber: props.showDayNumber, forceDayTop: props.forceDayTop, todayRange: props.todayRange, extraHookProps: props.extraHookProps })), + createElement("div", { className: "fc-daygrid-day-events", ref: props.fgContentElRef }, + props.fgContent, + createElement("div", { className: "fc-daygrid-day-bottom", style: { marginTop: props.moreMarginTop } }, + createElement(TableCellMoreLink, { allDayDate: date, singlePlacements: props.singlePlacements, moreCnt: props.moreCnt, alignmentElRef: rootElRef, alignGridTop: !props.showDayNumber, extraDateSpan: props.extraDateSpan, dateProfile: props.dateProfile, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, todayRange: props.todayRange }))), + createElement("div", { className: "fc-daygrid-day-bg" }, props.bgContent)))); })); + }; + return TableCell; + }(DateComponent)); + + function computeFgSegPlacement(segs, // assumed already sorted + dayMaxEvents, dayMaxEventRows, strictOrder, eventInstanceHeights, maxContentHeight, cells) { + var hierarchy = new DayGridSegHierarchy(); + hierarchy.allowReslicing = true; + hierarchy.strictOrder = strictOrder; + if (dayMaxEvents === true || dayMaxEventRows === true) { + hierarchy.maxCoord = maxContentHeight; + hierarchy.hiddenConsumes = true; + } + else if (typeof dayMaxEvents === 'number') { + hierarchy.maxStackCnt = dayMaxEvents; + } + else if (typeof dayMaxEventRows === 'number') { + hierarchy.maxStackCnt = dayMaxEventRows; + hierarchy.hiddenConsumes = true; + } + // create segInputs only for segs with known heights + var segInputs = []; + var unknownHeightSegs = []; + for (var i = 0; i < segs.length; i += 1) { + var seg = segs[i]; + var instanceId = seg.eventRange.instance.instanceId; + var eventHeight = eventInstanceHeights[instanceId]; + if (eventHeight != null) { + segInputs.push({ + index: i, + thickness: eventHeight, + span: { + start: seg.firstCol, + end: seg.lastCol + 1, + }, + }); + } + else { + unknownHeightSegs.push(seg); + } + } + var hiddenEntries = hierarchy.addSegs(segInputs); + var segRects = hierarchy.toRects(); + var _a = placeRects(segRects, segs, cells), singleColPlacements = _a.singleColPlacements, multiColPlacements = _a.multiColPlacements, leftoverMargins = _a.leftoverMargins; + var moreCnts = []; + var moreMarginTops = []; + // add segs with unknown heights + for (var _i = 0, unknownHeightSegs_1 = unknownHeightSegs; _i < unknownHeightSegs_1.length; _i++) { + var seg = unknownHeightSegs_1[_i]; + multiColPlacements[seg.firstCol].push({ + seg: seg, + isVisible: false, + isAbsolute: true, + absoluteTop: 0, + marginTop: 0, + }); + for (var col = seg.firstCol; col <= seg.lastCol; col += 1) { + singleColPlacements[col].push({ + seg: resliceSeg(seg, col, col + 1, cells), + isVisible: false, + isAbsolute: false, + absoluteTop: 0, + marginTop: 0, + }); + } + } + // add the hidden entries + for (var col = 0; col < cells.length; col += 1) { + moreCnts.push(0); + } + for (var _b = 0, hiddenEntries_1 = hiddenEntries; _b < hiddenEntries_1.length; _b++) { + var hiddenEntry = hiddenEntries_1[_b]; + var seg = segs[hiddenEntry.index]; + var hiddenSpan = hiddenEntry.span; + multiColPlacements[hiddenSpan.start].push({ + seg: resliceSeg(seg, hiddenSpan.start, hiddenSpan.end, cells), + isVisible: false, + isAbsolute: true, + absoluteTop: 0, + marginTop: 0, + }); + for (var col = hiddenSpan.start; col < hiddenSpan.end; col += 1) { + moreCnts[col] += 1; + singleColPlacements[col].push({ + seg: resliceSeg(seg, col, col + 1, cells), + isVisible: false, + isAbsolute: false, + absoluteTop: 0, + marginTop: 0, + }); + } + } + // deal with leftover margins + for (var col = 0; col < cells.length; col += 1) { + moreMarginTops.push(leftoverMargins[col]); + } + return { singleColPlacements: singleColPlacements, multiColPlacements: multiColPlacements, moreCnts: moreCnts, moreMarginTops: moreMarginTops }; + } + // rects ordered by top coord, then left + function placeRects(allRects, segs, cells) { + var rectsByEachCol = groupRectsByEachCol(allRects, cells.length); + var singleColPlacements = []; + var multiColPlacements = []; + var leftoverMargins = []; + for (var col = 0; col < cells.length; col += 1) { + var rects = rectsByEachCol[col]; + // compute all static segs in singlePlacements + var singlePlacements = []; + var currentHeight = 0; + var currentMarginTop = 0; + for (var _i = 0, rects_1 = rects; _i < rects_1.length; _i++) { + var rect = rects_1[_i]; + var seg = segs[rect.index]; + singlePlacements.push({ + seg: resliceSeg(seg, col, col + 1, cells), + isVisible: true, + isAbsolute: false, + absoluteTop: rect.levelCoord, + marginTop: rect.levelCoord - currentHeight, + }); + currentHeight = rect.levelCoord + rect.thickness; + } + // compute mixed static/absolute segs in multiPlacements + var multiPlacements = []; + currentHeight = 0; + currentMarginTop = 0; + for (var _a = 0, rects_2 = rects; _a < rects_2.length; _a++) { + var rect = rects_2[_a]; + var seg = segs[rect.index]; + var isAbsolute = rect.span.end - rect.span.start > 1; // multi-column? + var isFirstCol = rect.span.start === col; + currentMarginTop += rect.levelCoord - currentHeight; // amount of space since bottom of previous seg + currentHeight = rect.levelCoord + rect.thickness; // height will now be bottom of current seg + if (isAbsolute) { + currentMarginTop += rect.thickness; + if (isFirstCol) { + multiPlacements.push({ + seg: resliceSeg(seg, rect.span.start, rect.span.end, cells), + isVisible: true, + isAbsolute: true, + absoluteTop: rect.levelCoord, + marginTop: 0, + }); + } + } + else if (isFirstCol) { + multiPlacements.push({ + seg: resliceSeg(seg, rect.span.start, rect.span.end, cells), + isVisible: true, + isAbsolute: false, + absoluteTop: rect.levelCoord, + marginTop: currentMarginTop, // claim the margin + }); + currentMarginTop = 0; + } + } + singleColPlacements.push(singlePlacements); + multiColPlacements.push(multiPlacements); + leftoverMargins.push(currentMarginTop); + } + return { singleColPlacements: singleColPlacements, multiColPlacements: multiColPlacements, leftoverMargins: leftoverMargins }; + } + function groupRectsByEachCol(rects, colCnt) { + var rectsByEachCol = []; + for (var col = 0; col < colCnt; col += 1) { + rectsByEachCol.push([]); + } + for (var _i = 0, rects_3 = rects; _i < rects_3.length; _i++) { + var rect = rects_3[_i]; + for (var col = rect.span.start; col < rect.span.end; col += 1) { + rectsByEachCol[col].push(rect); + } + } + return rectsByEachCol; + } + function resliceSeg(seg, spanStart, spanEnd, cells) { + if (seg.firstCol === spanStart && seg.lastCol === spanEnd - 1) { + return seg; + } + var eventRange = seg.eventRange; + var origRange = eventRange.range; + var slicedRange = intersectRanges(origRange, { + start: cells[spanStart].date, + end: addDays(cells[spanEnd - 1].date, 1), + }); + return __assign(__assign({}, seg), { firstCol: spanStart, lastCol: spanEnd - 1, eventRange: { + def: eventRange.def, + ui: __assign(__assign({}, eventRange.ui), { durationEditable: false }), + instance: eventRange.instance, + range: slicedRange, + }, isStart: seg.isStart && slicedRange.start.valueOf() === origRange.start.valueOf(), isEnd: seg.isEnd && slicedRange.end.valueOf() === origRange.end.valueOf() }); + } + var DayGridSegHierarchy = /** @class */ (function (_super) { + __extends(DayGridSegHierarchy, _super); + function DayGridSegHierarchy() { + var _this = _super !== null && _super.apply(this, arguments) || this; + // config + _this.hiddenConsumes = false; + // allows us to keep hidden entries in the hierarchy so they take up space + _this.forceHidden = {}; + return _this; + } + DayGridSegHierarchy.prototype.addSegs = function (segInputs) { + var _this = this; + var hiddenSegs = _super.prototype.addSegs.call(this, segInputs); + var entriesByLevel = this.entriesByLevel; + var excludeHidden = function (entry) { return !_this.forceHidden[buildEntryKey(entry)]; }; + // remove the forced-hidden segs + for (var level = 0; level < entriesByLevel.length; level += 1) { + entriesByLevel[level] = entriesByLevel[level].filter(excludeHidden); + } + return hiddenSegs; + }; + DayGridSegHierarchy.prototype.handleInvalidInsertion = function (insertion, entry, hiddenEntries) { + var _a = this, entriesByLevel = _a.entriesByLevel, forceHidden = _a.forceHidden; + var touchingEntry = insertion.touchingEntry, touchingLevel = insertion.touchingLevel, touchingLateral = insertion.touchingLateral; + if (this.hiddenConsumes && touchingEntry) { + var touchingEntryId = buildEntryKey(touchingEntry); + // if not already hidden + if (!forceHidden[touchingEntryId]) { + if (this.allowReslicing) { + var placeholderEntry = __assign(__assign({}, touchingEntry), { span: intersectSpans(touchingEntry.span, entry.span) }); + var placeholderEntryId = buildEntryKey(placeholderEntry); + forceHidden[placeholderEntryId] = true; + entriesByLevel[touchingLevel][touchingLateral] = placeholderEntry; // replace touchingEntry with our placeholder + this.splitEntry(touchingEntry, entry, hiddenEntries); // split up the touchingEntry, reinsert it + } + else { + forceHidden[touchingEntryId] = true; + hiddenEntries.push(touchingEntry); + } + } + } + return _super.prototype.handleInvalidInsertion.call(this, insertion, entry, hiddenEntries); + }; + return DayGridSegHierarchy; + }(SegHierarchy)); + + var TableRow = /** @class */ (function (_super) { + __extends(TableRow, _super); + function TableRow() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.cellElRefs = new RefMap(); // the ? + 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 for the axis */, + createElement(TimeColsSlatsBody, { slatElRefs: this.slatElRefs, axis: props.axis, slatMetas: props.slatMetas })))); + }; + TimeColsSlats.prototype.componentDidMount = function () { + this.updateSizing(); + }; + TimeColsSlats.prototype.componentDidUpdate = function () { + this.updateSizing(); + }; + TimeColsSlats.prototype.componentWillUnmount = function () { + if (this.props.onCoords) { + this.props.onCoords(null); + } + }; + TimeColsSlats.prototype.updateSizing = function () { + var _a = this, context = _a.context, props = _a.props; + if (props.onCoords && + props.clientWidth !== null // means sizing has stabilized + ) { + var rootEl = this.rootElRef.current; + if (rootEl.offsetHeight) { // not hidden by css + props.onCoords(new TimeColsSlatsCoords(new PositionCache(this.rootElRef.current, collectSlatEls(this.slatElRefs.currentMap, props.slatMetas), false, true), this.props.dateProfile, context.options.slotDuration)); + } + } + }; + return TimeColsSlats; + }(BaseComponent)); + function collectSlatEls(elMap, slatMetas) { + return slatMetas.map(function (slatMeta) { return elMap[slatMeta.key]; }); + } + + function splitSegsByCol(segs, colCnt) { + var segsByCol = []; + var i; + for (i = 0; i < colCnt; i += 1) { + segsByCol.push([]); + } + if (segs) { + for (i = 0; i < segs.length; i += 1) { + segsByCol[segs[i].col].push(segs[i]); + } + } + return segsByCol; + } + function splitInteractionByCol(ui, colCnt) { + var byRow = []; + if (!ui) { + for (var i = 0; i < colCnt; i += 1) { + byRow[i] = null; + } + } + else { + for (var i = 0; i < colCnt; i += 1) { + byRow[i] = { + affectedInstances: ui.affectedInstances, + isEvent: ui.isEvent, + segs: [], + }; + } + for (var _i = 0, _a = ui.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + byRow[seg.col].segs.push(seg); + } + } + return byRow; + } + + var TimeColMoreLink = /** @class */ (function (_super) { + __extends(TimeColMoreLink, _super); + function TimeColMoreLink() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.rootElRef = createRef(); + return _this; + } + TimeColMoreLink.prototype.render = function () { + var _this = this; + var props = this.props; + return (createElement(MoreLinkRoot, { allDayDate: null, moreCnt: props.hiddenSegs.length, allSegs: props.hiddenSegs, hiddenSegs: props.hiddenSegs, alignmentElRef: this.rootElRef, defaultContent: renderMoreLinkInner, extraDateSpan: props.extraDateSpan, dateProfile: props.dateProfile, todayRange: props.todayRange, popoverContent: function () { return renderPlainFgSegs(props.hiddenSegs, props); } }, function (rootElRef, classNames, innerElRef, innerContent, handleClick) { return (createElement("a", { ref: function (el) { + setRef(rootElRef, el); + setRef(_this.rootElRef, el); + }, className: ['fc-timegrid-more-link'].concat(classNames).join(' '), style: { top: props.top, bottom: props.bottom }, onClick: handleClick }, + createElement("div", { ref: innerElRef, className: "fc-timegrid-more-link-inner fc-sticky" }, innerContent))); })); + }; + return TimeColMoreLink; + }(BaseComponent)); + function renderMoreLinkInner(props) { + return props.shortText; + } + + // segInputs assumed sorted + function buildPositioning(segInputs, strictOrder, maxStackCnt) { + var hierarchy = new SegHierarchy(); + if (strictOrder != null) { + hierarchy.strictOrder = strictOrder; + } + if (maxStackCnt != null) { + hierarchy.maxStackCnt = maxStackCnt; + } + var hiddenEntries = hierarchy.addSegs(segInputs); + var hiddenGroups = groupIntersectingEntries(hiddenEntries); + var web = buildWeb(hierarchy); + web = stretchWeb(web, 1); // all levelCoords/thickness will have 0.0-1.0 + var segRects = webToRects(web); + return { segRects: segRects, hiddenGroups: hiddenGroups }; + } + function buildWeb(hierarchy) { + var entriesByLevel = hierarchy.entriesByLevel; + var buildNode = cacheable(function (level, lateral) { return level + ':' + lateral; }, function (level, lateral) { + var siblingRange = findNextLevelSegs(hierarchy, level, lateral); + var nextLevelRes = buildNodes(siblingRange, buildNode); + var entry = entriesByLevel[level][lateral]; + return [ + __assign(__assign({}, entry), { nextLevelNodes: nextLevelRes[0] }), + entry.thickness + nextLevelRes[1], // the pressure builds + ]; + }); + return buildNodes(entriesByLevel.length + ? { level: 0, lateralStart: 0, lateralEnd: entriesByLevel[0].length } + : null, buildNode)[0]; + } + function buildNodes(siblingRange, buildNode) { + if (!siblingRange) { + return [[], 0]; + } + var level = siblingRange.level, lateralStart = siblingRange.lateralStart, lateralEnd = siblingRange.lateralEnd; + var lateral = lateralStart; + var pairs = []; + while (lateral < lateralEnd) { + pairs.push(buildNode(level, lateral)); + lateral += 1; + } + pairs.sort(cmpDescPressures); + return [ + pairs.map(extractNode), + pairs[0][1], // first item's pressure + ]; + } + function cmpDescPressures(a, b) { + return b[1] - a[1]; + } + function extractNode(a) { + return a[0]; + } + function findNextLevelSegs(hierarchy, subjectLevel, subjectLateral) { + var levelCoords = hierarchy.levelCoords, entriesByLevel = hierarchy.entriesByLevel; + var subjectEntry = entriesByLevel[subjectLevel][subjectLateral]; + var afterSubject = levelCoords[subjectLevel] + subjectEntry.thickness; + var levelCnt = levelCoords.length; + var level = subjectLevel; + // skip past levels that are too high up + for (; level < levelCnt && levelCoords[level] < afterSubject; level += 1) + ; // do nothing + for (; level < levelCnt; level += 1) { + var entries = entriesByLevel[level]; + var entry = void 0; + var searchIndex = binarySearch(entries, subjectEntry.span.start, getEntrySpanEnd); + var lateralStart = searchIndex[0] + searchIndex[1]; // if exact match (which doesn't collide), go to next one + var lateralEnd = lateralStart; + while ( // loop through entries that horizontally intersect + (entry = entries[lateralEnd]) && // but not past the whole seg list + entry.span.start < subjectEntry.span.end) { + lateralEnd += 1; + } + if (lateralStart < lateralEnd) { + return { level: level, lateralStart: lateralStart, lateralEnd: lateralEnd }; + } + } + return null; + } + function stretchWeb(topLevelNodes, totalThickness) { + var stretchNode = cacheable(function (node, startCoord, prevThickness) { return buildEntryKey(node); }, function (node, startCoord, prevThickness) { + var nextLevelNodes = node.nextLevelNodes, thickness = node.thickness; + var allThickness = thickness + prevThickness; + var thicknessFraction = thickness / allThickness; + var endCoord; + var newChildren = []; + if (!nextLevelNodes.length) { + endCoord = totalThickness; + } + else { + for (var _i = 0, nextLevelNodes_1 = nextLevelNodes; _i < nextLevelNodes_1.length; _i++) { + var childNode = nextLevelNodes_1[_i]; + if (endCoord === undefined) { + var res = stretchNode(childNode, startCoord, allThickness); + endCoord = res[0]; + newChildren.push(res[1]); + } + else { + var res = stretchNode(childNode, endCoord, 0); + newChildren.push(res[1]); + } + } + } + var newThickness = (endCoord - startCoord) * thicknessFraction; + return [endCoord - newThickness, __assign(__assign({}, node), { thickness: newThickness, nextLevelNodes: newChildren })]; + }); + return topLevelNodes.map(function (node) { return stretchNode(node, 0, 0)[1]; }); + } + // not sorted in any particular order + function webToRects(topLevelNodes) { + var rects = []; + var processNode = cacheable(function (node, levelCoord, stackDepth) { return buildEntryKey(node); }, function (node, levelCoord, stackDepth) { + var rect = __assign(__assign({}, node), { levelCoord: levelCoord, + stackDepth: stackDepth, stackForward: 0 }); + rects.push(rect); + return (rect.stackForward = processNodes(node.nextLevelNodes, levelCoord + node.thickness, stackDepth + 1) + 1); + }); + function processNodes(nodes, levelCoord, stackDepth) { + var stackForward = 0; + for (var _i = 0, nodes_1 = nodes; _i < nodes_1.length; _i++) { + var node = nodes_1[_i]; + stackForward = Math.max(processNode(node, levelCoord, stackDepth), stackForward); + } + return stackForward; + } + processNodes(topLevelNodes, 0, 0); + return rects; // TODO: sort rects by levelCoord to be consistent with toRects? + } + // TODO: move to general util + function cacheable(keyFunc, workFunc) { + var cache = {}; + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + var key = keyFunc.apply(void 0, args); + return (key in cache) + ? cache[key] + : (cache[key] = workFunc.apply(void 0, args)); + }; + } + + function computeSegVCoords(segs, colDate, slatCoords, eventMinHeight) { + if (slatCoords === void 0) { slatCoords = null; } + if (eventMinHeight === void 0) { eventMinHeight = 0; } + var vcoords = []; + if (slatCoords) { + for (var i = 0; i < segs.length; i += 1) { + var seg = segs[i]; + var spanStart = slatCoords.computeDateTop(seg.start, colDate); + var spanEnd = Math.max(spanStart + (eventMinHeight || 0), // :( + slatCoords.computeDateTop(seg.end, colDate)); + vcoords.push({ + start: Math.round(spanStart), + end: Math.round(spanEnd), // + }); + } + } + return vcoords; + } + function computeFgSegPlacements(segs, segVCoords, // might not have for every seg + eventOrderStrict, eventMaxStack) { + var segInputs = []; + var dumbSegs = []; // segs without coords + for (var i = 0; i < segs.length; i += 1) { + var vcoords = segVCoords[i]; + if (vcoords) { + segInputs.push({ + index: i, + thickness: 1, + span: vcoords, + }); + } + else { + dumbSegs.push(segs[i]); + } + } + var _a = buildPositioning(segInputs, eventOrderStrict, eventMaxStack), segRects = _a.segRects, hiddenGroups = _a.hiddenGroups; + var segPlacements = []; + for (var _i = 0, segRects_1 = segRects; _i < segRects_1.length; _i++) { + var segRect = segRects_1[_i]; + segPlacements.push({ + seg: segs[segRect.index], + rect: segRect, + }); + } + for (var _b = 0, dumbSegs_1 = dumbSegs; _b < dumbSegs_1.length; _b++) { + var dumbSeg = dumbSegs_1[_b]; + segPlacements.push({ seg: dumbSeg, rect: null }); + } + return { segPlacements: segPlacements, hiddenGroups: hiddenGroups }; + } + + var DEFAULT_TIME_FORMAT$1 = createFormatter({ + hour: 'numeric', + minute: '2-digit', + meridiem: false, + }); + var TimeColEvent = /** @class */ (function (_super) { + __extends(TimeColEvent, _super); + function TimeColEvent() { + return _super !== null && _super.apply(this, arguments) || this; + } + TimeColEvent.prototype.render = function () { + var classNames = [ + 'fc-timegrid-event', + 'fc-v-event', + ]; + if (this.props.isShort) { + classNames.push('fc-timegrid-event-short'); + } + return (createElement(StandardEvent, __assign({}, this.props, { defaultTimeFormat: DEFAULT_TIME_FORMAT$1, extraClassNames: classNames }))); + }; + return TimeColEvent; + }(BaseComponent)); + + var TimeColMisc = /** @class */ (function (_super) { + __extends(TimeColMisc, _super); + function TimeColMisc() { + return _super !== null && _super.apply(this, arguments) || this; + } + TimeColMisc.prototype.render = function () { + var props = this.props; + return (createElement(DayCellContent, { date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, extraHookProps: props.extraHookProps }, function (innerElRef, innerContent) { return (innerContent && + createElement("div", { className: "fc-timegrid-col-misc", ref: innerElRef }, innerContent)); })); + }; + return TimeColMisc; + }(BaseComponent)); + + var TimeCol = /** @class */ (function (_super) { + __extends(TimeCol, _super); + function TimeCol() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.sortEventSegs = memoize(sortEventSegs); + return _this; + } + // TODO: memoize event-placement? + TimeCol.prototype.render = function () { + var _this = this; + var _a = this, props = _a.props, context = _a.context; + var isSelectMirror = context.options.selectMirror; + var mirrorSegs = (props.eventDrag && props.eventDrag.segs) || + (props.eventResize && props.eventResize.segs) || + (isSelectMirror && props.dateSelectionSegs) || + []; + var interactionAffectedInstances = // TODO: messy way to compute this + (props.eventDrag && props.eventDrag.affectedInstances) || + (props.eventResize && props.eventResize.affectedInstances) || + {}; + var sortedFgSegs = this.sortEventSegs(props.fgEventSegs, context.options.eventOrder); + return (createElement(DayCellRoot, { elRef: props.elRef, date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, extraHookProps: props.extraHookProps }, function (rootElRef, classNames, dataAttrs) { return (createElement("td", __assign({ ref: rootElRef, className: ['fc-timegrid-col'].concat(classNames, props.extraClassNames || []).join(' ') }, dataAttrs, props.extraDataAttrs), + createElement("div", { className: "fc-timegrid-col-frame" }, + createElement("div", { className: "fc-timegrid-col-bg" }, + _this.renderFillSegs(props.businessHourSegs, 'non-business'), + _this.renderFillSegs(props.bgEventSegs, 'bg-event'), + _this.renderFillSegs(props.dateSelectionSegs, 'highlight')), + createElement("div", { className: "fc-timegrid-col-events" }, _this.renderFgSegs(sortedFgSegs, interactionAffectedInstances, false, false, false)), + createElement("div", { className: "fc-timegrid-col-events" }, _this.renderFgSegs(mirrorSegs, {}, Boolean(props.eventDrag), Boolean(props.eventResize), Boolean(isSelectMirror))), + createElement("div", { className: "fc-timegrid-now-indicator-container" }, _this.renderNowIndicator(props.nowIndicatorSegs)), + createElement(TimeColMisc, { date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, extraHookProps: props.extraHookProps })))); })); + }; + TimeCol.prototype.renderFgSegs = function (sortedFgSegs, segIsInvisible, isDragging, isResizing, isDateSelecting) { + var props = this.props; + if (props.forPrint) { + return renderPlainFgSegs(sortedFgSegs, props); + } + return this.renderPositionedFgSegs(sortedFgSegs, segIsInvisible, isDragging, isResizing, isDateSelecting); + }; + TimeCol.prototype.renderPositionedFgSegs = function (segs, // if not mirror, needs to be sorted + segIsInvisible, isDragging, isResizing, isDateSelecting) { + var _this = this; + var _a = this.context.options, eventMaxStack = _a.eventMaxStack, eventShortHeight = _a.eventShortHeight, eventOrderStrict = _a.eventOrderStrict, eventMinHeight = _a.eventMinHeight; + var _b = this.props, date = _b.date, slatCoords = _b.slatCoords, eventSelection = _b.eventSelection, todayRange = _b.todayRange, nowDate = _b.nowDate; + var isMirror = isDragging || isResizing || isDateSelecting; + var segVCoords = computeSegVCoords(segs, date, slatCoords, eventMinHeight); + var _c = computeFgSegPlacements(segs, segVCoords, eventOrderStrict, eventMaxStack), segPlacements = _c.segPlacements, hiddenGroups = _c.hiddenGroups; + return (createElement(Fragment, null, + this.renderHiddenGroups(hiddenGroups, segs), + segPlacements.map(function (segPlacement) { + var seg = segPlacement.seg, rect = segPlacement.rect; + var instanceId = seg.eventRange.instance.instanceId; + var isVisible = isMirror || Boolean(!segIsInvisible[instanceId] && rect); + var vStyle = computeSegVStyle(rect && rect.span); + var hStyle = (!isMirror && rect) ? _this.computeSegHStyle(rect) : { left: 0, right: 0 }; + var isInset = Boolean(rect) && rect.stackForward > 0; + var isShort = Boolean(rect) && (rect.span.end - rect.span.start) < eventShortHeight; // look at other places for this problem + return (createElement("div", { className: 'fc-timegrid-event-harness' + + (isInset ? ' fc-timegrid-event-harness-inset' : ''), key: instanceId, style: __assign(__assign({ visibility: isVisible ? '' : 'hidden' }, vStyle), hStyle) }, + createElement(TimeColEvent, __assign({ seg: seg, isDragging: isDragging, isResizing: isResizing, isDateSelecting: isDateSelecting, isSelected: instanceId === eventSelection, isShort: isShort }, getSegMeta(seg, todayRange, nowDate))))); + }))); + }; + // will already have eventMinHeight applied because segInputs already had it + TimeCol.prototype.renderHiddenGroups = function (hiddenGroups, segs) { + var _a = this.props, extraDateSpan = _a.extraDateSpan, dateProfile = _a.dateProfile, todayRange = _a.todayRange, nowDate = _a.nowDate, eventSelection = _a.eventSelection, eventDrag = _a.eventDrag, eventResize = _a.eventResize; + return (createElement(Fragment, null, hiddenGroups.map(function (hiddenGroup) { + var positionCss = computeSegVStyle(hiddenGroup.span); + var hiddenSegs = compileSegsFromEntries(hiddenGroup.entries, segs); + return (createElement(TimeColMoreLink, { key: buildIsoString(computeEarliestSegStart(hiddenSegs)), hiddenSegs: hiddenSegs, top: positionCss.top, bottom: positionCss.bottom, extraDateSpan: extraDateSpan, dateProfile: dateProfile, todayRange: todayRange, nowDate: nowDate, eventSelection: eventSelection, eventDrag: eventDrag, eventResize: eventResize })); + }))); + }; + TimeCol.prototype.renderFillSegs = function (segs, fillType) { + var _a = this, props = _a.props, context = _a.context; + var segVCoords = computeSegVCoords(segs, props.date, props.slatCoords, context.options.eventMinHeight); // don't assume all populated + var children = segVCoords.map(function (vcoords, i) { + var seg = segs[i]; + return (createElement("div", { key: buildEventRangeKey(seg.eventRange), className: "fc-timegrid-bg-harness", style: computeSegVStyle(vcoords) }, fillType === 'bg-event' ? + createElement(BgEvent, __assign({ seg: seg }, getSegMeta(seg, props.todayRange, props.nowDate))) : + renderFill(fillType))); + }); + return createElement(Fragment, null, children); + }; + TimeCol.prototype.renderNowIndicator = function (segs) { + var _a = this.props, slatCoords = _a.slatCoords, date = _a.date; + if (!slatCoords) { + return null; + } + return segs.map(function (seg, i) { return (createElement(NowIndicatorRoot, { isAxis: false, date: date, + // key doesn't matter. will only ever be one + key: i }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("div", { ref: rootElRef, className: ['fc-timegrid-now-indicator-line'].concat(classNames).join(' '), style: { top: slatCoords.computeDateTop(seg.start, date) } }, innerContent)); })); }); + }; + TimeCol.prototype.computeSegHStyle = function (segHCoords) { + var _a = this.context, isRtl = _a.isRtl, options = _a.options; + var shouldOverlap = options.slotEventOverlap; + var nearCoord = segHCoords.levelCoord; // the left side if LTR. the right side if RTL. floating-point + var farCoord = segHCoords.levelCoord + segHCoords.thickness; // the right side if LTR. the left side if RTL. floating-point + var left; // amount of space from left edge, a fraction of the total width + var right; // amount of space from right edge, a fraction of the total width + if (shouldOverlap) { + // double the width, but don't go beyond the maximum forward coordinate (1.0) + farCoord = Math.min(1, nearCoord + (farCoord - nearCoord) * 2); + } + if (isRtl) { + left = 1 - farCoord; + right = nearCoord; + } + else { + left = nearCoord; + right = 1 - farCoord; + } + var props = { + zIndex: segHCoords.stackDepth + 1, + left: left * 100 + '%', + right: right * 100 + '%', + }; + if (shouldOverlap && !segHCoords.stackForward) { + // add padding to the edge so that forward stacked events don't cover the resizer's icon + props[isRtl ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width + } + return props; + }; + return TimeCol; + }(BaseComponent)); + function renderPlainFgSegs(sortedFgSegs, _a) { + var todayRange = _a.todayRange, nowDate = _a.nowDate, eventSelection = _a.eventSelection, eventDrag = _a.eventDrag, eventResize = _a.eventResize; + var hiddenInstances = (eventDrag ? eventDrag.affectedInstances : null) || + (eventResize ? eventResize.affectedInstances : null) || + {}; + return (createElement(Fragment, null, sortedFgSegs.map(function (seg) { + var instanceId = seg.eventRange.instance.instanceId; + return (createElement("div", { key: instanceId, style: { visibility: hiddenInstances[instanceId] ? 'hidden' : '' } }, + createElement(TimeColEvent, __assign({ seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: instanceId === eventSelection, isShort: false }, getSegMeta(seg, todayRange, nowDate))))); + }))); + } + function computeSegVStyle(segVCoords) { + if (!segVCoords) { + return { top: '', bottom: '' }; + } + return { + top: segVCoords.start, + bottom: -segVCoords.end, + }; + } + function compileSegsFromEntries(segEntries, allSegs) { + return segEntries.map(function (segEntry) { return allSegs[segEntry.index]; }); + } + + var TimeColsContent = /** @class */ (function (_super) { + __extends(TimeColsContent, _super); + function TimeColsContent() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.splitFgEventSegs = memoize(splitSegsByCol); + _this.splitBgEventSegs = memoize(splitSegsByCol); + _this.splitBusinessHourSegs = memoize(splitSegsByCol); + _this.splitNowIndicatorSegs = memoize(splitSegsByCol); + _this.splitDateSelectionSegs = memoize(splitSegsByCol); + _this.splitEventDrag = memoize(splitInteractionByCol); + _this.splitEventResize = memoize(splitInteractionByCol); + _this.rootElRef = createRef(); + _this.cellElRefs = new RefMap(); + return _this; + } + TimeColsContent.prototype.render = function () { + var _this = this; + var _a = this, props = _a.props, context = _a.context; + var nowIndicatorTop = context.options.nowIndicator && + props.slatCoords && + props.slatCoords.safeComputeTop(props.nowDate); // might return void + var colCnt = props.cells.length; + var fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, colCnt); + var bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, colCnt); + var businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, colCnt); + var nowIndicatorSegsByRow = this.splitNowIndicatorSegs(props.nowIndicatorSegs, colCnt); + var dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, colCnt); + var eventDragByRow = this.splitEventDrag(props.eventDrag, colCnt); + var eventResizeByRow = this.splitEventResize(props.eventResize, colCnt); + return (createElement("div", { className: "fc-timegrid-cols", ref: this.rootElRef }, + createElement("table", { style: { + minWidth: props.tableMinWidth, + width: props.clientWidth, + } }, + props.tableColGroupNode, + createElement("tbody", null, + createElement("tr", null, + props.axis && (createElement("td", { className: "fc-timegrid-col fc-timegrid-axis" }, + createElement("div", { className: "fc-timegrid-col-frame" }, + createElement("div", { className: "fc-timegrid-now-indicator-container" }, typeof nowIndicatorTop === 'number' && (createElement(NowIndicatorRoot, { isAxis: true, date: props.nowDate }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("div", { ref: rootElRef, className: ['fc-timegrid-now-indicator-arrow'].concat(classNames).join(' '), style: { top: nowIndicatorTop } }, innerContent)); })))))), + props.cells.map(function (cell, i) { return (createElement(TimeCol, { key: cell.key, elRef: _this.cellElRefs.createRef(cell.key), dateProfile: props.dateProfile, date: cell.date, nowDate: props.nowDate, todayRange: props.todayRange, extraHookProps: cell.extraHookProps, extraDataAttrs: cell.extraDataAttrs, extraClassNames: cell.extraClassNames, extraDateSpan: cell.extraDateSpan, fgEventSegs: fgEventSegsByRow[i], bgEventSegs: bgEventSegsByRow[i], businessHourSegs: businessHourSegsByRow[i], nowIndicatorSegs: nowIndicatorSegsByRow[i], dateSelectionSegs: dateSelectionSegsByRow[i], eventDrag: eventDragByRow[i], eventResize: eventResizeByRow[i], slatCoords: props.slatCoords, eventSelection: props.eventSelection, forPrint: props.forPrint })); })))))); + }; + TimeColsContent.prototype.componentDidMount = function () { + this.updateCoords(); + }; + TimeColsContent.prototype.componentDidUpdate = function () { + this.updateCoords(); + }; + TimeColsContent.prototype.updateCoords = function () { + var props = this.props; + if (props.onColCoords && + props.clientWidth !== null // means sizing has stabilized + ) { + props.onColCoords(new PositionCache(this.rootElRef.current, collectCellEls(this.cellElRefs.currentMap, props.cells), true, // horizontal + false)); + } + }; + return TimeColsContent; + }(BaseComponent)); + function collectCellEls(elMap, cells) { + return cells.map(function (cell) { return elMap[cell.key]; }); + } + + /* A component that renders one or more columns of vertical time slots + ----------------------------------------------------------------------------------------------------------------------*/ + var TimeCols = /** @class */ (function (_super) { + __extends(TimeCols, _super); + function TimeCols() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.processSlotOptions = memoize(processSlotOptions); + _this.state = { + slatCoords: null, + }; + _this.handleRootEl = function (el) { + if (el) { + _this.context.registerInteractiveComponent(_this, { + el: el, + isHitComboAllowed: _this.props.isHitComboAllowed, + }); + } + else { + _this.context.unregisterInteractiveComponent(_this); + } + }; + _this.handleScrollRequest = function (request) { + var onScrollTopRequest = _this.props.onScrollTopRequest; + var slatCoords = _this.state.slatCoords; + if (onScrollTopRequest && slatCoords) { + if (request.time) { + var top_1 = slatCoords.computeTimeTop(request.time); + top_1 = Math.ceil(top_1); // zoom can give weird floating-point values. rather scroll a little bit further + if (top_1) { + top_1 += 1; // to overcome top border that slots beyond the first have. looks better + } + onScrollTopRequest(top_1); + } + return true; + } + return false; + }; + _this.handleColCoords = function (colCoords) { + _this.colCoords = colCoords; + }; + _this.handleSlatCoords = function (slatCoords) { + _this.setState({ slatCoords: slatCoords }); + if (_this.props.onSlatCoords) { + _this.props.onSlatCoords(slatCoords); + } + }; + return _this; + } + TimeCols.prototype.render = function () { + var _a = this, props = _a.props, state = _a.state; + return (createElement("div", { className: "fc-timegrid-body", 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(TimeColsSlats, { axis: props.axis, dateProfile: props.dateProfile, slatMetas: props.slatMetas, clientWidth: props.clientWidth, minHeight: props.expandRows ? props.clientHeight : '', tableMinWidth: props.tableMinWidth, tableColGroupNode: props.axis ? props.tableColGroupNode : null /* axis depends on the colgroup's shrinking */, onCoords: this.handleSlatCoords }), + createElement(TimeColsContent, { cells: props.cells, axis: props.axis, dateProfile: props.dateProfile, businessHourSegs: props.businessHourSegs, bgEventSegs: props.bgEventSegs, fgEventSegs: props.fgEventSegs, dateSelectionSegs: props.dateSelectionSegs, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, todayRange: props.todayRange, nowDate: props.nowDate, nowIndicatorSegs: props.nowIndicatorSegs, clientWidth: props.clientWidth, tableMinWidth: props.tableMinWidth, tableColGroupNode: props.tableColGroupNode, slatCoords: state.slatCoords, onColCoords: this.handleColCoords, forPrint: props.forPrint }))); + }; + TimeCols.prototype.componentDidMount = function () { + this.scrollResponder = this.context.createScrollResponder(this.handleScrollRequest); + }; + TimeCols.prototype.componentDidUpdate = function (prevProps) { + this.scrollResponder.update(prevProps.dateProfile !== this.props.dateProfile); + }; + TimeCols.prototype.componentWillUnmount = function () { + this.scrollResponder.detach(); + }; + TimeCols.prototype.queryHit = function (positionLeft, positionTop) { + var _a = this.context, dateEnv = _a.dateEnv, options = _a.options; + var colCoords = this.colCoords; + var dateProfile = this.props.dateProfile; + var slatCoords = this.state.slatCoords; + var _b = this.processSlotOptions(this.props.slotDuration, options.snapDuration), snapDuration = _b.snapDuration, snapsPerSlot = _b.snapsPerSlot; + var colIndex = colCoords.leftToIndex(positionLeft); + var slatIndex = slatCoords.positions.topToIndex(positionTop); + if (colIndex != null && slatIndex != null) { + var cell = this.props.cells[colIndex]; + var slatTop = slatCoords.positions.tops[slatIndex]; + var slatHeight = slatCoords.positions.getHeight(slatIndex); + var partial = (positionTop - slatTop) / slatHeight; // floating point number between 0 and 1 + var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat + var snapIndex = slatIndex * snapsPerSlot + localSnapIndex; + var dayDate = this.props.cells[colIndex].date; + var time = addDurations(dateProfile.slotMinTime, multiplyDuration(snapDuration, snapIndex)); + var start = dateEnv.add(dayDate, time); + var end = dateEnv.add(start, snapDuration); + return { + dateProfile: dateProfile, + dateSpan: __assign({ range: { start: start, end: end }, allDay: false }, cell.extraDateSpan), + dayEl: colCoords.els[colIndex], + rect: { + left: colCoords.lefts[colIndex], + right: colCoords.rights[colIndex], + top: slatTop, + bottom: slatTop + slatHeight, + }, + layer: 0, + }; + } + return null; + }; + return TimeCols; + }(DateComponent)); + function processSlotOptions(slotDuration, snapDurationOverride) { + var snapDuration = snapDurationOverride || slotDuration; + var snapsPerSlot = wholeDivideDurations(slotDuration, snapDuration); + if (snapsPerSlot === null) { + snapDuration = slotDuration; + snapsPerSlot = 1; + // TODO: say warning? + } + return { snapDuration: snapDuration, snapsPerSlot: snapsPerSlot }; + } + + var DayTimeColsSlicer = /** @class */ (function (_super) { + __extends(DayTimeColsSlicer, _super); + function DayTimeColsSlicer() { + return _super !== null && _super.apply(this, arguments) || this; + } + DayTimeColsSlicer.prototype.sliceRange = function (range, dayRanges) { + var segs = []; + for (var col = 0; col < dayRanges.length; col += 1) { + var segRange = intersectRanges(range, dayRanges[col]); + if (segRange) { + segs.push({ + start: segRange.start, + end: segRange.end, + isStart: segRange.start.valueOf() === range.start.valueOf(), + isEnd: segRange.end.valueOf() === range.end.valueOf(), + col: col, + }); + } + } + return segs; + }; + return DayTimeColsSlicer; + }(Slicer)); + + var DayTimeCols = /** @class */ (function (_super) { + __extends(DayTimeCols, _super); + function DayTimeCols() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.buildDayRanges = memoize(buildDayRanges); + _this.slicer = new DayTimeColsSlicer(); + _this.timeColsRef = createRef(); + return _this; + } + DayTimeCols.prototype.render = function () { + var _this = this; + var _a = this, props = _a.props, context = _a.context; + var dateProfile = props.dateProfile, dayTableModel = props.dayTableModel; + var isNowIndicator = context.options.nowIndicator; + var dayRanges = this.buildDayRanges(dayTableModel, dateProfile, context.dateEnv); + // give it the first row of cells + // TODO: would move this further down hierarchy, but sliceNowDate needs it + return (createElement(NowTimer, { unit: isNowIndicator ? 'minute' : 'day' }, function (nowDate, todayRange) { return (createElement(TimeCols, __assign({ ref: _this.timeColsRef }, _this.slicer.sliceProps(props, dateProfile, null, context, dayRanges), { forPrint: props.forPrint, axis: props.axis, dateProfile: dateProfile, slatMetas: props.slatMetas, slotDuration: props.slotDuration, cells: dayTableModel.cells[0], tableColGroupNode: props.tableColGroupNode, tableMinWidth: props.tableMinWidth, clientWidth: props.clientWidth, clientHeight: props.clientHeight, expandRows: props.expandRows, nowDate: nowDate, nowIndicatorSegs: isNowIndicator && _this.slicer.sliceNowDate(nowDate, context, dayRanges), todayRange: todayRange, onScrollTopRequest: props.onScrollTopRequest, onSlatCoords: props.onSlatCoords }))); })); + }; + return DayTimeCols; + }(DateComponent)); + function buildDayRanges(dayTableModel, dateProfile, dateEnv) { + var ranges = []; + for (var _i = 0, _a = dayTableModel.headerDates; _i < _a.length; _i++) { + var date = _a[_i]; + ranges.push({ + start: dateEnv.add(date, dateProfile.slotMinTime), + end: dateEnv.add(date, dateProfile.slotMaxTime), + }); + } + return ranges; + } + + // potential nice values for the slot-duration and interval-duration + // from largest to smallest + var STOCK_SUB_DURATIONS = [ + { hours: 1 }, + { minutes: 30 }, + { minutes: 15 }, + { seconds: 30 }, + { seconds: 15 }, + ]; + function buildSlatMetas(slotMinTime, slotMaxTime, explicitLabelInterval, slotDuration, dateEnv) { + var dayStart = new Date(0); + var slatTime = slotMinTime; + var slatIterator = createDuration(0); + var labelInterval = explicitLabelInterval || computeLabelInterval(slotDuration); + var metas = []; + while (asRoughMs(slatTime) < asRoughMs(slotMaxTime)) { + var date = dateEnv.add(dayStart, slatTime); + var isLabeled = wholeDivideDurations(slatIterator, labelInterval) !== null; + metas.push({ + date: date, + time: slatTime, + key: date.toISOString(), + isoTimeStr: formatIsoTimeString(date), + isLabeled: isLabeled, + }); + slatTime = addDurations(slatTime, slotDuration); + slatIterator = addDurations(slatIterator, slotDuration); + } + return metas; + } + // Computes an automatic value for slotLabelInterval + function computeLabelInterval(slotDuration) { + var i; + var labelInterval; + var slotsPerLabel; + // find the smallest stock label interval that results in more than one slots-per-label + for (i = STOCK_SUB_DURATIONS.length - 1; i >= 0; i -= 1) { + labelInterval = createDuration(STOCK_SUB_DURATIONS[i]); + slotsPerLabel = wholeDivideDurations(labelInterval, slotDuration); + if (slotsPerLabel !== null && slotsPerLabel > 1) { + return labelInterval; + } + } + return slotDuration; // fall back + } + + var DayTimeColsView = /** @class */ (function (_super) { + __extends(DayTimeColsView, _super); + function DayTimeColsView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.buildTimeColsModel = memoize(buildTimeColsModel); + _this.buildSlatMetas = memoize(buildSlatMetas); + return _this; + } + DayTimeColsView.prototype.render = function () { + var _this = this; + var _a = this.context, options = _a.options, dateEnv = _a.dateEnv, dateProfileGenerator = _a.dateProfileGenerator; + var props = this.props; + var dateProfile = props.dateProfile; + var dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator); + var splitProps = this.allDaySplitter.splitProps(props); + var slatMetas = this.buildSlatMetas(dateProfile.slotMinTime, dateProfile.slotMaxTime, options.slotLabelInterval, options.slotDuration, dateEnv); + var dayMinWidth = options.dayMinWidth; + var hasAttachedAxis = !dayMinWidth; + var hasDetachedAxis = dayMinWidth; + var headerContent = options.dayHeaders && (createElement(DayHeader, { dates: dayTableModel.headerDates, dateProfile: dateProfile, datesRepDistinctDays: true, renderIntro: hasAttachedAxis ? this.renderHeadAxis : null })); + var allDayContent = (options.allDaySlot !== false) && (function (contentArg) { return (createElement(DayTable, __assign({}, splitProps.allDay, { dateProfile: dateProfile, dayTableModel: dayTableModel, nextDayThreshold: options.nextDayThreshold, tableMinWidth: contentArg.tableMinWidth, colGroupNode: contentArg.tableColGroupNode, renderRowIntro: hasAttachedAxis ? _this.renderTableRowAxis : null, showWeekNumbers: false, expandRows: false, headerAlignElRef: _this.headerElRef, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, forPrint: props.forPrint }, _this.getAllDayMaxEventProps()))); }); + var timeGridContent = function (contentArg) { return (createElement(DayTimeCols, __assign({}, splitProps.timed, { dayTableModel: dayTableModel, dateProfile: dateProfile, axis: hasAttachedAxis, slotDuration: options.slotDuration, slatMetas: slatMetas, forPrint: props.forPrint, tableColGroupNode: contentArg.tableColGroupNode, tableMinWidth: contentArg.tableMinWidth, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, onSlatCoords: _this.handleSlatCoords, expandRows: contentArg.expandRows, onScrollTopRequest: _this.handleScrollTopRequest }))); }; + return hasDetachedAxis + ? this.renderHScrollLayout(headerContent, allDayContent, timeGridContent, dayTableModel.colCnt, dayMinWidth, slatMetas, this.state.slatCoords) + : this.renderSimpleLayout(headerContent, allDayContent, timeGridContent); + }; + return DayTimeColsView; + }(TimeColsView)); + function buildTimeColsModel(dateProfile, dateProfileGenerator) { + var daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator); + return new DayTableModel(daySeries, false); + } + + var OPTION_REFINERS$2 = { + allDaySlot: Boolean, + }; + + var timeGridPlugin = createPlugin({ + initialView: 'timeGridWeek', + optionRefiners: OPTION_REFINERS$2, + views: { + timeGrid: { + component: DayTimeColsView, + usesMinMaxTime: true, + allDaySlot: true, + slotDuration: '00:30:00', + slotEventOverlap: true, // a bad name. confused with overlap/constraint system + }, + timeGridDay: { + type: 'timeGrid', + duration: { days: 1 }, + }, + timeGridWeek: { + type: 'timeGrid', + duration: { weeks: 1 }, + }, + }, + }); + + var ListViewHeaderRow = /** @class */ (function (_super) { + __extends(ListViewHeaderRow, _super); + function ListViewHeaderRow() { + return _super !== null && _super.apply(this, arguments) || this; + } + ListViewHeaderRow.prototype.render = function () { + var _a = this.props, dayDate = _a.dayDate, todayRange = _a.todayRange; + var _b = this.context, theme = _b.theme, dateEnv = _b.dateEnv, options = _b.options, viewApi = _b.viewApi; + var dayMeta = getDateMeta(dayDate, todayRange); + // will ever be falsy? + var text = options.listDayFormat ? dateEnv.format(dayDate, options.listDayFormat) : ''; + // will ever be falsy? also, BAD NAME "alt" + var sideText = options.listDaySideFormat ? dateEnv.format(dayDate, options.listDaySideFormat) : ''; + var navLinkData = options.navLinks + ? buildNavLinkData(dayDate) + : null; + var hookProps = __assign({ date: dateEnv.toDate(dayDate), view: viewApi, text: text, + sideText: sideText, + navLinkData: navLinkData }, dayMeta); + var classNames = ['fc-list-day'].concat(getDayClassNames(dayMeta, theme)); + // TODO: make a reusable HOC for dayHeader (used in daygrid/timegrid too) + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.dayHeaderClassNames, content: options.dayHeaderContent, defaultContent: renderInnerContent, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("tr", { ref: rootElRef, className: classNames.concat(customClassNames).join(' '), "data-date": formatDayString(dayDate) }, + createElement("th", { colSpan: 3 }, + createElement("div", { className: 'fc-list-day-cushion ' + theme.getClass('tableCellShaded'), ref: innerElRef }, innerContent)))); })); + }; + return ListViewHeaderRow; + }(BaseComponent)); + function renderInnerContent(props) { + var navLinkAttrs = props.navLinkData // is there a type for this? + ? { 'data-navlink': props.navLinkData, tabIndex: 0 } + : {}; + return (createElement(Fragment, null, + props.text && (createElement("a", __assign({ className: "fc-list-day-text" }, navLinkAttrs), props.text)), + props.sideText && (createElement("a", __assign({ className: "fc-list-day-side-text" }, navLinkAttrs), props.sideText)))); + } + + var DEFAULT_TIME_FORMAT = createFormatter({ + hour: 'numeric', + minute: '2-digit', + meridiem: 'short', + }); + var ListViewEventRow = /** @class */ (function (_super) { + __extends(ListViewEventRow, _super); + function ListViewEventRow() { + return _super !== null && _super.apply(this, arguments) || this; + } + ListViewEventRow.prototype.render = function () { + var _a = this, props = _a.props, context = _a.context; + var seg = props.seg; + var timeFormat = context.options.eventTimeFormat || DEFAULT_TIME_FORMAT; + return (createElement(EventRoot, { seg: seg, timeText: "" // BAD. because of all-day content + , disableDragging: true, disableResizing: true, defaultContent: renderEventInnerContent, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday, isSelected: props.isSelected, isDragging: props.isDragging, isResizing: props.isResizing, isDateSelecting: props.isDateSelecting }, function (rootElRef, classNames, innerElRef, innerContent, hookProps) { return (createElement("tr", { className: ['fc-list-event', hookProps.event.url ? 'fc-event-forced-url' : ''].concat(classNames).join(' '), ref: rootElRef }, + buildTimeContent(seg, timeFormat, context), + createElement("td", { className: "fc-list-event-graphic" }, + createElement("span", { className: "fc-list-event-dot", style: { borderColor: hookProps.borderColor || hookProps.backgroundColor } })), + createElement("td", { className: "fc-list-event-title", ref: innerElRef }, innerContent))); })); + }; + return ListViewEventRow; + }(BaseComponent)); + function renderEventInnerContent(props) { + var event = props.event; + var url = event.url; + var anchorAttrs = url ? { href: url } : {}; + return (createElement("a", __assign({}, anchorAttrs), event.title)); + } + function buildTimeContent(seg, timeFormat, context) { + var options = context.options; + if (options.displayEventTime !== false) { + var eventDef = seg.eventRange.def; + var eventInstance = seg.eventRange.instance; + var doAllDay = false; + var timeText = void 0; + if (eventDef.allDay) { + doAllDay = true; + } + else if (isMultiDayRange(seg.eventRange.range)) { // TODO: use (!isStart || !isEnd) instead? + if (seg.isStart) { + timeText = buildSegTimeText(seg, timeFormat, context, null, null, eventInstance.range.start, seg.end); + } + else if (seg.isEnd) { + timeText = buildSegTimeText(seg, timeFormat, context, null, null, seg.start, eventInstance.range.end); + } + else { + doAllDay = true; + } + } + else { + timeText = buildSegTimeText(seg, timeFormat, context); + } + if (doAllDay) { + var hookProps = { + text: context.options.allDayText, + view: context.viewApi, + }; + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.allDayClassNames, content: options.allDayContent, defaultContent: renderAllDayInner, didMount: options.allDayDidMount, willUnmount: options.allDayWillUnmount }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("td", { className: ['fc-list-event-time'].concat(classNames).join(' '), ref: rootElRef }, innerContent)); })); + } + return (createElement("td", { className: "fc-list-event-time" }, timeText)); + } + return null; + } + function renderAllDayInner(hookProps) { + return hookProps.text; + } + + /* + Responsible for the scroller, and forwarding event-related actions into the "grid". + */ + var ListView = /** @class */ (function (_super) { + __extends(ListView, _super); + function ListView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.computeDateVars = memoize(computeDateVars); + _this.eventStoreToSegs = memoize(_this._eventStoreToSegs); + _this.setRootEl = function (rootEl) { + if (rootEl) { + _this.context.registerInteractiveComponent(_this, { + el: rootEl, + }); + } + else { + _this.context.unregisterInteractiveComponent(_this); + } + }; + return _this; + } + ListView.prototype.render = function () { + var _this = this; + var _a = this, props = _a.props, context = _a.context; + var extraClassNames = [ + 'fc-list', + context.theme.getClass('table'), + context.options.stickyHeaderDates !== false ? 'fc-list-sticky' : '', + ]; + var _b = this.computeDateVars(props.dateProfile), dayDates = _b.dayDates, dayRanges = _b.dayRanges; + var eventSegs = this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges); + return (createElement(ViewRoot, { viewSpec: context.viewSpec, elRef: this.setRootEl }, function (rootElRef, classNames) { return (createElement("div", { ref: rootElRef, className: extraClassNames.concat(classNames).join(' ') }, + createElement(Scroller, { liquid: !props.isHeightAuto, overflowX: props.isHeightAuto ? 'visible' : 'hidden', overflowY: props.isHeightAuto ? 'visible' : 'auto' }, eventSegs.length > 0 ? + _this.renderSegList(eventSegs, dayDates) : + _this.renderEmptyMessage()))); })); + }; + ListView.prototype.renderEmptyMessage = function () { + var _a = this.context, options = _a.options, viewApi = _a.viewApi; + var hookProps = { + text: options.noEventsText, + view: viewApi, + }; + return (createElement(RenderHook, { hookProps: hookProps, classNames: options.noEventsClassNames, content: options.noEventsContent, defaultContent: renderNoEventsInner, didMount: options.noEventsDidMount, willUnmount: options.noEventsWillUnmount }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("div", { className: ['fc-list-empty'].concat(classNames).join(' '), ref: rootElRef }, + createElement("div", { className: "fc-list-empty-cushion", ref: innerElRef }, innerContent))); })); + }; + ListView.prototype.renderSegList = function (allSegs, dayDates) { + var _a = this.context, theme = _a.theme, options = _a.options; + var segsByDay = groupSegsByDay(allSegs); // sparse array + return (createElement(NowTimer, { unit: "day" }, function (nowDate, todayRange) { + var innerNodes = []; + for (var dayIndex = 0; dayIndex < segsByDay.length; dayIndex += 1) { + var daySegs = segsByDay[dayIndex]; + if (daySegs) { // sparse array, so might be undefined + var dayStr = dayDates[dayIndex].toISOString(); + // append a day header + innerNodes.push(createElement(ListViewHeaderRow, { key: dayStr, dayDate: dayDates[dayIndex], todayRange: todayRange })); + daySegs = sortEventSegs(daySegs, options.eventOrder); + for (var _i = 0, daySegs_1 = daySegs; _i < daySegs_1.length; _i++) { + var seg = daySegs_1[_i]; + innerNodes.push(createElement(ListViewEventRow, __assign({ key: dayStr + ':' + seg.eventRange.instance.instanceId /* are multiple segs for an instanceId */, seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: false }, getSegMeta(seg, todayRange, nowDate)))); + } + } + } + return (createElement("table", { className: 'fc-list-table ' + theme.getClass('table') }, + createElement("tbody", null, innerNodes))); + })); + }; + ListView.prototype._eventStoreToSegs = function (eventStore, eventUiBases, dayRanges) { + return this.eventRangesToSegs(sliceEventStore(eventStore, eventUiBases, this.props.dateProfile.activeRange, this.context.options.nextDayThreshold).fg, dayRanges); + }; + ListView.prototype.eventRangesToSegs = function (eventRanges, dayRanges) { + var segs = []; + for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) { + var eventRange = eventRanges_1[_i]; + segs.push.apply(segs, this.eventRangeToSegs(eventRange, dayRanges)); + } + return segs; + }; + ListView.prototype.eventRangeToSegs = function (eventRange, dayRanges) { + var dateEnv = this.context.dateEnv; + var nextDayThreshold = this.context.options.nextDayThreshold; + var range = eventRange.range; + var allDay = eventRange.def.allDay; + var dayIndex; + var segRange; + var seg; + var segs = []; + for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex += 1) { + segRange = intersectRanges(range, dayRanges[dayIndex]); + if (segRange) { + seg = { + component: this, + eventRange: eventRange, + start: segRange.start, + end: segRange.end, + isStart: eventRange.isStart && segRange.start.valueOf() === range.start.valueOf(), + isEnd: eventRange.isEnd && segRange.end.valueOf() === range.end.valueOf(), + dayIndex: dayIndex, + }; + segs.push(seg); + // detect when range won't go fully into the next day, + // and mutate the latest seg to the be the end. + if (!seg.isEnd && !allDay && + dayIndex + 1 < dayRanges.length && + range.end < + dateEnv.add(dayRanges[dayIndex + 1].start, nextDayThreshold)) { + seg.end = range.end; + seg.isEnd = true; + break; + } + } + } + return segs; + }; + return ListView; + }(DateComponent)); + function renderNoEventsInner(hookProps) { + return hookProps.text; + } + function computeDateVars(dateProfile) { + var dayStart = startOfDay(dateProfile.renderRange.start); + var viewEnd = dateProfile.renderRange.end; + var dayDates = []; + var dayRanges = []; + while (dayStart < viewEnd) { + dayDates.push(dayStart); + dayRanges.push({ + start: dayStart, + end: addDays(dayStart, 1), + }); + dayStart = addDays(dayStart, 1); + } + return { dayDates: dayDates, dayRanges: dayRanges }; + } + // Returns a sparse array of arrays, segs grouped by their dayIndex + function groupSegsByDay(segs) { + var segsByDay = []; // sparse array + var i; + var seg; + for (i = 0; i < segs.length; i += 1) { + seg = segs[i]; + (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = [])) + .push(seg); + } + return segsByDay; + } + + var OPTION_REFINERS$1 = { + listDayFormat: createFalsableFormatter, + listDaySideFormat: createFalsableFormatter, + noEventsClassNames: identity, + noEventsContent: identity, + noEventsDidMount: identity, + noEventsWillUnmount: identity, + // noEventsText is defined in base options + }; + function createFalsableFormatter(input) { + return input === false ? null : createFormatter(input); + } + + var listPlugin = createPlugin({ + optionRefiners: OPTION_REFINERS$1, + views: { + list: { + component: ListView, + buttonTextKey: 'list', + listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' }, // like "January 1, 2016" + }, + listDay: { + type: 'list', + duration: { days: 1 }, + listDayFormat: { weekday: 'long' }, // day-of-week is all we need. full date is probably in headerToolbar + }, + listWeek: { + type: 'list', + duration: { weeks: 1 }, + listDayFormat: { weekday: 'long' }, + listDaySideFormat: { month: 'long', day: 'numeric', year: 'numeric' }, + }, + listMonth: { + type: 'list', + duration: { month: 1 }, + listDaySideFormat: { weekday: 'long' }, // day-of-week is nice-to-have + }, + listYear: { + type: 'list', + duration: { year: 1 }, + listDaySideFormat: { weekday: 'long' }, // day-of-week is nice-to-have + }, + }, + }); + + var BootstrapTheme = /** @class */ (function (_super) { + __extends(BootstrapTheme, _super); + function BootstrapTheme() { + return _super !== null && _super.apply(this, arguments) || this; + } + return BootstrapTheme; + }(Theme)); + BootstrapTheme.prototype.classes = { + root: 'fc-theme-bootstrap', + table: 'table-bordered', + tableCellShaded: 'table-active', + buttonGroup: 'btn-group', + button: 'btn btn-primary', + buttonActive: 'active', + popover: 'popover', + popoverHeader: 'popover-header', + popoverContent: 'popover-body', + }; + BootstrapTheme.prototype.baseIconClass = 'fa'; + BootstrapTheme.prototype.iconClasses = { + close: 'fa-times', + prev: 'fa-chevron-left', + next: 'fa-chevron-right', + prevYear: 'fa-angle-double-left', + nextYear: 'fa-angle-double-right', + }; + BootstrapTheme.prototype.rtlIconClasses = { + prev: 'fa-chevron-right', + next: 'fa-chevron-left', + prevYear: 'fa-angle-double-right', + nextYear: 'fa-angle-double-left', + }; + BootstrapTheme.prototype.iconOverrideOption = 'bootstrapFontAwesome'; // TODO: make TS-friendly. move the option-processing into this plugin + BootstrapTheme.prototype.iconOverrideCustomButtonOption = 'bootstrapFontAwesome'; + BootstrapTheme.prototype.iconOverridePrefix = 'fa-'; + var plugin = createPlugin({ + themeClasses: { + bootstrap: BootstrapTheme, + }, + }); + + // rename this file to options.ts like other packages? + var OPTION_REFINERS = { + googleCalendarApiKey: String, + }; + + var EVENT_SOURCE_REFINERS = { + googleCalendarApiKey: String, + googleCalendarId: String, + googleCalendarApiBase: String, + extraParams: identity, + }; + + // TODO: expose somehow + var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'; + var eventSourceDef = { + parseMeta: function (refined) { + var googleCalendarId = refined.googleCalendarId; + if (!googleCalendarId && refined.url) { + googleCalendarId = parseGoogleCalendarId(refined.url); + } + if (googleCalendarId) { + return { + googleCalendarId: googleCalendarId, + googleCalendarApiKey: refined.googleCalendarApiKey, + googleCalendarApiBase: refined.googleCalendarApiBase, + extraParams: refined.extraParams, + }; + } + return null; + }, + fetch: function (arg, onSuccess, onFailure) { + var _a = arg.context, dateEnv = _a.dateEnv, options = _a.options; + var meta = arg.eventSource.meta; + var apiKey = meta.googleCalendarApiKey || options.googleCalendarApiKey; + if (!apiKey) { + onFailure({ + message: 'Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/', + }); + } + else { + var url = buildUrl(meta); + // TODO: make DRY with json-feed-event-source + var extraParams = meta.extraParams; + var extraParamsObj = typeof extraParams === 'function' ? extraParams() : extraParams; + var requestParams_1 = buildRequestParams(arg.range, apiKey, extraParamsObj, dateEnv); + requestJson('GET', url, requestParams_1, function (body, xhr) { + if (body.error) { + onFailure({ + message: 'Google Calendar API: ' + body.error.message, + errors: body.error.errors, + xhr: xhr, + }); + } + else { + onSuccess({ + rawEvents: gcalItemsToRawEventDefs(body.items, requestParams_1.timeZone), + xhr: xhr, + }); + } + }, function (message, xhr) { + onFailure({ message: message, xhr: xhr }); + }); + } + }, + }; + function parseGoogleCalendarId(url) { + var match; + // detect if the ID was specified as a single string. + // will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars. + if (/^[^/]+@([^/.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) { + return url; + } + if ((match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^/]*)/.exec(url)) || + (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^/]*)/.exec(url))) { + return decodeURIComponent(match[1]); + } + return null; + } + function buildUrl(meta) { + var apiBase = meta.googleCalendarApiBase; + if (!apiBase) { + apiBase = API_BASE; + } + return apiBase + '/' + encodeURIComponent(meta.googleCalendarId) + '/events'; + } + function buildRequestParams(range, apiKey, extraParams, dateEnv) { + var params; + var startStr; + var endStr; + if (dateEnv.canComputeOffset) { + // strings will naturally have offsets, which GCal needs + startStr = dateEnv.formatIso(range.start); + endStr = dateEnv.formatIso(range.end); + } + else { + // when timezone isn't known, we don't know what the UTC offset should be, so ask for +/- 1 day + // from the UTC day-start to guarantee we're getting all the events + // (start/end will be UTC-coerced dates, so toISOString is okay) + startStr = addDays(range.start, -1).toISOString(); + endStr = addDays(range.end, 1).toISOString(); + } + params = __assign(__assign({}, (extraParams || {})), { key: apiKey, timeMin: startStr, timeMax: endStr, singleEvents: true, maxResults: 9999 }); + if (dateEnv.timeZone !== 'local') { + params.timeZone = dateEnv.timeZone; + } + return params; + } + function gcalItemsToRawEventDefs(items, gcalTimezone) { + return items.map(function (item) { return gcalItemToRawEventDef(item, gcalTimezone); }); + } + function gcalItemToRawEventDef(item, gcalTimezone) { + var url = item.htmlLink || null; + // make the URLs for each event show times in the correct timezone + if (url && gcalTimezone) { + url = injectQsComponent(url, 'ctz=' + gcalTimezone); + } + return { + id: item.id, + title: item.summary, + start: item.start.dateTime || item.start.date, + end: item.end.dateTime || item.end.date, + url: url, + location: item.location, + description: item.description, + attachments: item.attachments || [], + extendedProps: (item.extendedProperties || {}).shared || {}, + }; + } + // Injects a string like "arg=value" into the querystring of a URL + // TODO: move to a general util file? + function injectQsComponent(url, component) { + // inject it after the querystring but before the fragment + return url.replace(/(\?.*?)?(#|$)/, function (whole, qs, hash) { return (qs ? qs + '&' : '?') + component + hash; }); + } + var googleCalendarPlugin = createPlugin({ + eventSourceDefs: [eventSourceDef], + optionRefiners: OPTION_REFINERS, + eventSourceRefiners: EVENT_SOURCE_REFINERS, + }); + + globalPlugins.push(interactionPlugin, dayGridPlugin, timeGridPlugin, listPlugin, plugin, googleCalendarPlugin); + + exports.BASE_OPTION_DEFAULTS = BASE_OPTION_DEFAULTS; + exports.BASE_OPTION_REFINERS = BASE_OPTION_REFINERS; + exports.BaseComponent = BaseComponent; + exports.BgEvent = BgEvent; + exports.BootstrapTheme = BootstrapTheme; + exports.Calendar = Calendar; + exports.CalendarApi = CalendarApi; + exports.CalendarContent = CalendarContent; + exports.CalendarDataManager = CalendarDataManager; + exports.CalendarDataProvider = CalendarDataProvider; + exports.CalendarRoot = CalendarRoot; + exports.Component = Component; + exports.ContentHook = ContentHook; + exports.CustomContentRenderContext = CustomContentRenderContext; + exports.DateComponent = DateComponent; + exports.DateEnv = DateEnv; + exports.DateProfileGenerator = DateProfileGenerator; + exports.DayCellContent = DayCellContent; + exports.DayCellRoot = DayCellRoot; + exports.DayGridView = DayTableView; + exports.DayHeader = DayHeader; + exports.DaySeriesModel = DaySeriesModel; + exports.DayTable = DayTable; + exports.DayTableModel = DayTableModel; + exports.DayTableSlicer = DayTableSlicer; + exports.DayTimeCols = DayTimeCols; + exports.DayTimeColsSlicer = DayTimeColsSlicer; + exports.DayTimeColsView = DayTimeColsView; + exports.DelayedRunner = DelayedRunner; + exports.Draggable = ExternalDraggable; + exports.ElementDragging = ElementDragging; + exports.ElementScrollController = ElementScrollController; + exports.Emitter = Emitter; + exports.EventApi = EventApi; + exports.EventRoot = EventRoot; + exports.EventSourceApi = EventSourceApi; + exports.FeaturefulElementDragging = FeaturefulElementDragging; + exports.Fragment = Fragment; + exports.Interaction = Interaction; + exports.ListView = ListView; + exports.MoreLinkRoot = MoreLinkRoot; + exports.MountHook = MountHook; + exports.NamedTimeZoneImpl = NamedTimeZoneImpl; + exports.NowIndicatorRoot = NowIndicatorRoot; + exports.NowTimer = NowTimer; + exports.PointerDragging = PointerDragging; + exports.PositionCache = PositionCache; + exports.RefMap = RefMap; + exports.RenderHook = RenderHook; + exports.ScrollController = ScrollController; + exports.ScrollResponder = ScrollResponder; + exports.Scroller = Scroller; + exports.SegHierarchy = SegHierarchy; + exports.SimpleScrollGrid = SimpleScrollGrid; + exports.Slicer = Slicer; + exports.Splitter = Splitter; + exports.StandardEvent = StandardEvent; + exports.Table = Table; + exports.TableDateCell = TableDateCell; + exports.TableDowCell = TableDowCell; + exports.TableView = TableView; + exports.Theme = Theme; + exports.ThirdPartyDraggable = ThirdPartyDraggable; + exports.TimeCols = TimeCols; + exports.TimeColsSlatsCoords = TimeColsSlatsCoords; + exports.TimeColsView = TimeColsView; + exports.ViewApi = ViewApi; + exports.ViewContextType = ViewContextType; + exports.ViewRoot = ViewRoot; + exports.WeekNumberRoot = WeekNumberRoot; + exports.WindowScrollController = WindowScrollController; + exports.addDays = addDays; + exports.addDurations = addDurations; + exports.addMs = addMs; + exports.addWeeks = addWeeks; + exports.allowContextMenu = allowContextMenu; + exports.allowSelection = allowSelection; + exports.applyMutationToEventStore = applyMutationToEventStore; + exports.applyStyle = applyStyle; + exports.applyStyleProp = applyStyleProp; + exports.asCleanDays = asCleanDays; + exports.asRoughMinutes = asRoughMinutes; + exports.asRoughMs = asRoughMs; + exports.asRoughSeconds = asRoughSeconds; + exports.binarySearch = binarySearch; + exports.buildClassNameNormalizer = buildClassNameNormalizer; + exports.buildDayRanges = buildDayRanges; + exports.buildDayTableModel = buildDayTableModel; + exports.buildEntryKey = buildEntryKey; + exports.buildEventApis = buildEventApis; + exports.buildEventRangeKey = buildEventRangeKey; + exports.buildHashFromArray = buildHashFromArray; + exports.buildIsoString = buildIsoString; + exports.buildNavLinkData = buildNavLinkData; + exports.buildSegCompareObj = buildSegCompareObj; + exports.buildSegTimeText = buildSegTimeText; + exports.buildSlatMetas = buildSlatMetas; + exports.buildTimeColsModel = buildTimeColsModel; + exports.collectFromHash = collectFromHash; + exports.combineEventUis = combineEventUis; + exports.compareByFieldSpec = compareByFieldSpec; + exports.compareByFieldSpecs = compareByFieldSpecs; + exports.compareNumbers = compareNumbers; + exports.compareObjs = compareObjs; + exports.computeEarliestSegStart = computeEarliestSegStart; + exports.computeEdges = computeEdges; + exports.computeFallbackHeaderFormat = computeFallbackHeaderFormat; + exports.computeHeightAndMargins = computeHeightAndMargins; + exports.computeInnerRect = computeInnerRect; + exports.computeRect = computeRect; + exports.computeSegDraggable = computeSegDraggable; + exports.computeSegEndResizable = computeSegEndResizable; + exports.computeSegStartResizable = computeSegStartResizable; + exports.computeShrinkWidth = computeShrinkWidth; + exports.computeSmallestCellWidth = computeSmallestCellWidth; + exports.computeVisibleDayRange = computeVisibleDayRange; + exports.config = config; + exports.constrainPoint = constrainPoint; + exports.createContext = createContext; + exports.createDuration = createDuration; + exports.createElement = createElement; + exports.createEmptyEventStore = createEmptyEventStore; + exports.createEventInstance = createEventInstance; + exports.createEventUi = createEventUi; + exports.createFormatter = createFormatter; + exports.createPlugin = createPlugin; + exports.createPortal = createPortal; + exports.createRef = createRef; + exports.diffDates = diffDates; + exports.diffDayAndTime = diffDayAndTime; + exports.diffDays = diffDays; + exports.diffPoints = diffPoints; + exports.diffWeeks = diffWeeks; + exports.diffWholeDays = diffWholeDays; + exports.diffWholeWeeks = diffWholeWeeks; + exports.disableCursor = disableCursor; + exports.elementClosest = elementClosest; + exports.elementMatches = elementMatches; + exports.enableCursor = enableCursor; + exports.eventTupleToStore = eventTupleToStore; + exports.filterEventStoreDefs = filterEventStoreDefs; + exports.filterHash = filterHash; + exports.findDirectChildren = findDirectChildren; + exports.findElements = findElements; + exports.flexibleCompare = flexibleCompare; + exports.flushToDom = flushToDom; + exports.formatDate = formatDate; + exports.formatDayString = formatDayString; + exports.formatIsoTimeString = formatIsoTimeString; + exports.formatRange = formatRange; + exports.getAllowYScrolling = getAllowYScrolling; + exports.getCanVGrowWithinCell = getCanVGrowWithinCell; + exports.getClippingParents = getClippingParents; + exports.getDateMeta = getDateMeta; + exports.getDayClassNames = getDayClassNames; + exports.getDefaultEventEnd = getDefaultEventEnd; + exports.getElRoot = getElRoot; + exports.getElSeg = getElSeg; + exports.getEntrySpanEnd = getEntrySpanEnd; + exports.getEventClassNames = getEventClassNames; + exports.getEventTargetViaRoot = getEventTargetViaRoot; + exports.getIsRtlScrollbarOnLeft = getIsRtlScrollbarOnLeft; + exports.getRectCenter = getRectCenter; + exports.getRelevantEvents = getRelevantEvents; + exports.getScrollGridClassNames = getScrollGridClassNames; + exports.getScrollbarWidths = getScrollbarWidths; + exports.getSectionClassNames = getSectionClassNames; + exports.getSectionHasLiquidHeight = getSectionHasLiquidHeight; + exports.getSegMeta = getSegMeta; + exports.getSlotClassNames = getSlotClassNames; + exports.getStickyFooterScrollbar = getStickyFooterScrollbar; + exports.getStickyHeaderDates = getStickyHeaderDates; + exports.getUnequalProps = getUnequalProps; + exports.globalLocales = globalLocales; + exports.globalPlugins = globalPlugins; + exports.greatestDurationDenominator = greatestDurationDenominator; + exports.groupIntersectingEntries = groupIntersectingEntries; + exports.guid = guid; + exports.hasBgRendering = hasBgRendering; + exports.hasShrinkWidth = hasShrinkWidth; + exports.identity = identity; + exports.interactionSettingsStore = interactionSettingsStore; + exports.interactionSettingsToStore = interactionSettingsToStore; + exports.intersectRanges = intersectRanges; + exports.intersectRects = intersectRects; + exports.intersectSpans = intersectSpans; + exports.isArraysEqual = isArraysEqual; + exports.isColPropsEqual = isColPropsEqual; + exports.isDateSelectionValid = isDateSelectionValid; + exports.isDateSpansEqual = isDateSpansEqual; + exports.isInt = isInt; + exports.isInteractionValid = isInteractionValid; + exports.isMultiDayRange = isMultiDayRange; + exports.isPropsEqual = isPropsEqual; + exports.isPropsValid = isPropsValid; + exports.isValidDate = isValidDate; + exports.joinSpans = joinSpans; + exports.listenBySelector = listenBySelector; + exports.mapHash = mapHash; + exports.memoize = memoize; + exports.memoizeArraylike = memoizeArraylike; + exports.memoizeHashlike = memoizeHashlike; + exports.memoizeObjArg = memoizeObjArg; + exports.mergeEventStores = mergeEventStores; + exports.multiplyDuration = multiplyDuration; + exports.padStart = padStart; + exports.parseBusinessHours = parseBusinessHours; + exports.parseClassNames = parseClassNames; + exports.parseDragMeta = parseDragMeta; + exports.parseEventDef = parseEventDef; + exports.parseFieldSpecs = parseFieldSpecs; + exports.parseMarker = parse; + exports.pointInsideRect = pointInsideRect; + exports.preventContextMenu = preventContextMenu; + exports.preventDefault = preventDefault; + exports.preventSelection = preventSelection; + exports.rangeContainsMarker = rangeContainsMarker; + exports.rangeContainsRange = rangeContainsRange; + exports.rangesEqual = rangesEqual; + exports.rangesIntersect = rangesIntersect; + exports.refineEventDef = refineEventDef; + exports.refineProps = refineProps; + exports.removeElement = removeElement; + exports.removeExact = removeExact; + exports.render = render; + exports.renderChunkContent = renderChunkContent; + exports.renderFill = renderFill; + exports.renderMicroColGroup = renderMicroColGroup; + exports.renderScrollShim = renderScrollShim; + exports.requestJson = requestJson; + exports.sanitizeShrinkWidth = sanitizeShrinkWidth; + exports.setElSeg = setElSeg; + exports.setRef = setRef; + exports.sliceEventStore = sliceEventStore; + exports.sliceEvents = sliceEvents; + exports.sortEventSegs = sortEventSegs; + exports.startOfDay = startOfDay; + exports.translateRect = translateRect; + exports.triggerDateSelect = triggerDateSelect; + exports.unmountComponentAtNode = unmountComponentAtNode; + exports.unpromisify = unpromisify; + exports.version = version; + exports.whenTransitionDone = whenTransitionDone; + exports.wholeDivideDurations = wholeDivideDurations; + + Object.defineProperty(exports, '__esModule', { value: true }); + + return exports; + +}({})); diff --git a/apps/schoolCalendar/screenshot_basic.png b/apps/schoolCalendar/screenshot_basic.png new file mode 100644 index 000000000..879ad6a3c Binary files /dev/null and b/apps/schoolCalendar/screenshot_basic.png differ diff --git a/apps/schoolCalendar/screenshot_info.png b/apps/schoolCalendar/screenshot_info.png new file mode 100644 index 000000000..c539b1b1c Binary files /dev/null and b/apps/schoolCalendar/screenshot_info.png differ diff --git a/apps/scolor/bangle1-view-color-screenshot.png b/apps/scolor/bangle1-view-color-screenshot.png new file mode 100644 index 000000000..d5139f4d9 Binary files /dev/null and b/apps/scolor/bangle1-view-color-screenshot.png differ diff --git a/apps/sensible/ChangeLog b/apps/sensible/ChangeLog new file mode 100644 index 000000000..ba597a22f --- /dev/null +++ b/apps/sensible/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Corrected variable initialisation diff --git a/apps/sensible/README.md b/apps/sensible/README.md new file mode 100644 index 000000000..f79b61aea --- /dev/null +++ b/apps/sensible/README.md @@ -0,0 +1,35 @@ +# Sensible + +Collect all the sensor data from the Bangle.js 2, display the live readings in menu pages, and broadcast in Bluetooth Low Energy (BLE) advertising packets to any listening devices in range. + + +## Usage + +The advertising packets will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the standard packet types. Also convenient for testing individual sensors of the Bangle.js 2 via the menu interface. + + +## Features + +Currently implements: +- Accelerometer +- Barometer +- GPS +- Heart Rate Monitor +- Magnetometer + +in the menu display but NOT YET in Bluetooth Low Energy advertising (which will be implemented in a subsequent version). + + +## Controls + +Browse and control sensors using the standard Espruino menu interface. + + +## Requests + +[Contact reelyActive](https://www.reelyactive.com/contact/) for support/updates. + + +## Creator + +Developed by [jeffyactive](https://github.com/jeffyactive) of [reelyActive](https://www.reelyactive.com) diff --git a/apps/sensible/sensible-icon.js b/apps/sensible/sensible-icon.js new file mode 100644 index 000000000..f904fc7f3 --- /dev/null +++ b/apps/sensible/sensible-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkG/4AG+cilGIxGCkU/B44AGmQUBAAsjCyoYN+QWJAAMvCxsjLQXzG4gYIOIZwG+YLDCw34BRIkFx4JFHQRDElGCJYgOCFw5RCPQwJFGAg4BIoSRIDAQQEG4YLBHgYAGJQIjCJ4RGBDoU4SIqNDwYwDJAQEDFwSRGDAQfBFQgIDFwQtDRoowBAgQDEDYQzC7oACTogrEA4IfF/4WDDAY/Fx4CCEYQbB/oXF74TDCAYGBUoIDDCwowCUoIkBAYSABGwIDCLogADBIKMCAYRODLwRGGJAaMFPwghBnoXJHoJ8DF4Q5DC5HTKogVBgAAFpoXH6oQGAA1dC7/UC5sNC4/dCA0QAwsEC50BC40AC5FQC4sgMB4XFgUwC40FC4/QBwkD+B5HDA6oFh/xSREFqtVbogMEj/yVxkFMwRgEl//Y5sAqhgF///SA4AHghgDgQXBPBAAHrpICh4XBMBoADC4ReBAALxHABUBCwX/bI4AKgYXD+YXRn4XDSKCNDAAZ5QOoZhSLohhESRkBLopJQIo4YOCxYYCJQ0BCxoACmURCoMRkYOI")) \ No newline at end of file diff --git a/apps/sensible/sensible.js b/apps/sensible/sensible.js new file mode 100644 index 000000000..c569ff720 --- /dev/null +++ b/apps/sensible/sensible.js @@ -0,0 +1,162 @@ +/** + * Copyright reelyActive 2021 + * We believe in an open Internet of Things + */ + + +// Non-user-configurable constants +const APP_ID = 'sensible'; + + +// Global variables +let acc, bar, hrm, mag; +let isAccMenu = false; +let isBarMenu = false; +let isGpsMenu = false; +let isHrmMenu = false; +let isMagMenu = false; +let isBarEnabled = true; +let isGpsEnabled = true; +let isHrmEnabled = true; +let isMagEnabled = true; + + +// Menus +let mainMenu = { + "": { "title": "-- SensiBLE --" }, + "Acceleration": function() { E.showMenu(accMenu); isAccMenu = true; }, + "Barometer": function() { E.showMenu(barMenu); isBarMenu = true; }, + "GPS": function() { E.showMenu(gpsMenu); isGpsMenu = true; }, + "Heart Rate": function() { E.showMenu(hrmMenu); isHrmMenu = true; }, + "Magnetometer": function() { E.showMenu(magMenu); isMagMenu = true; } +}; +let accMenu = { + "": { "title" : "- Acceleration -" }, + "State": { value: "On" }, + "x": { value: null }, + "y": { value: null }, + "z": { value: null }, + "<-": function() { E.showMenu(mainMenu); isAccMenu = false; }, +}; +let barMenu = { + "": { "title" : "- Barometer -" }, + "State": { + value: isBarEnabled, + format: v => v ? "On" : "Off", + onchange: v => { isBarEnabled = v; Bangle.setBarometerPower(v, APP_ID); } + }, + "Altitude": { value: null }, + "Press": { value: null }, + "Temp": { value: null }, + "<-": function() { E.showMenu(mainMenu); isBarMenu = false; }, +}; +let gpsMenu = { + "": { "title" : "- GPS -" }, + "State": { + value: isGpsEnabled, + format: v => v ? "On" : "Off", + onchange: v => { isGpsEnabled = v; Bangle.setGPSPower(v, APP_ID); } + }, + "Lat": { value: null }, + "Lon": { value: null }, + "Altitude": { value: null }, + "Satellites": { value: null }, + "HDOP": { value: null }, + "<-": function() { E.showMenu(mainMenu); isGpsMenu = false; }, +}; +let hrmMenu = { + "": { "title" : "- Heart Rate -" }, + "State": { + value: isHrmEnabled, + format: v => v ? "On" : "Off", + onchange: v => { isHrmEnabled = v; Bangle.setHRMPower(v, APP_ID); } + }, + "BPM": { value: null }, + "Confidence": { value: null }, + "<-": function() { E.showMenu(mainMenu); isHrmMenu = false; }, +}; +let magMenu = { + "": { "title" : "- Magnetometer -" }, + "State": { + value: isMagEnabled, + format: v => v ? "On" : "Off", + onchange: v => { isMagEnabled = v; Bangle.setCompassPower(v, APP_ID); } + }, + "x": { value: null }, + "y": { value: null }, + "z": { value: null }, + "Heading": { value: null }, + "<-": function() { E.showMenu(mainMenu); isMagMenu = false; }, +}; + + +// Update acceleration +Bangle.on('accel', function(newAcc) { + acc = newAcc; + + if(isAccMenu) { + accMenu.x.value = acc.x.toFixed(2); + accMenu.y.value = acc.y.toFixed(2); + accMenu.z.value = acc.z.toFixed(2); + E.showMenu(accMenu); + } +}); + +// Update barometer +Bangle.on('pressure', function(newBar) { + bar = newBar; + + if(isBarMenu) { + barMenu.Altitude.value = bar.altitude.toFixed(1) + 'm'; + barMenu.Press.value = bar.pressure.toFixed(1) + 'mbar'; + barMenu.Temp.value = bar.temperature.toFixed(1) + 'C'; + E.showMenu(barMenu); + } +}); + +// Update GPS +Bangle.on('GPS', function(newGps) { + gps = newGps; + + if(isGpsMenu) { + gpsMenu.Lat.value = gps.lat.toFixed(4); + gpsMenu.Lon.value = gps.lon.toFixed(4); + gpsMenu.Altitude.value = gps.alt + 'm'; + gpsMenu.Satellites.value = gps.satellites; + gpsMenu.HDOP.value = (gps.hdop * 5).toFixed(1) + 'm'; + E.showMenu(gpsMenu); + } +}); + +// Update heart rate monitor +Bangle.on('HRM', function(newHrm) { + hrm = newHrm; + + if(isHrmMenu) { + hrmMenu.BPM.value = hrm.bpm; + hrmMenu.Confidence.value = hrm.confidence + '%'; + E.showMenu(hrmMenu); + } +}); + +// Update magnetometer +Bangle.on('mag', function(newMag) { + mag = newMag; + + if(isMagMenu) { + magMenu.x.value = mag.x; + magMenu.y.value = mag.y; + magMenu.z.value = mag.z; + magMenu.Heading.value = mag.heading.toFixed(1); + E.showMenu(magMenu); + } +}); + + +// On start: enable sensors and display main menu +g.clear(); +Bangle.setBarometerPower(isBarEnabled, APP_ID); +Bangle.setGPSPower(isGpsEnabled, APP_ID); +Bangle.setHRMPower(isHrmEnabled, APP_ID); +Bangle.setCompassPower(isMagEnabled, APP_ID); +E.showMenu(mainMenu); \ No newline at end of file diff --git a/apps/sensible/sensible.png b/apps/sensible/sensible.png new file mode 100644 index 000000000..d3e3dfbef Binary files /dev/null and b/apps/sensible/sensible.png differ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index faa50405f..b393dda00 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -36,3 +36,5 @@ 0.31: Remove Bangle 1 settings when running on Bangle 2 0.32: Fix 'beep' menu on Bangle.js 2 0.33: Really fix 'beep' menu on Bangle.js 2 this time +0.34: Remove Quiet Mode LCD settings: now handled by Quiet Mode Schedule app +0.35: Change App/Widget settings to 'App Settings' so it fits on Bangle screen diff --git a/apps/setting/README.md b/apps/setting/README.md index 1875fc3b0..fb567030f 100644 --- a/apps/setting/README.md +++ b/apps/setting/README.md @@ -44,6 +44,4 @@ The exact effects depend on the app. In general the watch will not wake up by i - Off: Normal operation - Alarms: Stops notifications, but "alarm" apps will still work - Silent: Blocks even alarms -* **LCD Brightness**, **LCD Timeout**, **Wake on X**: - Override default settings while Quit Mode is active (either as *Alarms* or *Silent*) \ No newline at end of file diff --git a/apps/setting/settings.js b/apps/setting/settings.js index fcf651b6f..e00c15462 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -7,17 +7,12 @@ let settings; function updateSettings() { //storage.erase('setting.json'); // - not needed, just causes extra writes if settings were the same - if (Object.keys(settings.qmOptions).length === 0) delete settings.qmOptions; storage.write('setting.json', settings); - if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in this file } function updateOptions() { updateSettings(); Bangle.setOptions(settings.options) - if (settings.quiet) { - Bangle.setOptions(settings.qmOptions) - } } function gToInternal(g) { @@ -56,18 +51,12 @@ function resetSettings() { twistMaxY: -800, twistTimeout: 1000 }, - // Quiet Mode options: - // we only set these if we want to override the default value - // qmOptions: {}, - // qmBrightness: undefined, - // qmTimeout: undefined, }; updateSettings(); } settings = storage.readJSON('setting.json', 1); if (!settings) resetSettings(); -if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in here const boolFormat = v => v ? "On" : "Off"; @@ -107,7 +96,7 @@ function showMainMenu() { '': { 'title': 'Settings' }, '< Back': ()=>load(), 'Make Connectable': ()=>makeConnectable(), - 'App/Widget Settings': ()=>showAppSettingsMenu(), + 'App Settings': ()=>showAppSettingsMenu(), 'BLE': ()=>showBLEMenu(), 'Debug Info': { value: settings.log, @@ -130,7 +119,16 @@ function showMainMenu() { } } }, - "Quiet Mode": ()=>showQuietModeMenu(), + "Quiet Mode": { + value: settings.quiet|0, + format: v => ["Off", "Alarms", "Silent"][v%3], + onchange: v => { + settings.quiet = v%3; + updateSettings(); + updateOptions(); + if ("qmsched" in WIDGETS) WIDGETS["qmsched"].draw(); + }, + }, 'Locale': ()=>showLocaleMenu(), 'Select Clock': ()=>showClockMenu(), 'Set Time': ()=>showSetTimeMenu(), @@ -352,9 +350,7 @@ function showLCDMenu() { onchange: v => { settings.brightness = v || 1; updateSettings(); - if (!(settings.quiet && "qmBrightness" in settings)) { - Bangle.setLCDBrightness(settings.brightness); - } + Bangle.setLCDBrightness(settings.brightness); } }, 'LCD Timeout': { @@ -365,9 +361,7 @@ function showLCDMenu() { onchange: v => { settings.timeout = 0 | v; updateSettings(); - if (!(settings.quiet && "qmTimeout" in settings)) { - Bangle.setLCDTimeout(settings.timeout); - } + Bangle.setLCDTimeout(settings.timeout); } }, 'Wake on BTN1': { @@ -455,105 +449,6 @@ function showLCDMenu() { }); return E.showMenu(lcdMenu) } -function showQuietModeMenu() { - // we always keep settings.quiet and settings.qmOptions - // other qm values are deleted when not set - const modes = ["Off", "Alarms", "Silent"]; - const qmDisabledFormat = v => v ? "Off" : "-"; - const qmMenu = { - "": {"title": "Quiet Mode"}, - "< Back": () => showMainMenu(), - "Quiet Mode": { - value: settings.quiet|0, - format: v => modes[v%3], - onchange: v => { - settings.quiet = v%3; - updateSettings(); - updateOptions(); - if ("qmsched" in WIDGETS) {WIDGETS["qmsched"].draw();} - }, - }, - "LCD Brightness": { - value: settings.qmBrightness || 0, - min: 0, // 0 = use default - max: 1, - step: 0.1, - format: v => (v>0.05) ? v : "-", - onchange: v => { - if (v>0.05) { // prevent v=0.000000000000001 bugs - settings.qmBrightness = v; - } else { - delete settings.qmBrightness; - } - updateSettings(); - if (settings.qmBrightness) { // show result, even if not quiet right now - Bangle.setLCDBrightness(v); - } else { - Bangle.setLCDBrightness(settings.brightness); - } - }, - }, - "LCD Timeout": { - value: settings.qmTimeout || 0, - min: 0, // 0 = use default (no constant on for quiet mode) - max: 60, - step: 5, - format: v => v>1 ? v : "-", - onchange: v => { - if (v>1) { - settings.qmTimeout = v; - } else { - delete settings.qmTimeout; - } - updateSettings(); - if (settings.quiet && v>1) { - Bangle.setLCDTimeout(v); - } else { - Bangle.setLCDTimeout(settings.timeout); - } - }, - }, - // we disable wakeOn* events by overwriting them as false in qmOptions - // not disabled = not present in qmOptions at all - "Wake on FaceUp": { - value: "wakeOnFaceUp" in settings.qmOptions, - format: qmDisabledFormat, - onchange: () => { - if ("wakeOnFaceUp" in settings.qmOptions) { - delete settings.qmOptions.wakeOnFaceUp; - } else { - settings.qmOptions.wakeOnFaceUp = false; - } - updateOptions(); - }, - }, - "Wake on Touch": { - value: "wakeOnTouch" in settings.qmOptions, - format: qmDisabledFormat, - onchange: () => { - if ("wakeOnTouch" in settings.qmOptions) { - delete settings.qmOptions.wakeOnTouch; - } else { - settings.qmOptions.wakeOnTouch = false; - } - updateOptions(); - }, - }, - "Wake on Twist": { - value: "wakeOnTwist" in settings.qmOptions, - format: qmDisabledFormat, - onchange: () => { - if ("wakeOnTwist" in settings.qmOptions) { - delete settings.qmOptions.wakeOnTwist; - } else { - settings.qmOptions.wakeOnTwist = false; - } - updateOptions(); - }, - }, - }; - return E.showMenu(qmMenu); -} function showLocaleMenu() { const localemenu = { diff --git a/apps/simpletimer/bangle1-timer-screenshot.png b/apps/simpletimer/bangle1-timer-screenshot.png new file mode 100644 index 000000000..91462d3d4 Binary files /dev/null and b/apps/simpletimer/bangle1-timer-screenshot.png differ diff --git a/apps/slevel/ChangeLog b/apps/slevel/ChangeLog index 5560f00bc..3a6431e50 100644 --- a/apps/slevel/ChangeLog +++ b/apps/slevel/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Updated to work with both Bangle.js 1 and 2. diff --git a/apps/slevel/spiritlevel.js b/apps/slevel/spiritlevel.js index 492fc60e1..9db54b825 100644 --- a/apps/slevel/spiritlevel.js +++ b/apps/slevel/spiritlevel.js @@ -1,5 +1,7 @@ g.clear(); var old = {x:0,y:0}; +var W = g.getWidth(); +var H = g.getHeight(); Bangle.on('accel',function(v) { var max = Math.max(Math.abs(v.x),Math.abs(v.y),Math.abs(v.z)); if (Math.abs(v.y)==max) { @@ -14,17 +16,17 @@ Bangle.on('accel',function(v) { g.setColor(1,1,1); g.setFont("6x8",2); g.setFontAlign(0,-1); - g.clearRect(60,0,180,16); - g.drawString(ang.toFixed(1),120,0); + g.clearRect(W*(1/4),0,W*(3/4),H*(1/16)); + g.drawString(ang.toFixed(1),W/2,0); var n = { - x:E.clip(120+v.x*256,4,236), - y:E.clip(120+v.y*256,4,236)}; + x:E.clip(W/2+v.x*256,4,W-4), + y:E.clip(H/2+v.y*256,4,H-4)}; g.clearRect(old.x-3,old.y-3,old.x+6,old.y+6); g.setColor(1,1,1); g.fillRect(n.x-3,n.y-3,n.x+6,n.y+6); g.setColor(1,0,0); - g.drawCircle(120,120,20); - g.drawCircle(120,120,60); - g.drawCircle(120,120,100); + g.drawCircle(W/2,H/2,W*(1/12)); + g.drawCircle(W/2,H/2,W*(1/4)); + g.drawCircle(W/2,H/2,W*(5/12)); old = n; }); diff --git a/apps/slomoclock/bangle1-slow-mo-clock-screenshot.png b/apps/slomoclock/bangle1-slow-mo-clock-screenshot.png new file mode 100644 index 000000000..59018f93c Binary files /dev/null and b/apps/slomoclock/bangle1-slow-mo-clock-screenshot.png differ diff --git a/apps/snake/README.md b/apps/snake/README.md index 483eae7a9..278dffbc4 100644 --- a/apps/snake/README.md +++ b/apps/snake/README.md @@ -7,8 +7,7 @@ Eat apples and don't bite your tail. ## Controls -- UP: BTN1 -- DOWN: BTN3 -- LEFT: BTN4 -- RIGHT: BTN5 -- PAUSE: BTN2 +- BTN1: turn to left +- BTN2: pause +- BTN3: turn to right + diff --git a/apps/snek/snek-icon.js b/apps/snek/snek-icon.js new file mode 100644 index 000000000..c919c429e --- /dev/null +++ b/apps/snek/snek-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("pFIwkB+MRAEH/EUIA7i93u9xEUIABEf4AC+93/4CBETpDBv//gEHEf6MB9wkB/8HSDl3vwjCAAfnErIjiEQYdBAAXuAoSNXEYIdDAAoj4j/3DpN3v6NWAA3/fDYjgRgIjLu9xESj2BAAN/SQwLBEe4XDdwghDBQQjSCgN+C5D9FEebTDEZJEWEQSDVEdZpDZYPnETYhDAAhpeEbzREI0rTbEdXuETb4Bvz1BAYIj/EYxrg9yQDv/3JoS9WEcoaGAAQtBOwYABEaSMBWoYeFJKgjjiIUD9ySEEjwAJFogj0SQgAFBQ4jRABcfXoQj/TowjCOgIkeEf7lHvz+CEb93Ef4jHR8Rr/fY4jCuIifAAQj/EIojbeohGeEcQhHfLRFDeoIicEcQbDv3uEYiNXIgnu87SgEcf/DKnxBJEf/4ACESf/EcYA==")) \ No newline at end of file diff --git a/apps/snek/snek.js b/apps/snek/snek.js new file mode 100644 index 000000000..4c3aec73e --- /dev/null +++ b/apps/snek/snek.js @@ -0,0 +1,469 @@ +function init() { + this.titleScreen = true; + this.min = 0; + this.max = 160; + this.step = 20; + this.scoreMultiplier = 25; + this.totalGrid = this.max / this.step; + + if (g.theme.dark) { + this.textColor = 1; + } else { + this.textColor = 0; + } + + this.getNewPosistion = () => { + let newPos; + while (!newPos) { + // random bonus points for bad luck / lag + if (currentPosition.length > 10) { + this.score += 1; + } + const x = Math.floor(Math.random() * this.totalGrid + 1) * this.step; + const y = Math.floor(Math.random() * this.totalGrid + 1) * this.step; + const found = currentPosition.find(pos => { + return pos.x === x && pos.y === y; + }); + if (!found) { + newPos = {x: x, y: y}; + } + } + return newPos; + }; + + this.restart = () => { + g.clear(); + this.titleScreen = false; + this.score = 0; + this.paused = false; + this.currentPosition = [{x: 2 * step, y: 3 * step},{x: 1 * step, y: 3 * step}]; + this.death = false; + this.gameSpeed = 200; + this.directionSet = null; + this.direction = 1; + this.createApple(); + }; + + const game = () => { + if (this.death && !this.paused) { + g.clear(); + this.showDeathScreen(); + } else if (this.titleScreen && !this.paused) { + this.showTitleScreen(); + } else if (!this.paused) { + g.clear(); + this.drawApple(); + this.drawSnake(); + this.boundries(); + + } + + setTimeout(() => { + game(); + }, this.gameSpeed); + }; + + this.increaseDifficulity = () => { + if (gameSpeed > 59) { + gameSpeed -= 10; + } + }; + + this.createApple = () => { + this.applePosition = getNewPosistion(); + }; + + this.drawApple = () => { + g.setColor(0, 1, 0); + + g.drawImage(this.appleLeaf, this.applePosition.x - 15, this.applePosition.y - 10); + g.setColor(1, 0, 0); + + g.drawImage(this.apple, this.applePosition.x - 15, this.applePosition.y - 2); + }; + + this.checkmax = (x) => { + if (x > this.max) { + return this.min; + } else if (x < this.min) { + return this.max; + } + return x; + }; + + this.movement = (lastItem) => { + let newPosition; + switch(this.direction) { + case 3: + newPosition = { + x: checkmax(lastItem.x + this.step), + y: lastItem.y + }; + break; + case 1: + newPosition = { + x: checkmax(lastItem.x - this.step), + y: lastItem.y + }; + break; + case 2: + newPosition = { + x: lastItem.x, + y: checkmax(lastItem.y + this.step) + }; + break; + case 0: + newPosition = { + x: lastItem.x, + y: checkmax(lastItem.y - this.step) + }; + break; + } + this.directionSet = false; + this.checkDeath(newPosition); + this.currentPosition.push(newPosition); + + }; + + this.snakeHead = (props) => { + switch (this.direction) { + case 0: + return [this.snakeUp, props.x - 9, props.y - 12]; + case 1: + return [this.snakeLeft, props.x - 20, props.y - 10]; + case 3: + return [this.snakeRight, props.x - 12, props.y - 12]; + case 2: + return [this.snakeDown, props.x - 12, props.y - 7]; + default: + return [this.snakeDown, props.x - 12, props.y - 7]; + } + }; + + this.drawSnake = () => { + const totalItems = this.currentPosition.length - 1; + g.setColor(0, 1, 0); + this.movement(this.currentPosition[totalItems]); + this.currentPosition.forEach((props, index) => { + if (index-1 === totalItems) { + const head = this.snakeHead(props); + + g.drawImage(head[0], head[1], head[2]); + } else { + g.fillCircle(props.x, props.y, 10); + } + }); + if (this.currentPosition[totalItems].x === this.applePosition.x && this.currentPosition[totalItems].y === this.applePosition.y) { + this.createApple(); + this.increaseDifficulity(); + } else { + this.currentPosition.shift(); + } + }; + + this.checkDeath = (newPos) => { + + const found = this.currentPosition.find((oldPos) => { + return newPos.x === oldPos.x && newPos.y === oldPos.y; + }); + if (found) { + Bangle.buzz(); + g.clear(); + this.death = true; + } + }; + + this.boundries = () => { + if (this.currentPosition.x >= this.maxPx) { + this.currentPosition.x = this.maxPx; + } + else if (this.currentPosition.x < 10) { + this.currentPosition.x = 10; + } + + if ( this.currentPosition.y >= this.maxPy) { + this.currentPosition.y = this.maxPy; + } else if (this.currentPosition.y < 10) { + this.currentPosition.y = 10; + } + }; + + this.creatTopScrore = () => { + require("Storage").writeJSON("snek_jd", { + topScore: this.calculateScore() + }); + }; + + this.calculateScore = () => { + return currentPosition.length * this.scoreMultiplier + this.score; + }; + + this.showDeathScreen = () => { + this.paused = true; + g.setFont('Vector', 25); + g.setColor(1, 0, 0); + g.drawString("GAME OVER",15, 50, "solid"); + g.setFont('Vector', 15); + g.setColor(this.textColor, this.textColor, this.textColor); + g.drawString("Score : " + this.calculateScore(), 50, 78, "solid"); + + let storage = require("Storage").readJSON("snek_jd"); + if (storage && storage.topScore) { + if (storage.topScore < this.calculateScore()) { + g.setColor(0, 1, 1); + g.drawString("New top score!", 20, 95, "solid"); + g.setFont('Vector', 22); + g.drawString(this.calculateScore(), 20, 115, "solid"); + + this.creatTopScrore(); + } else { + g.setColor(this.textColor, this.textColor, this.textColor); + g.drawString("Top score : " + storage.topScore, 20, 95, "solid"); + } + } else { + this.creatTopScrore(); + } + g.setFont('Vector', 25); + }; + + /* Events */ + Bangle.on('tap', (data) => { + Bangle.setLCDPower(true); + if (this.death) { + this.showTitleScreen(); + } else if (this.titleScreen || this.paused) { + this.restart(); + } + }); + + Bangle.on('accel', (xyz) => { + if (Math.abs(xyz.x) > Math.abs(xyz.y)) { + if (xyz.x < 0) { + if (!this.directionSet && this.direction !== 1) { + Bangle.setLCDPower(true); + this.direction = 3; + } + } else { + if (!this.directionSet && this.direction !== 3) { + Bangle.setLCDPower(true); + this.direction = 1; + } + } + } else { + if (xyz.y < 0) { + if (!this.directionSet && this.direction !== 0) { + Bangle.setLCDPower(true); + this.direction = 2; + } + } else { + if (!this.directionSet && this.direction !== 2) { + Bangle.setLCDPower(true); + this.direction = 0; + } + } + } + this.directionSet = true; + }); + + this.showTitleScreen = () => { + this.death = false; + g.clear(); + g.setColor(0, 1, 0); + g.setFont('Vector', 50); + g.drawString("nek", 70, 15, "solid"); + g.drawImage(this.titleScreenImg, 20, 20); + g.fillPoly([ + 15, 66, + 152, 70, + 159, 79, + 21, 71 ]); + g.setColor(this.textColor, this.textColor, this.textColor); + g.setFont('Vector', 15); + g.drawString("Tilt to turn", 20, 100, "solid"); + g.drawString("Tap to start", 20, 120, "solid"); + + g.setColor(0, 1, 0); + + g.setFont('4x6', 3); + g.drawString("Jason de Belle", 5, 145, "solid"); + + + + }; + +/* Graphics */ + this.snakeUp = Graphics.createImage(` + XX XX + xx xx + xx xx + xx + xx + xx + xx + xx + xxxxxxxx + xxxx xxxx + xxxxxx xxxxxxx + xxxxxxxxxxxxxxxx + xxxxx xXXx xxxxx + xxxxx XX xxxxx + xxxxx XX xxxxx + xxxxxx xxxx xxxxx + xxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxx + `); + this.snakeDown = Graphics.createImage(` + xxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxx xxxx xxxxxx + xxxxxx xxxx xxxxxx + xxxx XX xxxxx + xxxx XX xxxxx + xxxx xXXx xxxx + xxxx xXXx xxxx + xXxxxxxxxxxxxx + xXxxxxxxxxxxxx + xxx xxx + xxxx xxxx + xxxx + xx + xx + xx + xx + x x + xx xx + xx xx + `); + + this.snakeRight = Graphics.createImage(` + xxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xXxxxxxx xxxx + xXxxxxxx xxxx + xxxxxxxX xxx + xxxxxxxX xxx xxxx +xxxxxxxxxxXX xxxxxx xxxx +xxxxxxxxxxXX xxxxxx xx +xxxxxxxxxxXXxxxxx xxxxxxxx +xxxxxxxxxxXXxxxxx xxxxxxxx +xxxxxxxxxxxx xxxxx xx +xxxxxxxxxxxx xxxxx xxx + xxxxxxxx xx xxxx + xxxxxxxx xx + xxxxxxxx xxx + xxxxxxxx xxx + xxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxx + xxxxxxxxx + `); + this.snakeLeft = Graphics.createImage(` + xxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xXxxxxxxxxxxxx + xX xxxxxxxxxx + x xx xXxxxxxxxx + xx xx xXxxxxxxxx + xx xx xxxx xxXXxxxxxxxx + xxxxxxx xxxxxXXxxxxxxxx + xxxxxxx xxxxxXXxxxxxxxx + xx xx xxxx xxXXxxxxxxxx + xx xxx xxxxxxxxxx + x xxx xxxxxxxxx + xxxx xxxxxxxxx + xxxx xxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxx + xxxxxxxxx + `); + + this.apple = Graphics.createImage(` + xxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxx + `); + + this.appleLeaf = Graphics.createImage(` + xxxxxx + xxxxxx + xxxx + XXxxxxxxxx + xx + xx + xx + xx + xx + `); + + +this.titleScreenImg = Graphics.createImage(` + sxxxxxxxs + xxsxxx xxxxxs + xxxxxxxxsxx xxxxsx + xxxxxxxxxxxxxxxsxxs xxxxsxx + xxxxxxxxxxxxxxxxxxxxxsxxxsssxxxxxxsxxxx + xxxxxxxxxxxxxxxxxxxsxxxxsxxs xxsxxx + xxxxxxxxxxxxxxxxxxxxxxxxxsxx xxxsxx + xxxxxxxxxxxxxxxxxxxxx sxxx ssxxsx + xxxxxxxxxxxxxxx xxxxxxs + xxxxxxxxxxxx ssss + xxxxxxxxxxxxx +xxxxxxxxxxxx +xxxxxxxxxxx +xxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxx +xxxxxxx +xxx +`); + + game(); +} +init(); \ No newline at end of file diff --git a/apps/snek/snek.png b/apps/snek/snek.png new file mode 100644 index 000000000..8cf8d05e3 Binary files /dev/null and b/apps/snek/snek.png differ diff --git a/apps/svclock/bangle2-simple-v-clock-screenshot.png b/apps/svclock/bangle2-simple-v-clock-screenshot.png new file mode 100644 index 000000000..a2050bb89 Binary files /dev/null and b/apps/svclock/bangle2-simple-v-clock-screenshot.png differ diff --git a/apps/swatch/bangle1-stopwatch-screenshot.png b/apps/swatch/bangle1-stopwatch-screenshot.png new file mode 100644 index 000000000..be3ed0c9a Binary files /dev/null and b/apps/swatch/bangle1-stopwatch-screenshot.png differ diff --git a/apps/sweepclock/bangle1-sweep-clock-screenshot.png b/apps/sweepclock/bangle1-sweep-clock-screenshot.png new file mode 100644 index 000000000..4ad5a0b7a Binary files /dev/null and b/apps/sweepclock/bangle1-sweep-clock-screenshot.png differ diff --git a/apps/swiperclocklaunch/ChangeLog b/apps/swiperclocklaunch/ChangeLog index 2286a7f70..c1b4a5fbb 100644 --- a/apps/swiperclocklaunch/ChangeLog +++ b/apps/swiperclocklaunch/ChangeLog @@ -1 +1,2 @@ -0.01: New App! \ No newline at end of file +0.01: New App! +0.02: Fix issue with mode being undefined diff --git a/apps/swiperclocklaunch/boot.js b/apps/swiperclocklaunch/boot.js index 0bb8d588a..e9b203eee 100644 --- a/apps/swiperclocklaunch/boot.js +++ b/apps/swiperclocklaunch/boot.js @@ -3,6 +3,7 @@ var sui = Bangle.setUI; Bangle.setUI = function(mode, cb) { sui(mode,cb); + if(!mode) return; if (!mode.startsWith("clock")) return; Bangle.swipeHandler = dir => { if (dir<0) Bangle.showLauncher(); }; Bangle.on("swipe", Bangle.swipeHandler); @@ -14,4 +15,4 @@ setTimeout(function() { Bangle.swipeHandler = dir => { if (dir>0) load(); }; Bangle.on("swipe", Bangle.swipeHandler); } -}, 10); \ No newline at end of file +}, 10); diff --git a/apps/swlclk/bangle1-SWL-clock-screenshot.png b/apps/swlclk/bangle1-SWL-clock-screenshot.png new file mode 100644 index 000000000..6b4f8fceb Binary files /dev/null and b/apps/swlclk/bangle1-SWL-clock-screenshot.png differ diff --git a/apps/thermom/ChangeLog b/apps/thermom/ChangeLog index 78fed5826..6ab6ba8e5 100644 --- a/apps/thermom/ChangeLog +++ b/apps/thermom/ChangeLog @@ -1 +1,2 @@ 0.02: New App! +0.03: Improved messages and added Celsius sign diff --git a/apps/thermom/app.js b/apps/thermom/app.js index baa38e8ec..7eae9b3d4 100644 --- a/apps/thermom/app.js +++ b/apps/thermom/app.js @@ -3,9 +3,9 @@ function onTemperature(p) { g.setFont("6x8",2).setFontAlign(0,0); var x = g.getWidth()/2; var y = g.getHeight()/2 + 10; - g.drawString("Temperature", x, y - 45); + g.drawString("Temperature:", x, y - 45); g.setFontVector(70).setFontAlign(0,0); - g.drawString(p.temperature.toFixed(1), x, y); + g.drawString(p.temperature.toFixed(1) + " °C", x, y); } function drawTemperature() { @@ -23,6 +23,6 @@ setInterval(function() { drawTemperature(); }, 20000); drawTemperature(); -E.showMessage("Loading..."); +E.showMessage("Reading temperature..."); Bangle.loadWidgets(); -Bangle.drawWidgets(); \ No newline at end of file +Bangle.drawWidgets(); diff --git a/apps/timecal/icon.png b/apps/timecal/icon.png new file mode 100644 index 000000000..ca57bf416 Binary files /dev/null and b/apps/timecal/icon.png differ diff --git a/apps/timecal/timecal.app.js b/apps/timecal/timecal.app.js new file mode 100644 index 000000000..b28326c46 --- /dev/null +++ b/apps/timecal/timecal.app.js @@ -0,0 +1,94 @@ +var center = g.getWidth() / 2; +var lastDayDraw; +var lastTimeDraw; + +var fontColor = g.theme.fg; +var accentColor = "#FF0000"; +var locale = require("locale"); + +function loop() { + var d = new Date(); + var cleared = false; + if(lastDayDraw != d.getDate()){ + lastDayDraw = d.getDate(); + drawDate(d); + drawCal(d); + } + + if(lastTimeDraw != d.getMinutes() || cleared){ + lastTimeDraw = d.getMinutes(); + drawTime(d); + } +} +function drawTime(d){ + var hour = ("0" + d.getHours()).slice(-2); + var min = ("0" + d.getMinutes()).slice(-2); + g.setFontAlign(0,-1,0); + g.setFont("Vector",40); + g.setColor(fontColor); + g.clearRect(0,50,g.getWidth(),90); + g.drawString(hour + ":" + min,center,50); +} +function drawDate(d){ + var day = ("0" + d.getDate()).slice(-2); + var month = ("0" + d.getMonth()).slice(-2); + var dateStr = locale.date(d,1); + g.clearRect(0,24,g.getWidth(),44); + g.setFont("Vector",20); + g.setColor(fontColor); + g.setFontAlign(0,-1,0); + g.drawString(dateStr,center,24); +} + +function drawCal(d){ + var calStart = 101; + var cellSize = g.getWidth() / 7; + var halfSize = cellSize / 2; + g.clearRect(0,calStart,g.getWidth(),g.getHeight()); + g.drawLine(0,calStart,g.getWidth(),calStart); + var days = ["Mo","Tu","We","Th","Fr","Sa","Su"]; + g.setFont("Vector",10); + g.setColor(fontColor); + g.setFontAlign(-1,-1,0); + for(var i = 0; i < days.length;i++){ + g.drawString(days[i],i*cellSize+5,calStart -11); + if(i!=0){ + g.drawLine(i*cellSize,calStart,i*cellSize,g.getHeight()); + } + } + var cellHeight = (g.getHeight() -calStart ) / 3; + for(var i = 0;i < 3;i++){ + var starty = calStart + i * cellHeight; + g.drawLine(0,starty,g.getWidth(),starty); + } + + g.setFont("Vector",15); + + var dayOfWeek = d.getDay(); + var dayRem = d.getDay() - 1; + if(dayRem <0){ + dayRem = 0; + } + + var start = new Date(); + start.setDate(start.getDate()-(7+dayRem)); + g.setFontAlign(0,-1,0); + for (var y = 0;y < 3; y++){ + for(var x = 0;x < 7; x++){ + if(start.getDate() === d.getDate()){ + g.setColor(accentColor); + }else{ + g.setColor(fontColor); + } + g.drawString(start.getDate(),x*cellSize +(cellSize / 2) + 2,calStart+(cellHeight*y) + 5); + start.setDate(start.getDate()+1); + } + } +} + +g.clear(); +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +loop(); +setInterval(loop,1000); \ No newline at end of file diff --git a/apps/toucher/settings.js b/apps/toucher/settings.js index 6f7320513..51275d846 100644 --- a/apps/toucher/settings.js +++ b/apps/toucher/settings.js @@ -9,7 +9,7 @@ highres: true, animation : true, frame : 3, - debug: true + debug: false }; } @@ -56,4 +56,4 @@ }, '< Back': back }); -}); \ No newline at end of file +}); diff --git a/apps/vectorclock/bangle1-vector-clock-screenshot.png b/apps/vectorclock/bangle1-vector-clock-screenshot.png new file mode 100644 index 000000000..9a98a8782 Binary files /dev/null and b/apps/vectorclock/bangle1-vector-clock-screenshot.png differ diff --git a/apps/verticalface/bangle1-vertical-watch-face-screenshot.png b/apps/verticalface/bangle1-vertical-watch-face-screenshot.png new file mode 100644 index 000000000..044d194bc Binary files /dev/null and b/apps/verticalface/bangle1-vertical-watch-face-screenshot.png differ diff --git a/apps/vibrclock/bangle1-vibrate-clock-screenshot.png b/apps/vibrclock/bangle1-vibrate-clock-screenshot.png new file mode 100644 index 000000000..da135dc7d Binary files /dev/null and b/apps/vibrclock/bangle1-vibrate-clock-screenshot.png differ diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index 8f997a83e..c1a0504a4 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -6,4 +6,5 @@ 0.07: Add theme support and unknown icon. 0.08: Refactor and reduce widget ram usage. 0.09: Fix crash when weather.json is absent. -0.10: Use new Layout library \ No newline at end of file +0.10: Use new Layout library +0.11: Bangle.js 2 support diff --git a/apps/weather/app.js b/apps/weather/app.js index 6a0852f81..8c8526fbd 100644 --- a/apps/weather/app.js +++ b/apps/weather/app.js @@ -53,8 +53,8 @@ function draw() { layout.hum.label = current.hum+"%"; const wind = locale.speed(current.wind).match(/^(\D*\d*)(.*)$/); layout.wind.label = wind[1]; - layout.windUnit.label = wind[2] + " " + current.wrose.toUpperCase(); - layout.cond.label = current.txt.charAt(0).toUpperCase()+current.txt.slice(1); + layout.windUnit.label = wind[2] + " " + (current.wrose||'').toUpperCase(); + layout.cond.label = current.txt.charAt(0).toUpperCase()+(current.txt||'').slice(1); layout.loc.label = current.loc; layout.updateTime.label = `${formatDuration(Date.now() - current.time)} ago`; layout.update(); diff --git a/apps/weather/lib.js b/apps/weather/lib.js index 299009e74..76ed2aaa4 100644 --- a/apps/weather/lib.js +++ b/apps/weather/lib.js @@ -1,4 +1,5 @@ const storage = require('Storage'); +const B2 = process.env.HWVERSION===2; let expiryTimeout; function scheduleExpiry(json) { @@ -54,13 +55,13 @@ scheduleExpiry(storage.readJSON('weather.json')||{}); exports.drawIcon = function(cond, x, y, r) { function drawSun(x, y, r) { - g.setColor(g.theme.dark ? "#FE0" : "#FC0"); + g.setColor(B2 ? '#FF0' : (g.theme.dark ? "#FE0" : "#FC0")); g.fillCircle(x, y, r); } function drawCloud(x, y, r, c) { const u = r/12; - if (c==null) c = g.theme.dark ? "#BBB" : "#AAA"; + if (c==null) c = B2 ? '#FFF': (g.theme.dark ? "#BBB" : "#AAA"); g.setColor(c); g.fillCircle(x-8*u, y+3*u, 4*u); g.fillCircle(x-4*u, y-2*u, 5*u); @@ -77,7 +78,7 @@ exports.drawIcon = function(cond, x, y, r) { } function drawBrokenClouds(x, y, r) { - drawCloud(x+1/8*r, y-1/8*r, 7/8*r, "#777"); + drawCloud(x+1/8*r, y-1/8*r, 7/8*r, "#777"); // dithers on B2, but that's ok drawCloud(x-1/8*r, y+1/8*r, 7/8*r); } @@ -87,23 +88,23 @@ exports.drawIcon = function(cond, x, y, r) { } function drawRainLines(x, y, r) { - g.setColor(g.theme.dark ? "#0CF" : "#07F"); + g.setColor(B2 ? '#0FF' : (g.theme.dark ? "#0CF" : "#07F")); const y1 = y+1/2*r; const y2 = y+1*r; - - g.fillPolyAA([ + const poly = g.fillPolyAA ? p => g.fillPolyAA(p) : p => g.fillPoly(p); + poly([ x-6/12*r, y1, x-8/12*r, y2, x-7/12*r, y2, x-5/12*r, y1, ]); - g.fillPolyAA([ + poly([ x-2/12*r, y1, x-4/12*r, y2, x-3/12*r, y2, x-1/12*r, y1, ]); - g.fillPolyAA([ + poly([ x+2/12*r, y1, x+0/12*r, y2, x+1/12*r, y2, @@ -123,7 +124,7 @@ exports.drawIcon = function(cond, x, y, r) { function drawThunderstorm(x, y, r) { function drawLightning(x, y, r) { - g.setColor(g.theme.dark ? "#FE0" : "#FC0"); + g.setColor(B2 ? '#FF0' : (g.theme.dark ? "#FE0" : "#FC0")); g.fillPoly([ x-2/6*r, y-r, x-4/6*r, y+1/6*r, @@ -151,7 +152,7 @@ exports.drawIcon = function(cond, x, y, r) { } } - g.setColor(g.theme.dark ? "#FFF" : "#CCC"); + g.setColor(B2 ? '#FFF' : (g.theme.dark ? "#FFF" : "#CCC")); const w = 1/12*r; for(let i = 0; i<=6; ++i) { const points = [ @@ -186,7 +187,7 @@ exports.drawIcon = function(cond, x, y, r) { [-0.2, 0.3], ]; - g.setColor(g.theme.dark ? "#FFF" : "#CCC"); + g.setColor(B2 ? '#FFF' : (g.theme.dark ? "#FFF" : "#CCC")); for(let i = 0; i<5; ++i) { g.fillRect(x+layers[i][0]*r, y+(0.4*i-0.9)*r, x+layers[i][1]*r, y+(0.4*i-0.7)*r-1); @@ -196,7 +197,7 @@ exports.drawIcon = function(cond, x, y, r) { } function drawUnknown(x, y, r) { - drawCloud(x, y, r, "#777"); + drawCloud(x, y, r, "#777"); // dithers on B2, but that's ok g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6); } diff --git a/apps/weatherClock/ChangeLog b/apps/weatherClock/ChangeLog new file mode 100644 index 000000000..f4a63e976 --- /dev/null +++ b/apps/weatherClock/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Minor layout format tweak so it uses less memory and draws ok on Bangle.js 1 (#1012) diff --git a/apps/weatherClock/README.md b/apps/weatherClock/README.md new file mode 100644 index 000000000..f1f146440 --- /dev/null +++ b/apps/weatherClock/README.md @@ -0,0 +1,19 @@ +# Weather Clock + +A clock which displays the current weather conditions. Temperature, wind speed, and an icon indicating the weather conditions are displayed. + +Standard widgets are displayed. + +## Requirements + +**This clock requires Gadgetbridge and the weather app in order to get weather data!** + +See the [Bangle.js Gadgetbridge documentation](https://www.espruino.com/Gadgetbridge) for instructions on setting up Gadgetbridge and weather. + +![Screenshot](screens/screen1.png) + +![Screenshot2](screens/screen2.png) + +## Creator + +James Gough diff --git a/apps/weatherClock/app-icon.js b/apps/weatherClock/app-icon.js new file mode 100644 index 000000000..e289f6c8b --- /dev/null +++ b/apps/weatherClock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkE/4A/AH4A/ADEzAgk/AogACn/zAgYLBmMACAQEBgMvAwUQgAABj8wAgUD//yAgIQFBQIXFl4XFmETC4QcBn8xgQXDGgYACmAXBmQMBC4UB//zAQJoBC4JmE/8gj4kDC48Qn8QJAIxEBIIXFCgM/+IXELIIGBBwYcEAYcBCARHCiczNIINB+JrDC4inBFAjKCLQZ2CC4zRCHYgXFkBkFKAUSPQYABkJEBVQZ2DmUigEikfzgESiA7BkU/mEBkJfCOwjdFL4QnBEwL8CHIJ2F+ciAAI6CmUjAYRmCAwQSBbgoA/AG3zXoMvkMv+Uj+U/mcxBYP/kciTQMimKRCBYPyj/xXIMydYMvmKqBUYIWBkcyC4bmBmMT+P/C4Uzn/xEYXziYXBkYnBbAczmfyC4MT+bWBkMiiQKBiUyAoMhj6RzMgYASn3dmazIAYYmG/ve93dC48iCYMzAYQXF93u9oXIl4uBAQIAFCwIAB74XKkYvGIwIXJYQIDCC43+I4QKFAoh6Cn4IEO4RfF+QtDGQcTMQn97oABC4si+cRkcikIUBAQQAE6a8GiXzkUykUiZgMjj7gNEwMzXoLWCL4oANCSQAl")) \ No newline at end of file diff --git a/apps/weatherClock/app.js b/apps/weatherClock/app.js new file mode 100644 index 000000000..46cc38312 --- /dev/null +++ b/apps/weatherClock/app.js @@ -0,0 +1,134 @@ +const Layout = require("Layout"); +const storage = require('Storage'); +const locale = require("locale"); + +// weather icons from https://icons8.com/icon/set/weather/color +var sunIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AbhvQC6vd7ouVC4IwUCwIwUFwQwQCYgAHDZQXc9wACC6QWDDAgXN7wXF9oXPCwowDC5guGGAYXMCw4wCC5RGJJAZGTJBiNISIylQVJrLCC5owGF65fXR7AwBC5jvhC7JIILxapDFxAXOGAy9KC4owGBAQXODAgHDC54AHC8T0FAAQSOGg4qPGA4WUGAIuVC7AA/AH4AEA=")); + +var partSunIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AY6AWVhvdC6vd7owUFwIABFiYAFGR4Xa93u9oXTCwIYDC6HeC4fuC56MBC4ySOIwpIQXYQXHmYABRpwXECwQYKF5HjC4kwL5gQCAYYwO7wqFAAowK7wWKJBgXLJBPd6YX/AAoVMAAM/Cw0DC5yRHCx5JGFyAwGCyIwFC/4XyR4inXa64wRFwowQCw4A/AH4AkA")); + +var cloudIcon = require("heatshrink").decompress(atob("mEwwhC/AH4A/AH4AtgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAH4A/AH4A/ADg=")); + +var snowIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AhxGAC9YUBC4QZRhAVBAIWIC6QAEI6IYEI5cIBgwWOC64NCKohHPNox3RBgqnQEo7XPHpKONR5AXYAH4ASLa4XWXILiBC6r5LDBgWWDBRrKC5hsCEacIHawvMCIwvQC5QvQFAROEfZ5ADLJ4YGCywvVI7CPGC9IA/AH4AF")); + +var rainIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AFgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAGeIBJEIwAVJhGIC5AJBC5QMJEJQMEC44JBC6QSCC54FHLxgNBBgYSEDgKpPMhQXneSwuUAH4A/AA4=")); + +var stormIcon = require("heatshrink").decompress(atob("mEwwhC/AFEzmcwCyoYUgYXDmYuVGAY0OFwocHC6pNLCxYXYJBQXuCxhhJRpgYKCyBKFFyIXFCyJIFC/4XaO66nU3eza6k7C4IWFGBwXBCwwwO3ewC5AZMC6RaCIxZiI3e7AYYwRCQIIBC4QwPIQIpDC5owDhYREIxgAEFIouNC4orDFyBGBGAcLC6BaFhYWRLSRIFISQXcCyqhRAH4Az")); + +// err icon - https://icons8.com/icons/set/error +var errIcon = require("heatshrink").decompress(atob("mEwwkBiIA/AH4AZUAIWUiAXBWqgXXdIYuVGCgXBgICCIyYXCJCQTDC6QrEMCQSEJCQRFC6ApGJCCiDDQSpQFAYXEJBqNGJCA/EC4ZIOEwgXFJBgNEAhKlNAgxIKBgoXEJBjsLC5TsIeRycMBhRrMMBKzQEozjOBxAgHGww+IA6wfSH4hnIC47OMSJqlRIJAXCACIXaGoQARPwwuTAH4A/ABw")); + +/** +Choose weather icon to display based on condition. +Based on function from the Bangle weather app so it should handle all of the conditions +sent from gadget bridge. +*/ +function chooseIcon(condition) { + condition = condition.toLowerCase(); + if (condition.includes("thunderstorm")) return stormIcon; + if (condition.includes("freezing")||condition.includes("snow")|| + condition.includes("sleet")) { + return snowIcon; + } + if (condition.includes("drizzle")|| + condition.includes("shower")) { + return rainIcon; + } + if (condition.includes("rain")) return rainIcon; + if (condition.includes("clear")) return sunIcon; + if (condition.includes("few clouds")) return partSunIcon; + if (condition.includes("scattered clouds")) return cloudIcon; + if (condition.includes("clouds")) return cloudIcon; + if (condition.includes("mist") || + condition.includes("smoke") || + condition.includes("haze") || + condition.includes("sand") || + condition.includes("dust") || + condition.includes("fog") || + condition.includes("ash") || + condition.includes("squalls") || + condition.includes("tornado")) { + return cloudIcon; + } + return cloudIcon; +} + +/** +Get weather stored in json file by weather app. +*/ +function getWeather() { + let jsonWeather = storage.readJSON('weather.json'); + return jsonWeather; +} + +var clockLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"35%", halign: 0, fillx:1, pad: 8, label:"00:00", id:"time" }, + {type: "h", fillx: 1, c: [ + {type:"txt", font:"10%", label:"THU", id:"dow" }, + {type:"txt", font:"10%", label:"01/01/1970", id:"date" } + ] + }, + {type: "h", valign : 1, fillx:1, c: [ + {type: "img", filly: 1, id: "weatherIcon", src: sunIcon}, + {type: "v", fillx:1, c: [ + {type: "h", c: [ + {type: "txt", font: "10%", id: "temp", label: "000"}, + {type: "txt", font: "10%", id: "tempUnit", label: "°C"}, + ]}, + {type: "h", c: [ + {type: "txt", font: "10%", id: "wind", label: "00"}, + {type: "txt", font: "10%", id: "windUnit", label: "km/h"}, + ]} + ] + }, + ]}] +}); + +// 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 draw() { + var date = new Date(); + clockLayout.time.label = locale.time(date, 1); + clockLayout.date.label = locale.date(date, 1).toUpperCase(); + clockLayout.dow.label = locale.dow(date, 1).toUpperCase(); + var weatherJson = getWeather(); + if(weatherJson && weatherJson.weather){ + var currentWeather = weatherJson.weather; + const temp = locale.temp(currentWeather.temp-273.15).match(/^(\D*\d*)(.*)$/); + clockLayout.temp.label = temp[1]; + clockLayout.tempUnit.label = temp[2]; + clockLayout.weatherIcon.src = chooseIcon(currentWeather.txt); + const wind = locale.speed(currentWeather.wind).match(/^(\D*\d*)(.*)$/); + clockLayout.wind.label = wind[1] + " "; + clockLayout.windUnit.label = wind[2] + " " + (currentWeather.wrose||'').toUpperCase(); + } + else{ + clockLayout.temp.label = "Err"; + clockLayout.tempUnit.label = ""; + clockLayout.wind.label = "No Data"; + clockLayout.windUnit.label = ""; + clockLayout.weatherIcon.src = errIcon; + } + clockLayout.clear(); + clockLayout.render(); + // queue draw in one minute + queueDraw(); +} + +g.clear(); +Bangle.setUI("clock"); // Show launcher when middle button pressed +Bangle.loadWidgets(); +Bangle.drawWidgets(); +clockLayout.render(); +draw(); diff --git a/apps/weatherClock/app.png b/apps/weatherClock/app.png new file mode 100644 index 000000000..434811541 Binary files /dev/null and b/apps/weatherClock/app.png differ diff --git a/apps/weatherClock/icons/icons8-cloud-lightning-48.png b/apps/weatherClock/icons/icons8-cloud-lightning-48.png new file mode 100644 index 000000000..7ae0cd4a9 Binary files /dev/null and b/apps/weatherClock/icons/icons8-cloud-lightning-48.png differ diff --git a/apps/weatherClock/icons/icons8-clouds-48.png b/apps/weatherClock/icons/icons8-clouds-48.png new file mode 100644 index 000000000..7b7533c51 Binary files /dev/null and b/apps/weatherClock/icons/icons8-clouds-48.png differ diff --git a/apps/weatherClock/icons/icons8-error-48.png b/apps/weatherClock/icons/icons8-error-48.png new file mode 100644 index 000000000..b45640f31 Binary files /dev/null and b/apps/weatherClock/icons/icons8-error-48.png differ diff --git a/apps/weatherClock/icons/icons8-partly-cloudy-day-48.png b/apps/weatherClock/icons/icons8-partly-cloudy-day-48.png new file mode 100644 index 000000000..e83f55148 Binary files /dev/null and b/apps/weatherClock/icons/icons8-partly-cloudy-day-48.png differ diff --git a/apps/weatherClock/icons/icons8-rain-48.png b/apps/weatherClock/icons/icons8-rain-48.png new file mode 100644 index 000000000..690efd82c Binary files /dev/null and b/apps/weatherClock/icons/icons8-rain-48.png differ diff --git a/apps/weatherClock/icons/icons8-snow-storm-48.png b/apps/weatherClock/icons/icons8-snow-storm-48.png new file mode 100644 index 000000000..e5ff7dd11 Binary files /dev/null and b/apps/weatherClock/icons/icons8-snow-storm-48.png differ diff --git a/apps/weatherClock/icons/icons8-sun-48.png b/apps/weatherClock/icons/icons8-sun-48.png new file mode 100644 index 000000000..4933d6721 Binary files /dev/null and b/apps/weatherClock/icons/icons8-sun-48.png differ diff --git a/apps/weatherClock/screens/screen1.png b/apps/weatherClock/screens/screen1.png new file mode 100644 index 000000000..bb54e4ef7 Binary files /dev/null and b/apps/weatherClock/screens/screen1.png differ diff --git a/apps/weatherClock/screens/screen2.png b/apps/weatherClock/screens/screen2.png new file mode 100644 index 000000000..21b814dc7 Binary files /dev/null and b/apps/weatherClock/screens/screen2.png differ diff --git a/apps/welcome/ChangeLog b/apps/welcome/ChangeLog index f72f77a4b..8e2f99b9a 100644 --- a/apps/welcome/ChangeLog +++ b/apps/welcome/ChangeLog @@ -15,3 +15,4 @@ 0.11: Fix initial screen fill colour 0.12: Fix swipe direction (#800) 0.13: Mods for Bangle.js 2 +0.14: Turn off and run later to use softOff to time is set right diff --git a/apps/welcome/app-bangle2.js b/apps/welcome/app-bangle2.js index 93d1c5657..41d051148 100644 --- a/apps/welcome/app-bangle2.js +++ b/apps/welcome/app-bangle2.js @@ -244,5 +244,6 @@ setWatch(()=>{ }, BTN1, {repeat:true}); Bangle.setLCDTimeout(0); +Bangle.setLocked(0); Bangle.setLCDPower(1); move(0); diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js index f269f238e..27a322c7f 100644 --- a/apps/welcome/settings.js +++ b/apps/welcome/settings.js @@ -11,7 +11,8 @@ 'Run Now': () => load('welcome.app.js'), 'Turn off & run next': () => { require('Storage').write('welcome.json', {welcomed: false}); - Bangle.off(); + Bangle.setLocked(true); // fix for pre-2v11 firmware that can accidentally leave touchscreen on + if (Bangle.softOff()) Bangle.softOff(); else Bangle.off(); }, '< Back': back, }) diff --git a/apps/widChargingStatus/ChangeLog b/apps/widChargingStatus/ChangeLog new file mode 100644 index 000000000..d3175e1ab --- /dev/null +++ b/apps/widChargingStatus/ChangeLog @@ -0,0 +1 @@ +0.1: First release. \ No newline at end of file diff --git a/apps/widChargingStatus/widget.js b/apps/widChargingStatus/widget.js new file mode 100644 index 000000000..90f9199fa --- /dev/null +++ b/apps/widChargingStatus/widget.js @@ -0,0 +1,31 @@ +(() => { + const icon = require("heatshrink").decompress(atob("ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA")); + const iconWidth = 18; + + function draw() { + g.reset(); + if (Bangle.isCharging()) { + g.setColor("#FD0"); + g.drawImage(icon, this.x + 1, this.y + 1, { + scale: 0.6875 + }); + } + } + + WIDGETS.chargingStatus = { + area: 'tr', + width: Bangle.isCharging() ? iconWidth : 0, + draw: draw, + }; + + Bangle.on('charging', (charging) => { + if (charging) { + Bangle.buzz(); + WIDGETS.chargingStatus.width = iconWidth; + } else { + WIDGETS.chargingStatus.width = 0; + } + Bangle.drawWidgets(); // re-layout widgets + g.flip(); + }); +})(); \ No newline at end of file diff --git a/apps/widChargingStatus/widget.png b/apps/widChargingStatus/widget.png new file mode 100644 index 000000000..0097d45ef Binary files /dev/null and b/apps/widChargingStatus/widget.png differ diff --git a/apps/wid_a_battery_widget/ChangeLog b/apps/wid_a_battery_widget/ChangeLog new file mode 100644 index 000000000..9b0649c27 --- /dev/null +++ b/apps/wid_a_battery_widget/ChangeLog @@ -0,0 +1,2 @@ +1.00: Release for Bangle 2 (2021/11/18) +1.01: Internal id update to wid_* as per Gordon's request (2021/11/21) diff --git a/apps/a_battery_widget/README.md b/apps/wid_a_battery_widget/README.md similarity index 100% rename from apps/a_battery_widget/README.md rename to apps/wid_a_battery_widget/README.md diff --git a/apps/a_battery_widget/a_battery_widget-pic.jpg b/apps/wid_a_battery_widget/a_battery_widget-pic.jpg similarity index 100% rename from apps/a_battery_widget/a_battery_widget-pic.jpg rename to apps/wid_a_battery_widget/a_battery_widget-pic.jpg diff --git a/apps/a_battery_widget/widget.js b/apps/wid_a_battery_widget/widget.js similarity index 88% rename from apps/a_battery_widget/widget.js rename to apps/wid_a_battery_widget/widget.js index d6f8236d4..9fb06e320 100644 --- a/apps/a_battery_widget/widget.js +++ b/apps/wid_a_battery_widget/widget.js @@ -39,7 +39,7 @@ } Bangle.on('charging',function(charging) { draw(); }); - setInterval(()=>WIDGETS["a_battery_widget"].draw(), 60000); + setInterval(()=>WIDGETS["wid_a_battery_widget"].draw(), 60000); - WIDGETS["a_battery_widget"]={area:"tr",width:30,draw:draw}; + WIDGETS["wid_a_battery_widget"]={area:"tr",width:30,draw:draw}; })(); diff --git a/apps/a_battery_widget/widget.png b/apps/wid_a_battery_widget/widget.png similarity index 100% rename from apps/a_battery_widget/widget.png rename to apps/wid_a_battery_widget/widget.png diff --git a/apps/widbars/ChangeLog b/apps/widbars/ChangeLog new file mode 100644 index 000000000..4c21f3ace --- /dev/null +++ b/apps/widbars/ChangeLog @@ -0,0 +1 @@ +0.01: New Widget! diff --git a/apps/widbars/README.md b/apps/widbars/README.md new file mode 100644 index 000000000..c1cb73a96 --- /dev/null +++ b/apps/widbars/README.md @@ -0,0 +1,15 @@ +# Bars Widget + +A simple widget that display several measurements as vertical bars. + +![Screenshot](screenshot.png) + +## Measurements from left to right: + +- Flash storage space used (*blue/cyan*) +- Memory usage (*magenta*) +- Battery charge (*green*) \ No newline at end of file diff --git a/apps/widbars/icon.png b/apps/widbars/icon.png new file mode 100644 index 000000000..3d6fcb053 Binary files /dev/null and b/apps/widbars/icon.png differ diff --git a/apps/widbars/screenshot.png b/apps/widbars/screenshot.png new file mode 100644 index 000000000..ae85e42f5 Binary files /dev/null and b/apps/widbars/screenshot.png differ diff --git a/apps/widbars/widget.js b/apps/widbars/widget.js new file mode 100644 index 000000000..a1134f31f --- /dev/null +++ b/apps/widbars/widget.js @@ -0,0 +1,67 @@ +(() => { + const h=24, // widget height + w=3, // width of single bar + bars=3; // number of bars + + // Note: HRM/temperature are commented out (they didn't seem very useful) + // If re-adding them, also adjust `bars` + + // ==HRM start== + // // We show HRM if available, but don't turn it on + // let bpm,rst,con=10; // always ignore HRM with confidence below 10% + // function noHrm() { // last value is no longer valid + // if (rst) clearTimeout(rst); + // rst=bpm=undefined; con=10; + // WIDGETS["bars"].draw(); + // } + // Bangle.on('HRM', hrm=>{ + // if (hrm.confidence>con || hrm.confidence>=80) { + // bpm=hrm.confidence; + // con=hrm.confidence; + // WIDGETS["bars"].draw(); + // if (rst) clearTimeout(rst); + // rst = setTimeout(noHrm, 10*60*1000); // forget HRM after 10 minutes + // } + // }); + // ==HRM end== + + /** + * Draw a bar + * + * @param {int} x left + * @param {int} y top (of full bar) + * @param {string} col Color + * @param {number} f Fraction of bar to draw + */ + function bar(x,y, col,f) { + if (!f) f = 0; // for f=NaN: set it to 0 -> don't even draw the bottom pixel + if (f>1) f = 1; + if (f<0) f = 0; + const top = Math.round((h-1)*(1-f)); + // use Math.min/max to make sure we stay within widget boundaries for f=0/f=1 + if (top) g .clearRect(x,y, x+w-1,y+top-1); // erase above bar + if (f) g.setColor(col).fillRect(x,y+top, x+w-1,y+h-1); // even for f=0.001 this is still 1 pixel high + } + function draw() { + g.reset(); + const x = this.x, y = this.y, + m = process.memory(); + let b=0; + // ==HRM== bar(x+(w*b++),y,'#f00'/*red */,bpm/200); // >200 seems very unhealthy; if we have no valid bpm this will just be empty space + // ==Temperature== bar(x+(w*b++),y,'#ff0'/*yellow */,E.getTemperature()/50); // you really don't want to wear a watch that's hotter than 50°C + bar(x+(w*b++),y,g.theme.dark?'#0ff':'#00f'/*cyan/blue*/,1-(require('Storage').getFree() / process.env.STORAGE)); + bar(x+(w*b++),y,'#f0f'/*magenta*/,m.usage/m.total); + bar(x+(w*b++),y,'#0f0'/*green */,E.getBattery()/100); + } + + let redraw; + Bangle.on('lcdPower', on => { + if (redraw) clearInterval(redraw) + redraw = undefined; + if (on) { + WIDGETS["bars"].draw(); + redraw = setInterval(()=>WIDGETS["bars"].draw, 10*1000); // redraw every 10 seconds + } + }); + WIDGETS["bars"]={area:"tr",width: bars*w,draw:draw}; +})() diff --git a/apps/widbata/ChangeLog b/apps/widbata/ChangeLog new file mode 100644 index 000000000..51575d3b4 --- /dev/null +++ b/apps/widbata/ChangeLog @@ -0,0 +1 @@ +0.01: Created diff --git a/apps/widbata/README.md b/apps/widbata/README.md new file mode 100644 index 000000000..6c3012793 --- /dev/null +++ b/apps/widbata/README.md @@ -0,0 +1,14 @@ +# Battery Level Widget (Themed) + +Shows the current battery level status in the top right using the clocks colour theme + +* Works with Bangle 2 +* Simple design, no settings +* 27 pixels wide +* Uses current colour theme to match clock + +![](screenshot_widbata_1.png) +![](screenshot_widbata_2.png) +![](screenshot_widbata_3.png) + +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/widbata/screenshot_widbata_1.png b/apps/widbata/screenshot_widbata_1.png new file mode 100644 index 000000000..5fdc3ac3d Binary files /dev/null and b/apps/widbata/screenshot_widbata_1.png differ diff --git a/apps/widbata/screenshot_widbata_2.png b/apps/widbata/screenshot_widbata_2.png new file mode 100644 index 000000000..6a6ec8581 Binary files /dev/null and b/apps/widbata/screenshot_widbata_2.png differ diff --git a/apps/widbata/screenshot_widbata_3.png b/apps/widbata/screenshot_widbata_3.png new file mode 100644 index 000000000..824309702 Binary files /dev/null and b/apps/widbata/screenshot_widbata_3.png differ diff --git a/apps/widbata/widbata.png b/apps/widbata/widbata.png new file mode 100644 index 000000000..877af1707 Binary files /dev/null and b/apps/widbata/widbata.png differ diff --git a/apps/widbata/widbata.wid.js b/apps/widbata/widbata.wid.js new file mode 100644 index 000000000..1c04bf8ae --- /dev/null +++ b/apps/widbata/widbata.wid.js @@ -0,0 +1,16 @@ +setInterval(()=>WIDGETS["bata"].draw(), 60000); +Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["bata"].draw(); +}); +WIDGETS["bata"]={area:"tr",width:27,draw:function() { + var s = 26; + var t = 13; // thickness + var x = this.x, y = this.y; + g.reset(); + g.setColor(g.theme.fg); + g.fillRect(x,y+2,x+s-4,y+2+t); // outer + g.clearRect(x+2,y+2+2,x+s-4-2,y+2+t-2); // centre + g.setColor(g.theme.fg); + g.fillRect(x+s-3,y+2+(((t - 1)/2)-1),x+s-2,y+2+(((t - 1)/2)-1)+4); // contact + g.fillRect(x+3, y+5, x +4 + E.getBattery()*(s-12)/100, y+t-1); // the level +}}; diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 09e4fabf4..99822b5a9 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -10,3 +10,4 @@ 0.11: Don't overwrite existing settings on app update 0.12: Fixed for Bangle 2 0.13: Fillbar setting added, see README +0.14: Fix drawing the bar when charging diff --git a/apps/widbatpc/README.md b/apps/widbatpc/README.md index c75154f72..48c6070f4 100644 --- a/apps/widbatpc/README.md +++ b/apps/widbatpc/README.md @@ -5,12 +5,12 @@ Show the current battery level and charging status in the top right of the clock Works with Bangle 1 and Bangle 2 When the fillbar setting is on the level colour will fill the entire -bar. This makes for an easier to read dsiplay when the charge is +bar. This makes for an easier to read display when the charge is below 50%. ![](widbatpc.full.jpg) -When the fillbar setting is off the level colour will follow the battry percentage +When the fillbar setting is off the level colour will follow the battery percentage ![](widbatpc.part.jpg) diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index caecf8ae4..3e5ff47b4 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -79,20 +79,20 @@ // else... var s = 39; var x = this.x, y = this.y; - const l = E.getBattery(); - let xl = x+4+l*(s-12)/100; + const l = E.getBattery(), + c = levelColor(l); - // show bar full in the level color, as you cant see the color if the bar is too small - if (setting('fillbar')) - xl = x+4+100*(s-12)/100; - - c = levelColor(l); - if (Bangle.isCharging() && setting('charger')) { g.setColor(chargerColor()).drawImage(atob( "DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); x+=16; } + + let xl = x+4+l*(s-12)/100; + // show bar full in the level color, as you can't see the color if the bar is too small + if (setting('fillbar')) + xl = x+4+100*(s-12)/100; + g.setColor(g.theme.fg); g.fillRect(x,y+2,x+s-4,y+21); g.clearRect(x+2,y+4,x+s-6,y+19); diff --git a/apps/wohrm/bangle1-workout-HRM-screenshot.png b/apps/wohrm/bangle1-workout-HRM-screenshot.png new file mode 100644 index 000000000..3280f310d Binary files /dev/null and b/apps/wohrm/bangle1-workout-HRM-screenshot.png differ diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index a84d26efd..ea45dc19b 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -209,6 +209,8 @@ apps.forEach((app,appIdx) => { // prefer "appid.json" over "appid.settings.json" (TODO: change to ERROR once all apps comply?) if (dataNames.includes(app.id+".settings.json") && !dataNames.includes(app.id+".json")) WARN(`App ${app.id} uses data file ${app.id+'.settings.json'} instead of ${app.id+'.json'}`) + else if (dataNames.includes(app.id+".settings.json")) + WARN(`App ${app.id} uses data file ${app.id+'.settings.json'}`) // settings files should be listed under data, not storage (TODO: change to ERROR once all apps comply?) if (fileNames.includes(app.id+".settings.json")) WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`) diff --git a/core b/core index 59f80bb52..23854083e 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 59f80bb52a38da12cb272f9106cb3951b49dab2e +Subproject commit 23854083e0c3f83c649073a2d85e8079efc471d3 diff --git a/css/main.css b/css/main.css index 82f6fbcfe..f4850babe 100644 --- a/css/main.css +++ b/css/main.css @@ -1,8 +1,52 @@ -.navbar { background-color: #5755d9; padding: 0.5em 1em 0.5em 1em; } +.navbar { background-color: #5755d9; padding: 1em 1em 1em 1em; } .navbar .navbar-brand { color: #fff; font-weight: bold; } + +.container.apploader-tab, ul.tab.tab-block { + padding-left: 1rem; + padding-right: 1rem; + border-bottom: 0px; +} + +.navbar-brand.mr-2 > img { + margin-left: 0.3rem; +} + +.panel-body.columns { + margin: 1px; +} + +.tile.column.col-6.col-sm-12.col-xs-12.app-tile { + border: solid 1px #fafafa; + margin: 0; + min-height: 150px; + padding-top: 0.5rem; +} + +.tab.tab-block .tab-item { + border-bottom: solid 1px #dadee4; +} + +a.mr-2{ + display: flex; + align-items: center; +} + +.navbar-section > a > div { + margin-left: 0.75rem; +} + +.dropdown-container { + margin-bottom: 0.5rem; + margin-top: 0.5rem; +} + +a.btn.btn-link.dropdown-toggle { + padding-left: 0.01em; +} + .avatar img { border-radius: 5px 5px 5px 5px; background: #fff; @@ -64,7 +108,9 @@ .icon.icon-favourite-active::before { content: "\02665"; /* 0x2665 = solid heart; 0x2605 = solid star */ } -.icon.icon-interface {text-indent: 0px;font-size: 130%;vertical-align: -30%;} /*override spectre*/ +.icon.icon-interface {text-indent: 0px;} /*override spectre*/ .icon.icon-interface::before { - content: "\01F5AB"; + position: absolute; left: 50%; top: 70%; + transform: translate(-50%,-50%); + content: url("data:image/svg+xml,%3C%3Fxml version='1.0'%3F%3E%3Csvg fill='rgb(87, 85, 217)' xmlns='http://www.w3.org/2000/svg' viewBox='2 2 28 28' width='1.5em' height='1.5em'%3E%3Cpath d='M 6 4 C 4.895 4 4 4.895 4 6 L 4 24 C 4 25.105 4.895 26 6 26 L 24 26 C 25.105 26 26 25.105 26 24 L 26 8 L 22 4 L 20 4 L 20 10 C 20 10.552 19.552 11 19 11 L 10 11 C 9.448 11 9 10.552 9 10 L 9 4 L 6 4 z M 16 4 L 16 9 L 18 9 L 18 4 L 16 4 z M 10 16 L 20 16 C 21.105 16 22 16.895 22 18 L 22 24 L 8 24 L 8 18 C 8 16.895 8.895 16 10 16 z'/%3E%3C/svg%3E"); } diff --git a/index.html b/index.html index e7c7c31cd..e22a1f9e7 100644 --- a/index.html +++ b/index.html @@ -21,8 +21,9 @@
/ 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