1
0
Fork 0

Merge branch 'espruino:master' into master

master
Ben Jabituya 2023-10-20 01:47:53 +01:00 committed by GitHub
commit 14ede8b3f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 370 additions and 139 deletions

View File

@ -1,14 +1,14 @@
(function (back) {
var FILE = "calendar.json";
const HOLIDAY_FILE = "calendar.days.json";
var settings = require('Storage').readJSON(FILE, true) || {};
var settings = require('Storage').readJSON(FILE, 1) || {};
if (settings.ndColors === undefined)
if (process.env.HWVERSION == 2) {
settings.ndColors = true;
} else {
settings.ndColors = false;
}
const holidays = require("Storage").readJSON(HOLIDAY_FILE,1).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
const holidays = (require("Storage").readJSON(HOLIDAY_FILE,1)||[]).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
function writeSettings() {
require('Storage').writeJSON(FILE, settings);

View File

@ -12,7 +12,8 @@
<a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">see the Bangle.js 1 instructions</a></b></p>
</div>
<ul>
<p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span> and DFU is <span id="boot-version" style="font-weight:bold">unknown</span></p>
<p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span> and DFU is <span id="boot-version" style="font-weight:bold">unknown</span>.
The DFU (bootloader) rarely changes, so it does not have to be the same version as your main firmware.</p>
</ul>
<div id="fw-ok" style="display:none">
<p id="fw-old-bootloader-msg">If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x DFU, the Firmware Update

View File

@ -2,3 +2,4 @@
0.02: Refactor code to store grocery list in separate file
0.03: Sort selected items to bottom and enable Widgets
0.04: Add settings to edit list on device
0.05: Drop app customiser as it is redundant with download interface

View File

@ -1,18 +1,19 @@
var filename = 'grocery_list.json';
var settings = require("Storage").readJSON(filename,1)|| { products: [] };
{
const filename = 'grocery_list.json';
const settings = require("Storage").readJSON(filename,1)|| { products: [] };
let menu;
function updateSettings() {
const updateSettings = function() {
require("Storage").writeJSON(filename, settings);
Bangle.buzz();
}
};
function twoChat(n){
const twoChat = function(n) {
if(n<10) return '0'+n;
return ''+n;
}
};
function sortMenu() {
const sortMenu = function() {
mainMenu.sort((a,b) => {
const byValue = a.value-b.value;
return byValue !== 0 ? byValue : a.index-b.index;
@ -20,7 +21,7 @@ function sortMenu() {
if (menu) {
menu.draw();
}
}
};
const mainMenu = settings.products.map((p,i) => ({
title: twoChat(p.quantity)+' '+p.name,
@ -35,9 +36,14 @@ const mainMenu = settings.products.map((p,i) => ({
}));
sortMenu();
mainMenu[''] = { 'title': 'Grocery list' };
mainMenu[''] = {
'title': 'Grocery list',
remove: () => {
},
};
mainMenu['< Back'] = ()=>{load();};
Bangle.loadWidgets();
menu = E.showMenu(mainMenu);
Bangle.drawWidgets();
}

View File

@ -1,116 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<h4>List of products</h4>
<table class="table">
<thead>
<tr>
<th>name</th>
<th>quantity</th>
<th>actions</th>
</tr>
</thead>
<tbody id="products">
</tbody>
</table>
<br><br>
<h4>Add a new product</h4>
<form id="add_product_form">
<div class="columns">
<div class="column col-4 col-xs-12">
<input class="form-input input-sm" type="text" id="add_product_name" placeholder="Name">
</div>
<div class="column col-4 col-xs-12">
<input class="form-input input-sm" value="1" type="number" id="add_product_quantity" placeholder="Quantity">
</div>
<div class="column col-4 col-xs-12">
<button id="add_product_button" class="btn btn-primary btn-sm">Add</button>
</div>
</div>
</form>
<br><br>
<button id="reset" class="btn btn-error">Reset</button> <button id="upload" class="btn btn-primary">Upload</button>
<script src="../../core/lib/customize.js"></script>
<script>
var products = []
try{
var stored = localStorage.getItem('grocery-product-list')
if(stored) products = JSON.parse(stored);
}catch(e){}
var $name = document.getElementById('add_product_name')
var $form = document.getElementById('add_product_form')
var $button = document.getElementById('add_product_button')
var $quantity = document.getElementById('add_product_quantity')
var $list = document.getElementById('products')
var $reset = document.getElementById('reset')
renderProducts()
$reset.addEventListener('click', reset)
$form.addEventListener('submit', event => {
event.preventDefault()
var name = $name.value.trim()
if(!name) return;
var quantity = parseInt($quantity.value)
products.push({
name, quantity,
ok: false
})
renderProducts()
$name.value = ''
$quantity.value = 1
save()
})
function save(){
localStorage.setItem('grocery-product-list',JSON.stringify(products));
}
function reset(){
products = []
save()
renderProducts()
}
function removeProduct(index){
products = products.filter((p,i) => i!==index)
save()
renderProducts()
}
function renderProducts(){
$list.innerHTML = ''
products.forEach((product,index) => {
var $product = document.createElement('tr')
$product.innerHTML = `<td>${product.name}</td><td>${product.quantity}</td><td><button class="btn btn-error" onclick="removeProduct(${index})">remove</button></td>`
$list.appendChild($product)
})
$name.focus()
}
document.getElementById("upload").addEventListener("click", function() {
sendCustomizedApp({
storage:[
{ name:"grocery_list.json", content: JSON.stringify({products: products}) }
]
});
});
</script>
</body>
</html>

View File

@ -80,7 +80,7 @@
// remove window
Util.hideModal();
settings = JSON.parse(data || "{products: []}");
settings = JSON.parse(data || '{"products": []}');
products = settings.products;
renderProducts();
});
@ -89,7 +89,6 @@
function save(){
settings.products = products;
Util.showModal("Saving...");
localStorage.setItem('grocery-product-list',JSON.stringify(products));
Util.writeStorage("grocery_list.json", JSON.stringify(settings), () => {
Util.hideModal();
});

View File

@ -1,13 +1,12 @@
{
"id": "grocery",
"name": "Grocery",
"version": "0.04",
"version": "0.05",
"description": "Simple grocery (shopping) list - Display a list of product and track if you already put them in your cart.",
"icon": "grocery.png",
"type": "app",
"tags": "tool,shopping,list",
"supports": ["BANGLEJS", "BANGLEJS2"],
"custom": "grocery.html",
"interface": "interface.html",
"allow_emulator": true,
"dependencies": {"textinput":"type"},

View File

@ -118,7 +118,7 @@
/*LANG*/"Edit List": () => editlist(),
/*LANG*/"Add item": () => {
settings.products.push({
"name":/*LANG*/"New item",
"name":/*LANG*/"New",
"quantity":1,
"ok":false
});

View File

@ -3,3 +3,4 @@
0.03: Do not clear outside of widget bar
0.04: Fork `widminbat`->`widminbate`. Only use the system theme foreground
colour.
0.05: Fix broken fork which removed the `update` function

View File

@ -1,7 +1,7 @@
{ "id": "widminbate",
"name": "Extra Minimal Battery",
"shortName":"ExtraMinBat",
"version":"0.04",
"version":"0.05",
"description": "An extra minimal (only use system theme foreground colour) version of the battery widget that only appears if the battery is running low (below 30%)",
"icon": "widget.png",
"type": "widget",

View File

@ -1,7 +1,7 @@
(()=>{
function getWidth() {
{
let getWidth = function() {
return E.getBattery() <= 30 || Bangle.isCharging() ? 40 : 0;
}
};
WIDGETS.minbate={area:"tr",width:getWidth(),draw:function() {
if(this.width < 40) return;
var s = 39;
@ -12,6 +12,7 @@
clearRect(x,y,x+s,y+23).
setColor(g.theme.fg).fillRect(x,y+2,x+s-4,y+21).clearRect(x+2,y+4,x+s-6,y+19).fillRect(x+s-3,y+10,x+s,y+14).//border
fillRect(x+4,y+6,x+4+barWidth,y+17);//indicator bar
},update: function() {
var newWidth = getWidth();
if(newWidth != this.width) {
this.width = newWidth;
@ -22,7 +23,7 @@
}};
setInterval(()=>{
var widget = WIDGETS.minbate;
if(widget) {widget.update();}
if(widget) widget.update();
}, 10*60*1000);
Bangle.on('charging', () => WIDGETS.minbate.update());
})();
}

233
modules/Slider.js Normal file
View File

@ -0,0 +1,233 @@
/* Copyright (c) 2023 Bangle.js contributors. See the file LICENSE for copying permission. */
// At time of writing in October 2023 this module is new and things are more likely to change during the coming weeks than in a month or two.
// See Slider.md for documentation
/* Minify to 'Slider.min.js' by: // TODO: Should we do this for Slider module?
* checking out: https://github.com/espruino/EspruinoDocs
* run: ../EspruinoDocs/bin/minify.js modules/Slider.js modules/Slider.min.js
*/
exports.create = function(cb, conf) {
const R = Bangle.appRect;
// Empty function added to cb if it's undefined.
if (!cb) cb = ()=>{};
let o = {};
o.v = {}; // variables go here.
o.f = {}; // functions go here.
// Default configuration for the indicator, modified by parameter `conf`:
o.c = Object.assign({ // constants go here.
initLevel:0,
horizontal:false,
xStart:R.x2-R.w/4-4,
width:R.w/4,
yStart:R.y+4,
height:R.h-10,
steps:30,
dragableSlider:true,
dragRect:R,
mode:"incr",
oversizeR:0,
oversizeL:0,
propagateDrag:false,
timeout:1,
drawableSlider:true,
colorFG:g.theme.fg2,
colorBG:g.theme.bg2,
rounded:true,
outerBorderSize:Math.round(2*R.w/176), // 176 is the # of pixels in a row on the Bangle.js 2's screen and typically also its app rectangles, used here to rescale to whatever pixel count is on the current app rectangle.
innerBorderSize:Math.round(2*R.w/176),
autoProgress:false,
},conf);
// If borders are bigger than the configured width, make them smaller to avoid glitches.
while (o.c.width <= 2*(o.c.outerBorderSize+o.c.innerBorderSize)) {
o.c.outerBorderSize--;
o.c.innerBorderSize--;
}
o.c.outerBorderSize = Math.max(0,o.c.outerBorderSize);
o.c.innerBorderSize = Math.max(0,o.c.innerBorderSize);
let totalBorderSize = o.c.outerBorderSize + o.c.innerBorderSize;
o.c.rounded = o.c.rounded?o.c.width/2:0;
if (o.c.rounded) o.c._rounded = (o.c.width-2*totalBorderSize)/2;
o.c.STEP_SIZE = ((o.c.height-2*totalBorderSize)-(!o.c.rounded?0:(2*o.c._rounded)))/o.c.steps;
// If horizontal, flip things around.
if (o.c.horizontal) {
let mediator = o.c.xStart;
o.c.xStart = o.c.yStart;
o.c.yStart = mediator;
mediator = o.c.width;
o.c.width = o.c.height;
o.c.height = mediator;
delete mediator;
}
// Make room for the border. Underscore indicates the area for the actual indicator bar without borders.
o.c._xStart = o.c.xStart + totalBorderSize;
o.c._width = o.c.width - 2*totalBorderSize;
o.c._yStart = o.c.yStart + totalBorderSize;
o.c._height = o.c.height - 2*totalBorderSize;
// Add a rectangle object with x, y, x2, y2, w and h values.
o.c.r = {x:o.c.xStart, y:o.c.yStart, x2:o.c.xStart+o.c.width, y2:o.c.yStart+o.c.height, w:o.c.width, h:o.c.height};
// Initialize the level
o.v.level = o.c.initLevel;
// Only add interactivity if wanted.
if (o.c.dragableSlider) {
let useMap = (o.c.mode==="map"||o.c.mode==="mapincr")?true:false;
let useIncr = (o.c.mode==="incr"||o.c.mode==="mapincr")?true:false;
const Y_MAX = g.getHeight()-1; // TODO: Should this take users screen calibration into account?
o.v.ebLast = 0;
o.v.dy = 0;
o.f.wasOnDragRect = (exFirst, eyFirst)=>{
"ram";
return exFirst>o.c.dragRect.x && exFirst<o.c.dragRect.x2 && eyFirst>o.c.dragRect.y && eyFirst<o.c.dragRect.y2;
};
o.f.wasOnIndicator = (exFirst)=>{
"ram";
if (!o.c.horizontal) return exFirst>o.c._xStart-o.c.oversizeL*o.c._width && exFirst<o.c._xStart+o.c._width+o.c.oversizeR*o.c._width;
if (o.c.horizontal) return exFirst>o.c._yStart-o.c.oversizeL*o.c._height && exFirst<o.c._yStart+o.c._height+o.c.oversizeR*o.c._height;
};
// Function to pass to `Bangle.on('drag', )`
o.f.dragSlider = e=>{
"ram";
if (o.v.ebLast==0) {
exFirst = o.c.horizontal?e.y:e.x;
eyFirst = o.c.horizontal?e.x:e.y;
}
// Only react if on allowed area.
if (o.f.wasOnDragRect(exFirst, eyFirst)) {
o.v.dragActive = true;
if (!o.c.propagateDrag) E.stopEventPropagation&&E.stopEventPropagation();
if (o.v.timeoutID) {clearTimeout(o.v.timeoutID); o.v.timeoutID = undefined;}
if (e.b==0 && !o.v.timeoutID && (o.c.timeout || o.c.timeout===0)) o.v.timeoutID = setTimeout(o.f.remove, 1000*o.c.timeout);
if (useMap && o.f.wasOnIndicator(exFirst)) { // If draging starts on the indicator, adjust one-to-one.
let input = !o.c.horizontal?
Math.min((Y_MAX-e.y)-o.c.yStart-3*o.c.rounded/4, o.c.height):
Math.min(e.x-o.c.xStart-3*o.c.rounded/4, o.c.width);
input = Math.round(input/o.c.STEP_SIZE);
o.v.level = Math.min(Math.max(input,0),o.c.steps);
o.v.cbObj = {mode:"map", value:o.v.level};
} else if (useIncr) { // Heavily inspired by "updown" mode of setUI.
o.v.dy += o.c.horizontal?-e.dx:e.dy;
//if (!e.b) o.v.dy=0;
while (Math.abs(o.v.dy)>32) {
let incr;
if (o.v.dy>0) { o.v.dy-=32; incr = 1;}
else { o.v.dy+=32; incr = -1;}
Bangle.buzz(20);
o.v.level = Math.min(Math.max(o.v.level-incr,0),o.c.steps);
o.v.cbObj = {mode:"incr", value:incr};
}
}
if (o.v.cbObj && (o.v.level!==o.v.prevLevel||o.v.level===0||o.v.level===o.c.steps)) {
cb(o.v.cbObj.mode, o.v.cbObj.value);
o.f.draw&&o.f.draw(o.v.level);
}
o.v.cbObj = null;
o.v.prevLevel = o.v.level;
o.v.ebLast = e.b;
}
};
// Cleanup.
o.f.remove = ()=> {
Bangle.removeListener('drag', o.f.dragSlider);
o.v.dragActive = false;
o.v.timeoutID = undefined;
cb("remove", o.v.level);
};
}
// Add standard slider graphics only if wanted.
if (o.c.drawableSlider) {
// Function for getting the indication bars size.
o.f.updateBar = (levelHeight)=>{
"ram";
if (!o.c.horizontal) return {x:o.c._xStart,y:o.c._yStart+o.c._height-levelHeight,w:o.c._width,y2:o.c._yStart+o.c._height,r:o.c.rounded};
if (o.c.horizontal) return {x:o.c._xStart,y:o.c._yStart,w:levelHeight,h:o.c._height,r:o.c.rounded};
};
o.c.borderRect = {x:o.c._xStart-totalBorderSize,y:o.c._yStart-totalBorderSize,w:o.c._width+2*totalBorderSize,h:o.c._height+2*totalBorderSize,r:o.c.rounded};
o.c.hollowRect = {x:o.c._xStart-o.c.innerBorderSize,y:o.c._yStart-o.c.innerBorderSize,w:o.c._width+2*o.c.innerBorderSize,h:o.c._height+2*o.c.innerBorderSize,r:o.c.rounded};
// Standard slider drawing method.
o.f.draw = (level)=>{
"ram";
g.setColor(o.c.colorFG).fillRect(o.c.borderRect). // To get outer border...
setColor(o.c.colorBG).fillRect(o.c.hollowRect). // ... and here it's made hollow.
setColor(0==level?o.c.colorBG:o.c.colorFG).fillRect(o.f.updateBar((!o.c.rounded?0:(2*o.c._rounded))+level*o.c.STEP_SIZE)); // Here the bar is drawn.
if (o.c.rounded && level===0) { // Hollow circle indicates level zero when slider is rounded.
g.setColor(o.c.colorFG).fillCircle(o.c._xStart+o.c._rounded, o.c._yStart+o.c._height-o.c._rounded, o.c._rounded).
setColor(o.c.colorBG).fillCircle(o.c._xStart+o.c._rounded, o.c._yStart+o.c._height-o.c._rounded, o.c._rounded-o.c.outerBorderSize);
}
};
}
// Add logic for auto progressing the slider only if wanted.
if (o.c.autoProgress) {
o.f.autoUpdate = ()=>{
o.v.level = o.v.autoInitLevel + Math.round((Date.now()-o.v.autoInitTime)/1000);
if (o.v.level>o.c.steps) o.v.level=o.c.steps;
cb("auto", o.v.level);
o.f.draw&&o.f.draw(o.v.level);
if (o.v.level==o.c.steps) {o.f.stopAutoUpdate();}
};
o.f.initAutoValues = ()=>{
o.v.autoInitTime=Date.now();
o.v.autoInitLevel=o.v.level;
};
o.f.startAutoUpdate = (intervalSeconds)=>{
if (!intervalSeconds) intervalSeconds = 1;
o.f.stopAutoUpdate();
o.f.initAutoValues();
o.f.draw&&o.f.draw(o.v.level);
o.v.autoIntervalID = setInterval(o.f.autoUpdate,1000*intervalSeconds);
};
o.f.stopAutoUpdate = ()=>{
if (o.v.autoIntervalID) {
clearInterval(o.v.autoIntervalID);
o.v.autoIntervalID = undefined;
}
o.v.autoInitLevel = undefined;
o.v.autoInitTime = undefined;
};
}
return o;
};

106
modules/Slider.md Normal file
View File

@ -0,0 +1,106 @@
Slider Library
==============
*At time of writing in October 2023 this module is new and things are more likely to change during the coming weeks than in a month or two.*
> Take a look at README.md for hints on developing with this library.
Usage
-----
```js
var Slider = require("Slider");
var slider = Slider(callbackFunction, configObject);
Bangle.on("drag", slider.f.dragSlider);
// If the slider should take precedent over other drag handlers use (fw2v18 and up):
// Bangle.prependListener("drag", slider.f.dragSlider);
```
`callbackFunction` (`cb`) (first argument) determines what `slider` is used for. `slider` will pass two arguments, `mode` and `feedback` (`fb`), into `callbackFunction` (if `slider` is interactive or auto progressing). The different `mode`/`feedback` combinations to expect are:
- `"map", o.v.level` | current level when interacting by mapping interface.
- `"incr", incr` | where `incr` == +/-1, when interacting by incrementing interface.
- `"remove", o.v.level` | last level when the slider times out.
- `"auto", o.v.level` | when auto progressing.
`configObject` (`conf`) (second argument, optional) has the following defaults:
```js
R = Bangle.appRect; // For use when determining defaults below.
{
initLevel: 0, // The level to initialize the slider with.
horizontal: false, // Slider should be horizontal?
xStart: R.x2-R.w/4-4, // Leftmost x-coordinate. (Uppermost y-coordinate if horizontal)
width: R.w/4, // Width of the slider. (Height if horizontal)
yStart: R.y+4, // Uppermost y-coordinate. (Rightmost x-coordinate if horizontal)
height: R.h-10, // Height of the slider. (Width if horizontal)
steps: 30, // Number of discrete steps of the slider.
dragableSlider: true, // Should supply the sliders standard interaction mechanisms?
dragRect: R, // Accept input within this rectangle.
mode: "incr", // What mode of draging to use: "map", "incr" or "mapincr".
oversizeR: 0, // Determines if the mapping area should be extend outside the indicator (Right/Up).
oversizeL: 0, // Determines if the mapping area should be extend outside the indicator (Left/Down).
propagateDrag: false, // Pass the drag event on down the handler chain?
timeout: 1, // Seconds until the slider times out. If set to `false` the slider stays active. The callback function is responsible for repainting over the slider graphics.
drawableSlider: true, // Should supply the sliders standard drawing mechanism?
colorFG: g.theme.fg2, // Foreground color.
colorBG: g.theme.bg2, // Background color.
rounded: true, // Slider should have rounded corners?
outerBorderSize: Math.round(2*R.w/176), // The size of the visual border. Scaled in relation to Bangle.js 2 screen width/typical app rectangle widths.
innerBorderSize: Math.round(2*R.w/176), // The distance between visual border and the slider.
autoProgress: false, // The slider should be able to progress automatically?
}
```
A slider initiated in the Web IDE terminal window reveals its internals to a degree:
```js
slider = require("Slider").create(()=>{}, {autoProgress:true})
={
v: { level: 0, ebLast: 0, dy: 0 },
f: {
wasOnDragRect: function (exFirst,eyFirst) { ... }, // Used internally.
wasOnIndicator: function (exFirst) { ... }, // Used internally.
dragSlider: function (e) { ... }, // The drag handler.
remove: function () { ... }, // Used to remove the drag handler and run the callback function.
updateBar: function (levelHeight) { ... }, // Used internally to get the variable height rectangle for the indicator.
draw: function (level) { ... }, // Draw the slider with the supplied level.
autoUpdate: function () { ... }, // Used to update the slider when auto progressing.
initAutoValues: function () { ... }, // Used internally.
startAutoUpdate: function (intervalSeconds) { ... }, // `intervalSeconds` defaults to 1 second if it's not supplied when `startAutoUpdate` is called.
stopAutoUpdate: function () { ... } // Stop auto progressing and clear some related values.
},
c: { initLevel: 0, horizontal: false, xStart: 127, width: 44,
yStart: 4, height: 166, steps: 30, dragableSlider: true,
dragRect: { x: 0, y: 0, w: 176, h: 176,
x2: 175, y2: 175 },
mode: "incr",
oversizeR: 0, oversizeL: 0, propagateDrag: false, timeout: 1, drawableSlider: true,
colorFG: 63488, colorBG: 8, rounded: 22, outerBorderSize: 2, innerBorderSize: 2,
autoProgress: true, _rounded: 18, STEP_SIZE: 4.06666666666, _xStart: 131, _width: 36,
_yStart: 8, _height: 158,
r: { x: 127, y: 4, x2: 171, y2: 170,
w: 44, h: 166 },
borderRect: { x: 127, y: 4, w: 44, h: 166,
r: 22 },
hollowRect: { x: 129, y: 6, w: 40, h: 162,
r: 22 }
}
}
>
```
Tips
----
You can implement custom graphics for a slider in the `callbackFunction`. The slider test app mentioned in the links below do this. To draw on top of the included slider graphics you need to wrap the drawing code in a timeout somewhat like so: `setTimeout(drawingFunction,0,fb)` (see [`setTimeout` documentation](https://www.espruino.com/Reference#l__global_setTimeout)).
Links
-----
There is a [slider test app on thyttan's personal app loader](https://thyttan.github.io/BangleApps/?q=slidertest) (at time of writing). Looking at [its code](https://github.com/thyttan/BangleApps/blob/ui-slider-lib/apps/slidertest/app.js) is a good way to see how the slider is used in app development.
The version of [Remote for Spotify on thyttan's personal app loader](https://thyttan.github.io/BangleApps/?q=spotrem) (at time of writing) also utilizes the `Slider` module. Here is [the code](https://github.com/thyttan/BangleApps/blob/ui-slider-lib/apps/spotrem/app.js).