From 2d256f23c5e9a2af969dca79d8eb0841e0bc7926 Mon Sep 17 00:00:00 2001 From: lauzonhomeschool <85599144+lauzonhomeschool@users.noreply.github.com> Date: Sun, 28 Apr 2024 22:59:53 -0400 Subject: [PATCH] [datetime_picker] create app and use it in alarm app --- apps/alarm/ChangeLog | 1 + apps/alarm/README.md | 2 +- apps/alarm/app.js | 112 +++++++++++++-------- apps/alarm/metadata.json | 2 +- apps/datetime_picker/ChangeLog | 1 + apps/datetime_picker/README.md | 9 ++ apps/datetime_picker/app-icon.js | 1 + apps/datetime_picker/app.js | 5 + apps/datetime_picker/app.png | Bin 0 -> 1679 bytes apps/datetime_picker/lib.js | 145 ++++++++++++++++++++++++++++ apps/datetime_picker/metadata.json | 16 +++ apps/datetime_picker/screenshot.png | Bin 0 -> 2430 bytes 12 files changed, 252 insertions(+), 42 deletions(-) create mode 100644 apps/datetime_picker/ChangeLog create mode 100644 apps/datetime_picker/README.md create mode 100644 apps/datetime_picker/app-icon.js create mode 100644 apps/datetime_picker/app.js create mode 100644 apps/datetime_picker/app.png create mode 100644 apps/datetime_picker/lib.js create mode 100644 apps/datetime_picker/metadata.json create mode 100644 apps/datetime_picker/screenshot.png diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 9cf5972c4..a3a5dfc1c 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -50,3 +50,4 @@ 0.45: Fix new alarm when selectedAlarm is undefined 0.46: Show alarm groups if the Show Group setting is ON. Scroll alarms menu back to previous position when getting back to it. 0.47: Fix wrap around when snoozed through midnight +0.48: Use datetimeinput for Events, if available. Scroll back when getting out of group. Menu date format setting for shorter dates on current year. diff --git a/apps/alarm/README.md b/apps/alarm/README.md index 43f72665e..77aa61d2c 100644 --- a/apps/alarm/README.md +++ b/apps/alarm/README.md @@ -2,7 +2,7 @@ This app allows you to add/modify any alarms, timers and events. -Optional: When a keyboard app is detected, you can add a message to display when any of these is triggered. +Optional: When a keyboard app is detected, you can add a message to display when any of these is triggered. If a datetime input app (e.g. datetime_picker) is detected, it will be used for the selection of the date+time of events. It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps. diff --git a/apps/alarm/app.js b/apps/alarm/app.js index 3f9aaba68..64f233c5c 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -50,13 +50,17 @@ function handleFirstDayOfWeek(dow) { alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow)); function getLabel(e) { - const dateStr = e.date && require("locale").date(new Date(e.date), 1); + const dateStr = getDateText(e.date); return (e.timer ? require("time_utils").formatDuration(e.timer) : (dateStr ? `${dateStr}${e.rp?"*":""} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeRepeat(e)}` : "")) ) + (e.msg ? ` ${e.msg}` : ""); } +function getDateText(d) { + return d && (settings.menuDateFormat === "mmdd" ? d.substring(d.startsWith(new Date().getFullYear()) ? 5 : 0) : require("locale").date(new Date(d), 1)); +} + function trimLabel(label, maxLength) { if(settings.showOverflow) return label; return (label.length > maxLength @@ -75,10 +79,10 @@ function formatAlarmProperty(msg) { } } -function showMainMenu(scroll, group) { +function showMainMenu(scroll, group, scrollback) { const menu = { "": { "title": group || /*LANG*/"Alarms & Timers", scroll: scroll }, - "< Back": () => group ? showMainMenu() : load(), + "< Back": () => group ? showMainMenu(scrollback) : load(), /*LANG*/"New...": () => showNewMenu(group) }; const getGroups = settings.showGroup && !group; @@ -98,7 +102,7 @@ function showMainMenu(scroll, group) { }); if (!group) { - Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g)); + Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g, scroller.scroll)); menu[/*LANG*/"Advanced"] = () => showAdvancedMenu(); } @@ -119,6 +123,7 @@ function showNewMenu(group) { } function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate, scroll, group) { + console.log(scroll); var isNew = alarmIndex === undefined; var alarm = require("sched").newDefaultAlarm(); @@ -138,6 +143,8 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate, scroll, group) { var title = date ? (isNew ? /*LANG*/"New Event" : /*LANG*/"Edit Event") : (isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm"); var keyboard = "textinput"; try {keyboard = require(keyboard);} catch(e) {keyboard = null;} + var datetimeinput; + try {datetimeinput = require("datetimeinput");} catch(e) {datetimeinput = null;} const menu = { "": { "title": title }, @@ -145,41 +152,66 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate, scroll, group) { prepareAlarmForSave(alarm, alarmIndex, time, date); saveAndReload(); showMainMenu(scroll, group); - }, - /*LANG*/"Hour": { - value: time.h, - format: v => ("0" + v).substr(-2), - min: 0, - max: 23, - wrap: true, - onchange: v => time.h = v - }, - /*LANG*/"Minute": { - value: time.m, - format: v => ("0" + v).substr(-2), - min: 0, - max: 59, - wrap: true, - onchange: v => time.m = v - }, - /*LANG*/"Day": { - value: date ? date.getDate() : null, - min: 1, - max: 31, - wrap: true, - onchange: v => date.setDate(v) - }, - /*LANG*/"Month": { - value: date ? date.getMonth() + 1 : null, - format: v => require("date_utils").month(v), - onchange: v => date.setMonth((v+11)%12) - }, - /*LANG*/"Year": { - value: date ? date.getFullYear() : null, - min: new Date().getFullYear(), - max: 2100, - onchange: v => date.setFullYear(v) - }, + } + }; + + if (alarm.date && datetimeinput) { + menu[`${getDateText(date.toLocalISOString().slice(0,10))} ${require("time_utils").formatTime(time)}`] = { + value: date, + format: v => "", + onchange: v => { + setTimeout(() => { + var datetime = new Date(v.getTime()); + datetime.setHours(time.h, time.m); + datetimeinput.input({datetime}).then(result => { + time.h = result.getHours(); + time.m = result.getMinutes(); + prepareAlarmForSave(alarm, alarmIndex, time, result, true); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group); + }); + }, 100); + } + }; + } else { + Object.assign(menu, { + /*LANG*/"Hour": { + value: time.h, + format: v => ("0" + v).substr(-2), + min: 0, + max: 23, + wrap: true, + onchange: v => time.h = v + }, + /*LANG*/"Minute": { + value: time.m, + format: v => ("0" + v).substr(-2), + min: 0, + max: 59, + wrap: true, + onchange: v => time.m = v + }, + /*LANG*/"Day": { + value: date ? date.getDate() : null, + min: 1, + max: 31, + wrap: true, + onchange: v => date.setDate(v) + }, + /*LANG*/"Month": { + value: date ? date.getMonth() + 1 : null, + format: v => require("date_utils").month(v), + onchange: v => date.setMonth((v+11)%12) + }, + /*LANG*/"Year": { + value: date ? date.getFullYear() : null, + min: new Date().getFullYear(), + max: 2100, + onchange: v => date.setFullYear(v) + } + }); + } + + Object.assign(menu, { /*LANG*/"Message": { value: alarm.msg, format: formatAlarmProperty, @@ -241,7 +273,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate, scroll, group) { saveAndReload(); showMainMenu(scroll, group); } - }; + }); if (!keyboard) delete menu[/*LANG*/"Message"]; if (!keyboard || !settings.showGroup) delete menu[/*LANG*/"Group"]; diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json index 20969e6df..78cd4bd4e 100644 --- a/apps/alarm/metadata.json +++ b/apps/alarm/metadata.json @@ -2,7 +2,7 @@ "id": "alarm", "name": "Alarms & Timers", "shortName": "Alarms", - "version": "0.47", + "version": "0.48", "description": "Set alarms and timers on your Bangle", "icon": "app.png", "tags": "tool,alarm", diff --git a/apps/datetime_picker/ChangeLog b/apps/datetime_picker/ChangeLog new file mode 100644 index 000000000..ef4afacd0 --- /dev/null +++ b/apps/datetime_picker/ChangeLog @@ -0,0 +1 @@ +0.01: New drag/swipe date time picker, e.g. for use with dated events alarms diff --git a/apps/datetime_picker/README.md b/apps/datetime_picker/README.md new file mode 100644 index 000000000..8dac1dceb --- /dev/null +++ b/apps/datetime_picker/README.md @@ -0,0 +1,9 @@ +# App Name + +Datetime Picker allows to swipe along the bars to select date and time elements, e.g. for the datetime of Events in the Alarm App. As a standalone app, it allows to see the weekday of a given date and, once a datetime is selected, the number of days and time between that datetime and now. + +Screenshot: ![datetime with swipe controls](screenshot.png) + +## Controls + +Swipe to increase or decrease date and time elements. Press button or go back to select shown datetime. diff --git a/apps/datetime_picker/app-icon.js b/apps/datetime_picker/app-icon.js new file mode 100644 index 000000000..89250ff58 --- /dev/null +++ b/apps/datetime_picker/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AAnfAgf9z4FD/AFE/gFECIoFB98+tv+voFB//C/99z3Z7+J84XC3/7DpAFhKYP3AgP3AoPAOQMD/v/84LB+Z2FABiDKPoqJFKaWe/P/9Pznuf+wKB/29z+2//uTYOeTYPtRMxZKQaPAh6hBnEBwEGAoMYgHf9+/dwP5A==")) diff --git a/apps/datetime_picker/app.js b/apps/datetime_picker/app.js new file mode 100644 index 000000000..7bc66f6c5 --- /dev/null +++ b/apps/datetime_picker/app.js @@ -0,0 +1,5 @@ +require("datetimeinput").input().then(result => { + E.showPrompt(`${result}\n\n${require("time_utils").formatDuration(Math.abs(result-Date.now()))}`, {buttons:{"Ok":true}}).then(function() { + load(); + }); +}); diff --git a/apps/datetime_picker/app.png b/apps/datetime_picker/app.png new file mode 100644 index 0000000000000000000000000000000000000000..b7cb4b46ba0943e627ad0c948b747e522735a7a0 GIT binary patch literal 1679 zcmV;A25|X_P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGma{vGma{-JZxd#9M01|XXSaefwW^{L9 za%BKVa%E+1b7*gLUR4MM000ICNklHl%v^{kwk8{n(TLEqDL) z_dM3Mow?6*pL3sco!9GJhd|%)_3Kyq`0*niwFVOt6a1G6Bqk=(fddEl2W`OY>@4lt zwTpUtdr6p@nj(FDeG&wL>g(&pB$|zghzPE)udgo&uV23=dwY8(QBhGLCed81tgKL6 zTpSZPa^#3|2!)1*lBK03C(s5=PEIm`y1F_gfn&#xk-51!zf@sqX^HOKxkKn7;K`FG z^!)jAer|7X=XXG3V({SSY-}v;-o2Y{+_*t`d3kjA?p;bxPp7?m_i_)S zV0D1x;lqdN{Q2`7udc49=;&yA_wF5~rlwLzNC=gemy<%F;P(I;1?46uC)2}+4=E@p zh=PNI>FwLMVhSZ>5kNQLW2kI=d^~^j^y$;9y>jc;Eh;D|pp1+RN=Zp!vI`3fVs7H+ z{QP{*i(c^X@SwA2&r)DuAccpA)9~;x$FY*AFfuYij~+du?CfmLgS;T!(a|BMP(l^~ zOcXyqKWPwId@V>BEN)Vs2#*U|1ob7f{BjQ>U0z zKtKSuY;JDu52VkWIl}}|c{C8d4KiyN7g+=_uCHCYMoCFYyzXXZX41`@H)&>OhFbqiOIdtX96`mWfUcKTxs2m8XHX?O^9MWKj&Ck#C@WpRyA&bDD1cvAD zV9N>oF8&hOYHU9O+9o9+KtX_|#ItA5$j;7=#T<6Vt;UxxU&JSAEH5w9$&)9UfWN;# z34?=!WMpK-Kh)ONipivbUDwOYi;4b;aQX6Oa&T~Oii!YscOF9L&YdH7 zcXv*(9-v*2>HiAYNY?{)d@Cy}Rsu}m)2B}~G&IC7*F#TF5AE5rha4RpC0lPZLPA0U z?bxw{E?&I2;bw=*15Dso0KxUA0GaaQ#S1Ye55G)6)4&A2ef!2io1jv$OaPJ@Gh17L z#&&aaqtVe(5du#Uz>y$aK{A!i5VY6c-nhuC6ZW=;-h&py|Ly=Irdu z1h66si;IhV!ZI^6BRp#Zc>VuQ0FBRc;P~<5WMN^!FSP+UPlG^JRh5#!wv^KlRj@X2 zmfbA$_4V;zXdOrexE^c8o9Z2abMyWC_t`H}hb#iHOrZAQ=isBTKhSme?%iYg4Oamk zj~t+qa5D}bJjii)Nf@?SSy{X!Vko?M^M>UN41yCUPOzH+M*yY27{dhiOlM~& z_W*hxao9Qg_wVPm9`-->D-~oBz=zSoC>TIXAYNHn$=wZ05GNuzK_4s#5>j|!_p76DDhUjkc=?MDDoTAOSYoZLVF7bpVOTQ~uR zhK4^qNNWmk9sGl>(%Ra}5dTNppxfsrOO=RwoZ3IMjy@ z9pa4vhZd;-?+9te#>TiH05_4BE?we(zL3hZQ9!x4R)tho2Tp-5E-vfV2N&p&*$Rb% Z{sH}H_||6oa%uno002ovPDHLkV1igS1n&R< literal 0 HcmV?d00001 diff --git a/apps/datetime_picker/lib.js b/apps/datetime_picker/lib.js new file mode 100644 index 000000000..c3e51ae4d --- /dev/null +++ b/apps/datetime_picker/lib.js @@ -0,0 +1,145 @@ +exports.input = function(options) { + options = options||{}; + var selectedDate; + if (options.datetime instanceof Date) { + selectedDate = new Date(options.datetime.getTime()); + } else { + selectedDate = new Date(); + selectedDate.setMinutes(0); + selectedDate.setSeconds(0); + selectedDate.setMilliseconds(0); + selectedDate.setHours(selectedDate.getHours() + 1); + } + + var R; + var tip = {w: 12, h: 10}; + var arrowRectArray; + var dragging = null; + var startPos = null; + var dateAtDragStart = null; + var SELECTEDFONT = '6x8:2'; + + function drawDateTime() { + g.clearRect(R.x+tip.w,R.y,R.x2-tip.w,R.y+40); + g.clearRect(R.x+tip.w,R.y2-60,R.x2-tip.w,R.y2-40); + + g.setFont(SELECTEDFONT).setColor(g.theme.fg).setFontAlign(-1, -1, 0); + var dateUtils = require('date_utils'); + g.drawString(selectedDate.getFullYear(), R.x+tip.w+10, R.y+15) + .drawString(dateUtils.month(selectedDate.getMonth()+1,1), R.x+tip.w+65, R.y+15) + .drawString(selectedDate.getDate(), R.x2-tip.w-40, R.y+15) + .drawString(`${dateUtils.dow(selectedDate.getDay(), 1)} ${selectedDate.toLocalISOString().slice(11,16)}`, R.x+tip.w+10, R.y2-60); + } + + let dragHandler = function(event) { + "ram"; + + if (event.b) { + if (dragging === null) { + // determine which component we are affecting + var rect = arrowRectArray.find(rect => rect.y2 + ? (event.y >= rect.y && event.y <= rect.y2 && event.x >= rect.x - 10 && event.x <= rect.x + tip.w + 10) + : (event.x >= rect.x && event.x <= rect.x2 && event.y >= rect.y - tip.w - 5 && event.y <= rect.y + 5)); + if (rect) { + dragging = rect; + startPos = dragging.y2 ? event.y : event.x; + dateAtDragStart = selectedDate; + } + } + + if (dragging) { + dragging.swipe(dragging.y2 ? startPos - event.y : event.x - startPos); + drawDateTime(); + } + } else { + dateAtDragStart = null; + dragging = null; + startPos = null; + } + }; + + let catchSwipe = ()=>{ + E.stopEventPropagation&&E.stopEventPropagation(); + }; + + return new Promise((resolve,reject) => { + // Interpret touch input + Bangle.setUI({ + mode: 'custom', + back: ()=>{ + Bangle.setUI(); + Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe listener if it was added with `Bangle.prependListener()` (fw2v19 and up). + g.clearRect(Bangle.appRect); + resolve(selectedDate); + }, + drag: dragHandler + }); + Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. + + R = Bangle.appRect; + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + function drawArrow(rect) { + if(rect.x2) { + g.fillRect(rect.x + tip.h, rect.y - tip.w + 4, rect.x2 - tip.h, rect.y - 4) + .fillPoly([rect.x + tip.h, rect.y, rect.x + tip.h, rect.y - tip.w, rect.x, rect.y - (tip.w / 2)]) + .fillPoly([rect.x2-tip.h, rect.y, rect.x2 - tip.h, rect.y - tip.w, rect.x2, rect.y - (tip.w / 2)]); + } else { + g.fillRect(rect.x + 4, rect.y + tip.h, rect.x + tip.w - 4, rect.y2 - tip.h) + .fillPoly([rect.x, rect.y + tip.h, rect.x + tip.w, rect.y + tip.h, rect.x + (tip.w / 2), rect.y]) + .fillPoly([rect.x, rect.y2 - tip.h, rect.x + tip.w, rect.y2 - tip.h, rect.x + (tip.w / 2), rect.y2]); + } + + } + + var yearArrowRect = {x: R.x, y: R.y, y2: R.y + (R.y2 - R.y) * 0.4, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setFullYear(dateAtDragStart.getFullYear() + Math.floor(d/10)); + if (dateAtDragStart.getDate() != selectedDate.getDate()) selectedDate.setDate(0); + }}; + + var weekArrowRect = {x: R.x, y: yearArrowRect.y2 + 10, y2: R.y2 - tip.w - 5, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setDate(dateAtDragStart.getDate() + (Math.floor(d/10) * 7)); + }}; + + var dayArrowRect = {x: R.x2 - tip.w, y: R.y, y2: R.y + (R.y2 - R.y) * 0.4, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setDate(dateAtDragStart.getDate() + Math.floor(d/10)); + }}; + + var fifteenMinutesArrowRect = {x: R.x2 - tip.w, y: dayArrowRect.y2 + 10, y2: R.y2 - tip.w - 5, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setMinutes((((dateAtDragStart.getMinutes() - (dateAtDragStart.getMinutes() % 15) + (Math.floor(d/14) * 15)) % 60) + 60) % 60); + }}; + + var weekdayArrowRect = {x: R.x, y: R.y2, x2: (R.x2 - R.x) * 0.3 - 5, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setDate(dateAtDragStart.getDate() + Math.floor(d/10)); + }}; + + var hourArrowRect = {x: weekdayArrowRect.x2 + 5, y: R.y2, x2: weekdayArrowRect.x2 + (R.x2 - R.x) * 0.38, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setHours((((dateAtDragStart.getHours() + Math.floor(d/10)) % 24) + 24) % 24); + }}; + + var minutesArrowRect = {x: hourArrowRect.x2 + 5, y: R.y2, x2: R.x2, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setMinutes((((dateAtDragStart.getMinutes() + Math.floor(d/7)) % 60) + 60) % 60); + }}; + + var monthArrowRect = {x: (R.x2 - R.x) * 0.2, y: R.y2 / 2 + 5, x2: (R.x2 - R.x) * 0.8, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setMonth(dateAtDragStart.getMonth() + Math.floor(d/10)); + if (dateAtDragStart.getDate() != selectedDate.getDate()) selectedDate.setDate(0); + }}; + + arrowRectArray = [yearArrowRect, weekArrowRect, dayArrowRect, fifteenMinutesArrowRect, + weekdayArrowRect, hourArrowRect, minutesArrowRect, monthArrowRect]; + + drawDateTime(); + arrowRectArray.forEach(drawArrow); + }); +}; diff --git a/apps/datetime_picker/metadata.json b/apps/datetime_picker/metadata.json new file mode 100644 index 000000000..df968589e --- /dev/null +++ b/apps/datetime_picker/metadata.json @@ -0,0 +1,16 @@ +{ "id": "datetime_picker", + "name": "Datetime picker", + "shortName":"Datetime picker", + "version":"0.01", + "description": "Allows to pick a date and time by swiping.", + "icon":"app.png", + "tags":"datetimeinput", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots" : [ { "url":"screenshot.png" } ], + "storage": [ + {"name":"datetimeinput","url":"lib.js"}, + {"name":"datetime_picker.app.js","url":"app.js"}, + {"name":"datetime_picker.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/datetime_picker/screenshot.png b/apps/datetime_picker/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..6e14af8be43a781874ce26e93b10562e53db0bc6 GIT binary patch literal 2430 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