diff --git a/apps/contacts/ChangeLog b/apps/contacts/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/contacts/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/contacts/README.md b/apps/contacts/README.md new file mode 100644 index 000000000..1bfc99c8e --- /dev/null +++ b/apps/contacts/README.md @@ -0,0 +1,29 @@ +# Contacts + +This app provides a common way to set up the `contacts.json` file. + +## Contacts JSON file + +When the app is loaded from the app loader, a file named +`contacts.json` is loaded along with the javascript etc. The file +has the following contents: + +``` +[ + { + "name":"NONE" + }, + { + "name":"First Last", + "number":"123456789", + } +] +``` + +## Contacts Editor + +Clicking on the download icon of `Contents` in the app loader invokes +the contact editor. The editor downloads and displays the current +`contacts.json` file. Clicking the `Edit` button beside an entry +causes the entry to be deleted from the list and displayed in the edit +boxes. It can be restored - by clicking the `Add` button. \ No newline at end of file diff --git a/apps/contacts/app-icon.js b/apps/contacts/app-icon.js new file mode 100644 index 000000000..3012be8d8 --- /dev/null +++ b/apps/contacts/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcBkmSpIC/AVsJCJ+AQaCZBCOeACKGQLKGQBA0ggARPJ4IRsYo0ggR9IoAIGiRiIpEECJsAiACBBYoRGpEAI4JBFI47CBLIRlDHYJrGYQIRCwQICL4MQOgx9GboUSeQ4RFwAFBiSGHCIo4CiVIWZyPICP4RaRIQROgARHdIwICoIIFkDpGBAKqHgGACI0AyVIggIDoEEMQ1ICINJCIj4CfwIREBwUgQYYOCfYoFDJQKDFCIopEO4RoDKAqJHRhAC/ATA=")) diff --git a/apps/contacts/app.png b/apps/contacts/app.png new file mode 100644 index 000000000..147dcc61a Binary files /dev/null and b/apps/contacts/app.png differ diff --git a/apps/contacts/contacts.app.js b/apps/contacts/contacts.app.js new file mode 100644 index 000000000..85eef625b --- /dev/null +++ b/apps/contacts/contacts.app.js @@ -0,0 +1,189 @@ +/* contacts.js */ + +var Layout = require("Layout"); + +const W = g.getWidth(); +const H = g.getHeight(); + +var wp = require('Storage').readJSON("contacts.json", true) || []; +// Use this with corrupted contacts +//var wp = []; + +var key; /* Shared between functions, typically wp name */ + +function writeContact() { + require('Storage').writeJSON("contacts.json", wp); +} + +function mainMenu() { + var menu = { + "< Back" : Bangle.load + }; + if (Object.keys(wp).length==0) Object.assign(menu, {"NO Contacts":""}); + else for (let id in wp) { + let i = id; + menu[wp[id]["name"]]=()=>{ decode(i); }; + } + menu["Add"]=addCard; + menu["Remove"]=removeCard; + g.clear(); + E.showMenu(menu); +} + +function decode(pin) { + var i = wp[pin]; + var l = i["name"] + "\n" + i["number"]; + var la = new Layout ({ + type:"v", c: [ + {type:"txt", font:"10%", pad:1, fillx:1, filly:1, label: l}, + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label:"OK", cb:l=>{mainMenu();}} + ], lazy:true}); + g.clear(); + la.render(); +} + +function showNumpad(text, key_, callback) { + key = key_; + E.showMenu(); + function addDigit(digit) { + key+=digit; + if (1) { + l = text[key.length]; + switch (l) { + case '.': case ' ': case "'": + key+=l; + break; + case 'd': case 'D': default: + break; + } + } + Bangle.buzz(20); + update(); + } + function update() { + g.reset(); + g.clearRect(0,0,g.getWidth(),23); + s = key + text.substr(key.length, 999); + g.setFont("Vector:24").setFontAlign(1,0).drawString(s,g.getWidth(),12); + } + ds="12%"; + var numPad = new Layout ({ + type:"v", c: [{ + type:"v", c: [ + {type:"", height:24}, + {type:"h",filly:1, c: [ + {type:"btn", font:ds, width:58, label:"7", cb:l=>{addDigit("7");}}, + {type:"btn", font:ds, width:58, label:"8", cb:l=>{addDigit("8");}}, + {type:"btn", font:ds, width:58, label:"9", cb:l=>{addDigit("9");}} + ]}, + {type:"h",filly:1, c: [ + {type:"btn", font:ds, width:58, label:"4", cb:l=>{addDigit("4");}}, + {type:"btn", font:ds, width:58, label:"5", cb:l=>{addDigit("5");}}, + {type:"btn", font:ds, width:58, label:"6", cb:l=>{addDigit("6");}} + ]}, + {type:"h",filly:1, c: [ + {type:"btn", font:ds, width:58, label:"1", cb:l=>{addDigit("1");}}, + {type:"btn", font:ds, width:58, label:"2", cb:l=>{addDigit("2");}}, + {type:"btn", font:ds, width:58, label:"3", cb:l=>{addDigit("3");}} + ]}, + {type:"h",filly:1, c: [ + {type:"btn", font:ds, width:58, label:"0", cb:l=>{addDigit("0");}}, + {type:"btn", font:ds, width:58, label:"C", cb:l=>{key=key.slice(0,-1); update();}}, + {type:"btn", font:ds, width:58, id:"OK", label:"OK", cb:callback} + ]} + ]} + ], lazy:true}); + g.clear(); + numPad.render(); + update(); +} + +function removeCard() { + var menu = { + "" : {title : "Select Contact"}, + "< Back" : mainMenu + }; + if (Object.keys(wp).length==0) Object.assign(menu, {"No Contacts":""}); + else { + wp.forEach((val, card) => { + const name = wp[card].name; + menu[name]=()=>{ + E.showMenu(); + var confirmRemove = new Layout ( + {type:"v", c: [ + {type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:"Delete"}, + {type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:name}, + {type:"h", c: [ + {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: "YES", cb:l=>{ + wp.splice(card, 1); + writeContact(); + mainMenu(); + }}, + {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: " NO", cb:l=>{mainMenu();}} + ]} + ], lazy:true}); + g.clear(); + confirmRemove.render(); + }; + }); + } + E.showMenu(menu); +} + +function askPosition(callback) { + let full = ""; + showNumpad("dddDDDddd", "", function() { + callback(key, ""); + }); +} + +function createContact(lat, name) { + let n = {}; + n["name"] = name; + n["number"] = lat; + wp.push(n); + print("add -- contacts", wp); + writeContact(); +} + +function addCardName2(key) { + g.clear(); + askPosition(function(lat, lon) { + print("position -- ", lat, lon); + createContact(lat, result); + mainMenu(); + }); +} + +function addCardName(key) { + result = key; + if (wp[result]!=undefined) { + E.showMenu(); + var alreadyExists = new Layout ( + {type:"v", c: [ + {type:"txt", font:Math.min(15,100/result.length)+"%", pad:1, fillx:1, filly:1, label:result}, + {type:"txt", font:"12%", pad:1, fillx:1, filly:1, label:"already exists."}, + {type:"h", c: [ + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "REPLACE", cb:l=>{ addCardName2(key); }}, + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "CANCEL", cb:l=>{mainMenu();}} + ]} + ], lazy:true}); + g.clear(); + alreadyExists.render(); + return; + } + addCardName2(key); +} + +function addCard() { + require("textinput").input({text:""}).then(result => { + if (result != "") { + addCardName(result); + } else + mainMenu(); + }); +} + +g.reset(); +Bangle.setUI(); +mainMenu(); diff --git a/apps/contacts/contacts.json b/apps/contacts/contacts.json new file mode 100644 index 000000000..40afa27dd --- /dev/null +++ b/apps/contacts/contacts.json @@ -0,0 +1,6 @@ +[ + { + "name":"EU emergency", + "number":"112" + } +] diff --git a/apps/contacts/interface.html b/apps/contacts/interface.html new file mode 100644 index 000000000..013478960 --- /dev/null +++ b/apps/contacts/interface.html @@ -0,0 +1,249 @@ + + + + + + + + + + + + + +

Contacts v.2

+
+
+ + + +
+
+ +
+
+
+ + + + + + + + + +
NameNumber
+
+

Add a new contact

+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + diff --git a/apps/contacts/metadata.json b/apps/contacts/metadata.json new file mode 100644 index 000000000..c466906b5 --- /dev/null +++ b/apps/contacts/metadata.json @@ -0,0 +1,19 @@ +{ "id": "contacts", + "name": "contacts", + "version":"0.01", + "description": "Provides means of storing user contacts, viewing/editing them on device and from the App loader", + "icon": "app.png", + "tags": "tool", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "interface": "interface.html", + "dependencies": {"textinput":"type"}, + "storage": [ + {"name":"contacts.app.js","url":"contacts.app.js"}, + {"name":"contacts.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"contacts.json","url":"contacts.json"} + ] +} diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index c3ea6041a..665d11afa 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -38,4 +38,5 @@ 0.30: Add clock info for showing and toggling recording state 0.31: Ensure that background-drawn tracks can get cancelled, and draw less at a time to make updates smoother plotTrack now draws the current track even if you're not actively recording -0.32: Add cadence data to output files \ No newline at end of file +0.32: Add cadence data to output files +0.33: Ensure that a new file is always created if the stuff that's being recorded has changed (fix #3081) \ No newline at end of file diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index a95ddf470..33034ae34 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.32", + "version": "0.33", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget,clkinfo", diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index 2525a96e4..585b97d52 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -159,6 +159,21 @@ return recorders; } + let getActiveRecorders = function() { + let activeRecorders = []; + let recorders = getRecorders(); + settings.record.forEach(r => { + var recorder = recorders[r]; + if (!recorder) { + console.log(/*LANG*/"Recorder for "+E.toJS(r)+/*LANG*/"+not found"); + return; + } + activeRecorders.push(recorder()); + }); + return activeRecorders; + }; + let getCSVHeaders = activeRecorders => ["Time"].concat(activeRecorders.map(r=>r.fields)); + let writeLog = function() { entriesWritten++; WIDGETS["recorder"].draw(); @@ -189,17 +204,9 @@ if (settings.recording) { // set up recorders - var recorders = getRecorders(); // TODO: order?? - settings.record.forEach(r => { - var recorder = recorders[r]; - if (!recorder) { - console.log(/*LANG*/"Recorder for "+E.toJS(r)+/*LANG*/"+not found"); - return; - } - var activeRecorder = recorder(); + activeRecorders = getActiveRecorders(); + activeRecorders.forEach(activeRecorder => { activeRecorder.start(); - activeRecorders.push(activeRecorder); - // TODO: write field names? }); WIDGETS["recorder"].width = 15 + ((activeRecorders.length+1)>>1)*12; // 12px per recorder // open/create file @@ -209,9 +216,7 @@ } else { storageFile = require("Storage").open(settings.file,"w"); // New file - write headers - var fields = ["Time"]; - activeRecorders.forEach(recorder => fields.push.apply(fields,recorder.fields)); - storageFile.write(fields.join(",")+"\n"); + storageFile.write(getCSVHeaders(activeRecorders).join(",")+"\n"); } // start recording... WIDGETS["recorder"].draw(); @@ -246,7 +251,8 @@ // if no filename set or date different, set up a new filename settings.file = getTrackFilename(); } - if (require("Storage").list(settings.file).length){ // if file exists + var headers = require("Storage").open(settings.file,"r").readLine(); + if (headers && headers.trim()==getCSVHeaders(getActiveRecorders()).join(",")){ // if file exists AND the headers match (#3081) if (!options.force) { // if not forced, ask the question g.reset(); // work around bug in 2v17 and earlier where bg color wasn't reset return E.showPrompt(