diff --git a/apps/agenda/agenda.app.js b/apps/agenda/agenda.app.js new file mode 100644 index 000000000..4aac371f9 --- /dev/null +++ b/apps/agenda/agenda.app.js @@ -0,0 +1,175 @@ +/* CALENDAR is a list of: + {id:int, + type, + timestamp, + durationInSeconds, + title, + description, + location, + color:int, + calName, + allDay: bool, + } +*/ + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var FILE = "android.calendar.json"; + +var Locale = require("locale"); + +var fontSmall = "6x8"; +var fontMedium = g.getFonts().includes("12x20")?"12x20":"6x8:2"; +var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; +var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; + +//FIXME maybe write the end from GB already? Not durationInSeconds here (or do while receiving?) +var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[]; +var settings = require("Storage").readJSON("agenda.settings.json",true)||{}; + +CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp); + +function getDate(timestamp) { + return new Date(timestamp*1000); +} +function formatDay(date) { + let formattedDate = Locale.dow(date, 1) + " " + Locale.date(date).replace(/\d\d\d\d/,""); + if (!settings.useToday) return formattedDate; + + const today = new Date(Date.now()); + const delta = deltaDate(today, date); + + if (delta === 0) return /*LANG*/"Today "; + else if (delta === 1) return /*LANG*/"Tomorrow "; + else if (delta <= 5) return require("locale").dow(date); + else return formattedDate; +} + +function formatDateLong(date, includeDay, allDay) { + let shortTime = Locale.time(date,1)+Locale.meridian(date); + if(allDay) shortTime = ""; + if(includeDay || allDay) { + return formatDay(date)+" "+shortTime; + } + return shortTime; +} + +function formatDateShort(date, allDay) { + return formatDay(date)+(allDay?"":Locale.time(date,1)+Locale.meridian(date)); +} + +function deltaDate(date1, date2) { + let tzo = date1.getTimezoneOffset() * 60000; // time zone offset in minutes * 60000 ms/min = tzo in ms + return (Math.floor((date2.valueOf() - tzo) / 86400000) - Math.floor((date1.valueOf() - tzo) / 86400000)); +} + +var lines = []; +function showEvent(ev) { + var bodyFont = fontBig; + if(!ev) return; + g.setFont(bodyFont); + //var lines = []; + if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10); + var titleCnt = lines.length; + var start = getDate(ev.timestamp); + var end = getDate((+ev.timestamp) + (+ev.durationInSeconds)); + var includeDay = true; + if (titleCnt) lines.push(""); // add blank line after title + if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth()) + includeDay = false; + if(includeDay && ev.allDay) { + //single day all day (average to avoid getting previous day) + lines = lines.concat( + g.wrapString(formatDateLong(new Date((start+end)/2), includeDay, ev.allDay), g.getWidth()-10)); + } else if(includeDay || ev.allDay) { + lines = lines.concat( + /*LANG*/"Start"+":", + g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10), + /*LANG*/"End"+":", + g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10)); + } else { + lines = lines.concat( + g.wrapString(formatDateShort(start,true), g.getWidth()-10), + g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10), + g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10)); + } + if(ev.location) + lines = lines.concat("",/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10)); + if(ev.description && ev.description.trim()) + lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10)); + if(ev.calName) + lines = lines.concat("",/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10)); + lines = lines.concat("",/*LANG*/"< Back"); + E.showScroller({ + h : g.getFontHeight(), // height of each menu item in pixels + c : lines.length, // number of menu items + // a function to draw a menu item + draw : function(idx, r) { + // FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12 + g.setBgColor(idx=lines.length-2) + showList(); + }, + back : () => showList() + }); +} + +function showList() { + //it might take time for GB to delete old events, decide whether to show them grayed out or hide entirely + if(!settings.pastEvents) { + let now = new Date(); + //TODO add threshold here? + CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000); + } + if(CALENDAR.length == 0) { + E.showMessage(/*LANG*/"No events"); + return; + } + E.showScroller({ + h : 74, + c : CALENDAR.length, //.max(CALENDAR.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) + draw : function(idx, r) {"ram" + var ev = CALENDAR[idx]; + g.setColor(g.theme.fg); + g.clearRect(r.x, r.y, r.x+r.w, r.y+r.h); + if (!ev) return; + let isPast = false; + var y = r.y + 5; + let title = ev.title; + let date = formatDateShort(getDate(ev.timestamp),ev.allDay); + // if (ev.location) var location = ev.location; // I don't think it's neccesary to show locaton in list view + if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000; + if (date) { + let fh = g.setFont(fontMedium).getFontHeight(); + //var col = g.blendColor(g.theme.fg,g.theme.bg, 0.75); + let col = g.theme.bg2; + g.setBgColor(col).clearRect(r.x, y, r.x + r.w, y + fh); + g.setBgColor(g.theme.bg); + g.setFontAlign(-1,-1).setColor(isPast ? "#888" : g.theme.fg); + g.drawString(date, r.x, y); + y += fh + 5; // set new y to position below date + } + if (title) { + let str = g.wrapString(title, r.x + r.w); + let numLines = str.length; + let titleStr = numLines > 1 ? str[0] + "\n" + str[1] : str[0]; + if(ev.color) { + g.setColor("#"+(0x1000000+Number(ev.color)).toString(16).padStart(6,"0")); + g.fillRect(r.x, y, r.x+3, r.y+r.h-3); // color bar + } + y = numLines > 1 ? y : y+5; + g.setFontAlign(-1,-1).setFont(fontBig) + .setColor(isPast ? "#888" : g.theme.fg).drawString(titleStr, r.x + 5, y); + } + g.setColor("#888").fillRect(r.x, r.y+r.h-1, r.x+r.w-1, r.y+r.h-1); // dividing line between items + }, + select : idx => showEvent(CALENDAR[idx]), + back : () => load() + }); +} +showList(); diff --git a/apps/agenda/agenda.settings.js b/apps/agenda/agenda.settings.js new file mode 100644 index 000000000..62e0c6dbd --- /dev/null +++ b/apps/agenda/agenda.settings.js @@ -0,0 +1,55 @@ +(function(back) { + function gbSend(message) { + Bluetooth.println(""); + Bluetooth.println(JSON.stringify(message)); + } + var settings = require("Storage").readJSON("agenda.settings.json",1)||{}; + function updateSettings() { + require("Storage").writeJSON("agenda.settings.json", settings); + } + var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[]; + var mainmenu = { + "" : { "title" : "Agenda" }, + "< Back" : back, + /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" }, + /*LANG*/"Force calendar sync" : () => { + if(NRF.getSecurityStatus().connected) { + E.showPrompt(/*LANG*/"Do you want to also clear the internal database first?", { + buttons: {/*LANG*/"Yes": 1, /*LANG*/"No": 2, /*LANG*/"Cancel": 3} + }).then((v)=>{ + switch(v) { + case 1: + require("Storage").writeJSON("android.calendar.json",[]); + CALENDAR = []; + /* falls through */ + case 2: + gbSend({t:"force_calendar_sync", ids: CALENDAR.map(e=>e.id)}); + E.showAlert(/*LANG*/"Request sent to the phone").then(()=>E.showMenu(mainmenu)); + break; + case 3: + default: + E.showMenu(mainmenu); + return; + } + }); + } else { + E.showAlert(/*LANG*/"You are not connected").then(()=>E.showMenu(mainmenu)); + } + }, + /*LANG*/"Show past events" : { + value : !!settings.pastEvents, + onchange: v => { + settings.pastEvents = v; + updateSettings(); + } + }, + /*LANG*/"Use 'Today',..." : { + value : !!settings.useToday, + onchange: v => { + settings.useToday = v; + updateSettings(); + } + }, + }; + E.showMenu(mainmenu); +})