From a9804987aa42c60efa162eadaf1d383531e1a5e6 Mon Sep 17 00:00:00 2001 From: Danny <31635744+DDDanny@users.noreply.github.com> Date: Thu, 20 Jan 2022 00:41:47 +0100 Subject: [PATCH] timecal 0.02 --- apps/timecal/ChangeLog | 7 + apps/timecal/README.md | 16 + apps/timecal/timecal.app.js | 326 +++++++++++++----- apps/timecal/timecal.app.test.js | 566 +++++++++++++++++++++++++++++++ apps/timecal/timecal.settings.js | 31 ++ 5 files changed, 862 insertions(+), 84 deletions(-) create mode 100644 apps/timecal/ChangeLog create mode 100644 apps/timecal/README.md create mode 100644 apps/timecal/timecal.app.test.js create mode 100644 apps/timecal/timecal.settings.js diff --git a/apps/timecal/ChangeLog b/apps/timecal/ChangeLog new file mode 100644 index 000000000..58da864d3 --- /dev/null +++ b/apps/timecal/ChangeLog @@ -0,0 +1,7 @@ +0.01: Initial creation of the clock face time and calendar +0.02: Feature Request #1154 and some findings... + -> get rendered time from optimisations + -> *BATT SAFE* only update once a minute instead of once a second + -> *RAM optimized* clean code, corrected minute update (timout, no intervall) + -> locale: weekday name (first two characters) from locale + -> added settings to render cal view begin day (-1: today, 0:sunday, 1:monday [default]) \ No newline at end of file diff --git a/apps/timecal/README.md b/apps/timecal/README.md new file mode 100644 index 000000000..345b117f3 --- /dev/null +++ b/apps/timecal/README.md @@ -0,0 +1,16 @@ +# Calendar Clock + +## Features +Shows the +* Date +* Time (hh:mm) - respecting 12/24 (uses locale string) +* 3 weeks calendar view (last,current and next week) + +### The settings menu +Calendar View can be customized +* Cal.Start Day: Set day of week start. Values: 0=Sunday ... 6=Saturday or -1 Relative to today (default 1: Monday) +* Date Type: Choose how the date is display. Values: none, locale (default), MMM YYYY #WW +* Sunday Color: Set Sundays color. Values: none (default), red, green or blue +* today Color: Set today color. Values: none, red (default), green or blue +* today Marker: Outline today graphically. Values: none (default), rect(angle) or circle +* today MColor: Color for Outline. Values: none (default), red, green or blue diff --git a/apps/timecal/timecal.app.js b/apps/timecal/timecal.app.js index b28326c46..090464be1 100644 --- a/apps/timecal/timecal.app.js +++ b/apps/timecal/timecal.app.js @@ -1,94 +1,252 @@ -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); - } +//Clock renders date, time and pre,current,next week calender view +class TimeCalClock{ + DATE_FONT_SIZE(){ return 20; } + TIME_FONT_SIZE(){ return 40; } - 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); -} + /** + * @param{Date} date optional the date (e.g. for testing) + * @param{Settings} settings optional settings to use e.g. for testing + */ + constructor(date, settings){ + if (date) + this.date=date; -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()); - } + if (settings) + this._settings = settings; + else + this._settings = require("Storage").readJSON("timecal.settings.json", 1) || {}; + + const defaults = { + showDate:"l", //(n)one, (l)ocale, (m)onth short(y)ear(w)eek + + wdStrt:1, //identical to getDay() 0->Su, 1->Mo, ... //Issue #1154: weekstart So/Mo, + + todayNumClr:"#00E", + todayMrker:"r", //(n)one, (c)ircle, (r)ect, (f)illed + todayMrkClr:"#0E0", + todayMrkMrkPxl:3, + + suColor:"#E00", //sunday + phColor:"#E00", //public holiday + + calBorder:true + }; + for (const k in this._settings) if (!defaults.hasOwnProperty(k)) delete this._settings[k]; //remove invalid settings + for (const k in defaults) if(!this._settings.hasOwnProperty(k)) this._settings[k] = defaults[k]; //assign missing defaults + + g.clear(); + Bangle.setUI("clock"); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + this.center_x = Bangle.appRect.w/2; } - 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); + + /** + * @returns {Object} current settings object + */ + settings(){ + return this._settings; + } + + + /* + * Run forest run + **/ + draw(){ + this.drawTime(); + } + + /** + * draw given or current time from date + * overwatch timezone changes + * schedules itself to update + */ + drawTime(){ + d=this.date ? this.date : new Date(); + console.log("drawTime", d); + const Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10; + + d=d?d :new Date(); + + g.setFontAlign(0, -1); + g.setFont("Vector", this.TIME_FONT_SIZE()); + g.setColor(g.theme.fg); + g.clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7); + g.drawString(("0" + require("locale").time(d, 1)).slice(-5), this.center_x, Y); + //.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7); //DEV-Option + + setTimeout(this.drawTime.bind(this), 60000-(d.getSeconds()*1000)-d.getMilliseconds()); + if (this.TZOffset===undefined || this.TZOffset!==d.getTimezoneOffset()) + this.drawDateAndCal(); + this.TZOffset=d.getTimezoneOffset(); + } + + /** + * draws given date and cal + * @param{Date} d provide date or uses today + */ + drawDateAndCal(){ + d=this.date ? this.date : new Date(); + + if (this.tOutD) //abort exisiting + clearTimeout(this.tOutD); + + this.drawDate(); + this.drawCal(); + this.tOutD=setTimeout(this.drawDateAndCal.bind(this), 86400000-(d.getHours()*24*60*1000)-(d.getMinutes()*60*1000)-d.getSeconds()-d.getMilliseconds()); } - 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); + /** + * draws given date as defiend in settings + */ + drawDate(){ + d=this.date ? this.date : new Date(); + + const FONT_SIZE=20; + const Y=Bangle.appRect.y; + var render=false; + var dateStr = ""; + console.log(">"+this.settings().showDate+"<"); + if (!(this.settings().showDate==="n")) + for (let c of this.settings().showDate) { //add part as configured + switch (c){ + case "l":{ //locale + render=true; + dateStr+=require("locale").date(d,1); + break; + } + case "m":{ //month e.g. Jan. + render=true; + dateStr+=require("locale").month(d,1); + break; + } + case "y":{ //year e.g. 2022 + render=true; + dateStr+=d.getFullYear(); + break; + } + case "w":{ //week e.g. #02 + dateStr+=("0"+this.ISO8601calWeek(d)).slice(-2); + break; + } + default: //append c + dateStr+=c; + render=dateStr.length>0; + break; //noop + } } - g.drawString(start.getDate(),x*cellSize +(cellSize / 2) + 2,calStart+(cellHeight*y) + 5); - start.setDate(start.getDate()+1); + if (render){ + g.clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3); + g.setFont("Vector", FONT_SIZE); + g.setColor(g.theme.fg); + g.setFontAlign(0, -1); + g.drawString(dateStr,this.center_x,Y); + } + //g.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3); //DEV-Option + } + + /** + * draws calender week view (-1,0,1) for given date + */ + drawCal(){ + d=this.date ? this.date : new Date(); + + const DAY_NAME_FONT_SIZE=10; + const CAL_Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10+this.TIME_FONT_SIZE()+3; + const CAL_AREA_H=Bangle.appRect.h-CAL_Y-24; //0,24,48 no,1,2 widget lines + const CELL_W=Bangle.appRect.w/7; //cell width + const CELL_H=(CAL_AREA_H-DAY_NAME_FONT_SIZE)/3; //cell heigth + const DAY_NUM_FONT_SIZE=Math.min(CELL_H-1,15); //size down, max 15 + + g.clearRect(Bangle.appRect.x, CAL_Y, Bangle.appRect.x2, CAL_Y+CAL_AREA_H); + + var dNames=[]; + if (require("locale") && require("locale").abday) + dNames=require("locale").abday.map((a) => a.length>2 ? a.substr(0, 2) : a ); //retrieve from locale and force max 2 chars + else + dNames=["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; //fallback + + g.setFont("Vector", DAY_NAME_FONT_SIZE); + g.setColor(g.theme.fg); + g.setFontAlign(-1, -1); + + //draw grid & Headline + for(var dNo=0; dNo0) + g.drawLine(dNo*CELL_W, CAL_Y, dNo*CELL_W, CAL_Y+CAL_AREA_H-1); + } + + var nextY=CAL_Y+DAY_NAME_FONT_SIZE; + + for(i=0; i<3; i++){ + const y=nextY+i*CELL_H; + g.drawLine(Bangle.appRect.x, y, Bangle.appRect.x2, y); + } + + g.setFont("Vector", DAY_NUM_FONT_SIZE); + + //write days + const todayDate=d.getDate(); + const days=7+(7+d.getDay()-this.settings().wdStrt)%7;//start day (week before=7 days + days in this week realtive to week start) + var rD=new Date(); + rD.setDate(rD.getDate()-days); + var rDate=rD.getDate(); + for(var y=0; y<3; y++){ + for(var x=0; xSu, 1->Mo, ... //Issue #1154: weekstart So/Mo, + + todayNumClr:"#00E", + todayMrker:"r", //(n)one, (c)ircle, (r)ect, (f)illed + todayMrkClr:"#0E0", + todayMrkMrkPxl:3, + + suColor:"#E00", //sunday + phColor:"#E00", //public holiday + + calBorder:true + }; + for (const k in this._settings) if (!defaults.hasOwnProperty(k)) delete this._settings[k]; //remove invalid settings + for (const k in defaults) if(!this._settings.hasOwnProperty(k)) this._settings[k] = defaults[k]; //assign missing defaults + + g.clear(); + Bangle.setUI("clock"); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + this.center_x = Bangle.appRect.w/2; + } + + /** + * @returns {Object} current settings object + */ + settings(){ + return this._settings; + } + + + /* + * Run forest run + **/ + draw(){ + this.drawTime(); + } + + /** + * draw given or current time from date + * overwatch timezone changes + * schedules itself to update + */ + drawTime(){ + d=this.date ? this.date : new Date(); + console.log("drawTime", d); + const Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10; + + d=d?d :new Date(); + + g.setFontAlign(0, -1); + g.setFont("Vector", this.TIME_FONT_SIZE()); + g.setColor(g.theme.fg); + g.clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7); + g.drawString(("0" + require("locale").time(d, 1)).slice(-5), this.center_x, Y); + //.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7); //DEV-Option + + setTimeout(this.drawTime.bind(this), 60000-(d.getSeconds()*1000)-d.getMilliseconds()); + if (this.TZOffset===undefined || this.TZOffset!==d.getTimezoneOffset()) + this.drawDateAndCal(); + this.TZOffset=d.getTimezoneOffset(); + } + + /** + * draws given date and cal + * @param{Date} d provide date or uses today + */ + drawDateAndCal(){ + d=this.date ? this.date : new Date(); + + if (this.tOutD) //abort exisiting + clearTimeout(this.tOutD); + + this.drawDate(); + this.drawCal(); + this.tOutD=setTimeout(this.drawDateAndCal.bind(this), 86400000-(d.getHours()*24*60*1000)-(d.getMinutes()*60*1000)-d.getSeconds()-d.getMilliseconds()); + } + + /** + * draws given date as defiend in settings + */ + drawDate(){ + d=this.date ? this.date : new Date(); + + const FONT_SIZE=20; + const Y=Bangle.appRect.y; + var render=false; + var dateStr = ""; + console.log(this.settings().showDate); + if (this.settings().showDate!=="n"); + for (let c of this.settings().showDate) { //add part as configured + switch (c){ + case "l":{ //locale + render=true; + dateStr+=require("locale").date(d,1); + break; + } + case "m":{ //month e.g. Jan. + render=true; + dateStr+=require("locale").month(d,1); + break; + } + case "y":{ //year e.g. 2022 + render=true; + dateStr+=d.getFullYear(); + break; + } + case "w":{ //week e.g. #02 + dateStr+=("0"+this.ISO8601calWeek(d)).slice(-2); + break; + } + default: //append c + dateStr+=c; + render=dateStr.length>0; + break; //noop + } + } + if (render){ + g.clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3); + g.setFont("Vector", FONT_SIZE); + g.setColor(g.theme.fg); + g.setFontAlign(0, -1); + g.drawString(dateStr,this.center_x,Y); + } + //g.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3); //DEV-Option + } + + /** + * draws calender week view (-1,0,1) for given date + */ + drawCal(){ + d=this.date ? this.date : new Date(); + + const DAY_NAME_FONT_SIZE=10; + const CAL_Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10+this.TIME_FONT_SIZE()+3; + const CAL_AREA_H=Bangle.appRect.h-CAL_Y-24; //0,24,48 no,1,2 widget lines + const CELL_W=Bangle.appRect.w/7; //cell width + const CELL_H=(CAL_AREA_H-DAY_NAME_FONT_SIZE)/3; //cell heigth + const DAY_NUM_FONT_SIZE=Math.min(CELL_H-1,15); //size down, max 15 + + g.clearRect(Bangle.appRect.x, CAL_Y, Bangle.appRect.x2, CAL_Y+CAL_AREA_H); + + var dNames=[]; + if (require("locale") && require("locale").abday) + dNames=require("locale").abday.map((a) => a.length>2 ? a.substr(0, 2) : a ); //retrieve from locale and force max 2 chars + else + dNames=["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; //fallback + + g.setFont("Vector", DAY_NAME_FONT_SIZE); + g.setColor(g.theme.fg); + g.setFontAlign(-1, -1); + + //draw grid & Headline + for(var dNo=0; dNo0) + g.drawLine(dNo*CELL_W, CAL_Y, dNo*CELL_W, CAL_Y+CAL_AREA_H-1); + } + + var nextY=CAL_Y+DAY_NAME_FONT_SIZE; + + for(i=0; i<3; i++){ + const y=nextY+i*CELL_H; + g.drawLine(Bangle.appRect.x, y, Bangle.appRect.x2, y); + } + + g.setFont("Vector", DAY_NUM_FONT_SIZE); + + //write days + const todayDate=d.getDate(); + const days=7+(7+d.getDay()-this.settings().wdStrt)%7;//start day (week before=7 days + days in this week realtive to week start) + var rD=new Date(); + rD.setDate(rD.getDate()-days); + var rDate=rD.getDate(); + for(var y=0; y<3; y++){ + for(var x=0; x", + functionNames: ["required, ", "..."], + cases: [ + { + value: "required,", + beforeTxt: "optional,", + beforeExpression: "optional,", + afterText: "optional,", + afterExpression: "optional," + } + ] + } + } + + constructor(data){ + + this._validate(data); + + this.setting = data.setting; + this.methodNames = data.functionNames; + this.cases = data.cases.map((entry) => { + return { + value: entry.value, + beforeTxt: entry.beforeTxt||"", + beforeExpression: entry.beforeExpression||true, + afterTxt: entry.afterTxt||"", + afterExpression: entry.afterExpression||true + }; + }); + } + + /** + * validates the given data config + */ + _validate(data){ + //validate given config + if (!data.setting) throw new EmptyMandatoryError("setting", data, this.TEST_SETTING_SAMPLE()); + if (!data.cases instanceof Array || data.cases.length==0) throw new EmptyMandatoryError("cases", data, this.TEST_SETTING_SAMPLE()); + if (!data.functionNames instanceof Array || data.functionNames==0) throw new EmptyMandatoryError("functionNames", data, this.TEST_SETTING_SAMPLE()); + + data.cases.forEach((entry,idx) => { + if (!entry.value) throw new EmptyMandatoryError("cases["+idx+"].value", entry, this.TEST_SETTING_SAMPLE()); + }); + } +} + +/*************************************************************************/ + +/** + * Testing a Bangle object + */ +class BangleTestRunner{ + /** + * create for ObjClass + * @param {Class} objClass + * @param {LogSeverity} minSeverity to Log + */ + constructor(objClass, minSeverity){ + this.TESTCASE_MSG_BEFORE_TIMEOUT = 1000; //5s + this.TESTCASE_RUN_TIMEOUT = 1000; //5s + this.TESTCASE_MSG_AFTER_TIMEOUT = 1000; //5s + + this.oClass = objClass; + this.minSvrty = minSeverity; + this.tests = []; + + this.currentCaseNum = this.currentTestNum = this.currentTest = this.currentCase = undefined; + } + + /** + * add a Setting Test, return instance for chaining + * @param {TestSetting} + */ + addSetting(test) { + this.tests.push(test); + return this; + } + + /** + * Test execution of all tests + */ + execute() { + this._init(); + while (this._nextTest()) { + this._beforeTest(); + while (this._nextCase()) { + this._beforeCase(); + this._runCase(); + this._afterCase(); + } + this._afterTest(); + }; + } + + /** + * global prepare + */ + _init() { + console.log(new Date(),">>init"); + this.currentTestNum=-1; + this.currentCaseNum=-1; + } + + /** + * before each test + */ + _beforeTest() { + console.log(new Date(),">>test #" + this.currentTestNum); + } + + /** + * befor each testcase + */ + _beforeCase() { + console.log(new Date(),">>case #" + this.currentTestNum + "." + this.currentCaseNum + "/" + (this.currentTest.cases.length-1)); + if (this.currentTest instanceof TestSetting) { + console.log( + this.currentTest.setting + "="+this.currentCase.value+"\n"+ + this.currentCase.beforeTxt ? "testcase: " + this.currentCase.beforeTxt : "" + ); + } + } + + _runCase() { + console.log(new Date(), ">>running..."); + var returns = []; + this.currentTest.methodNames.forEach((methodName) => { + var instance = eval("new " + this.oClass + ""); + console.log(instance); + const method = instance[methodName]; + //console.log(">>"+this.oClass+"["+methodName+"]()"); + //if (typeof method !== "function") + // throw new InvalidMethodName(this.oClass, methodName); + let settings={}; settings[this.currentTest.setting] = this.currentCase.value; + returns.push(new TimeCalClock(new Date(), settings).drawDate()); + //returns.push(method()); + console.log("<<"+this.oClass+"["+methodName+"]()"); + g.dump() + }); + + //this._delay(1).then((result) => console.log(new Date(), "finished")); + //g.dump(); + /*var testCaseNum=0; + test.cases.forEach(testcase => { //execute test + testcase.dates.forEach(date => { //spawn with each date + testCaseNum++; + E.showMessage( + "="+testcase.value+"\n" + +"expected: "+testcase.descr, + "#"+testCaseNum+": "+test.setting + ); + this._delay(TESTCASE_MSG_TIMEOUT).then((r,e) => { + const objUnderTest = new Object.create(this.ObjClass)(date, new Object()[test.setting]=test.value ); + objUnderTest.draw(); + this._delay(TESTCASE_RUN_TIMEOUT).then((r,e) => { + }); + }); + }); + });*/ + console.log(new Date(), "<<...running"); + } + + _afterCase() { + if (this.currentTest instanceof TestSetting) { + if (this.currentCase.afterTxt.length>0) { + console.log( + "--EXPECTED: " + this.currentCase.afterTxt + ); + } + } + console.log(new Date(), "< setTimeout(resolve, sec)); + } + + _waits(sec) { + this._delay(1).then(); + } + + _log() { + + } + + _nextTest() { + if (this.currentTestNum>=-1 && (this.currentTestNum+1)=-1 && (this.currentCaseNum+1) back(), + "Cal.Start Day": { + value: settings.wdStrt === undefined ? 1 : settings.wdStrt, //def: 1: mon + min: -1, max: 6, + //render dow from locale or LANG"today" + format: v => (v>=0 ? (require("locale") && require("locale").abday && require("locale").abday[v] ? : && require("locale").abday[v] : DOW_abbr_FB[v]) : /*LANG*/"today"), + onchange: v => { + settings.weekDay = v; + writeSettings(); + } + } +}; \ No newline at end of file