Merge branch 'espruino:master' into master

master
kkayam 2025-02-16 17:51:16 +00:00 committed by GitHub
commit ec683724b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
134 changed files with 2650 additions and 222 deletions

View File

@ -270,7 +270,8 @@ and which gives information about the app for the Launcher.
// 'notify' - provides 'notify' library for showing notifications
// 'locale' - provides 'locale' library for language-specific date/distance/etc
// (a version of 'locale' is included in the firmware)
"tags": "", // comma separated tag list for searching
// 'defaultconfig' - a set of apps that will can be installed and will wipe out all previously installed apps
"tags": "", // comma separated tag list for searching (don't include uppercase or spaces)
// common types are:
// 'clock' - it's a clock
// 'widget' - it is (or provides) a widget

View File

@ -5,7 +5,7 @@
"version": "0.02",
"description": "A UI/UX for espruino smartwatches, displays dinamically calc. x,y coordinates.",
"icon": "app.png",
"tags": "Color,input,buttons,touch,UI",
"tags": "color,input,buttons,touch,ui",
"supports": ["BANGLEJS"],
"readme": "README.md",
"screenshots": [{"url":"UI4swatch_icon.png"},{"url":"UI4swatch_s1.png"}],

View File

@ -5,7 +5,7 @@
"version": "0.01",
"description": "Aerobic fitness test created by Léger & Lambert",
"icon": "beeptest.png",
"tags": "Health",
"tags": "health",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [

View File

@ -33,3 +33,4 @@ clkinfo.addInteractive that would cause ReferenceError.
0.31: Use clock_info module as an app
0.32: Make the border of the clock_info box extend all the way to the right of the screen.
0.33: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749)
0.34: Support 12-hour time format

View File

@ -239,11 +239,9 @@ let drawTime = function() {
var y = y1;
var date = new Date();
var hours = String(date.getHours());
var minutes = date.getMinutes();
minutes = minutes < 10 ? String("0") + minutes : minutes;
var colon = settings.hideColon ? "" : ":";
var timeStr = hours + colon + minutes;
var timeStr = locale.time(date, 1);
if (settings.hideColon)
timeStr = timeStr.replace(":", "");
// Set y coordinates correctly
y += parseInt((H - y)/2) + 5;

View File

@ -1,7 +1,7 @@
{
"id": "bwclk",
"name": "BW Clock",
"version": "0.33",
"version": "0.34",
"description": "A very minimalistic clock.",
"readme": "README.md",
"icon": "app.png",

View File

@ -36,3 +36,4 @@ clkinfo.addInteractive that would cause ReferenceError.
Do an quick inital fillRect on theclock info area.
0.33: Make the border of the clock_info box extend all the way to the right of the screen.
0.34: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749)
0.35: Support 12-hour time format

View File

@ -199,11 +199,9 @@ let drawTime = function() {
let y = y1;
let date = new Date();
let hours = String(date.getHours());
let minutes = date.getMinutes();
minutes = minutes < 10 ? String("0") + minutes : minutes;
let colon = settings.hideColon ? "" : ":";
let timeStr = hours + colon + minutes;
var timeStr = locale.time(date, 1);
if (settings.hideColon)
timeStr = timeStr.replace(":", "");
// Set y coordinates correctly
y += parseInt((H - y)/2) + 5;

View File

@ -1,7 +1,7 @@
{
"id": "bwclklite",
"name": "BW Clock Lite",
"version": "0.34",
"version": "0.35",
"description": "A very minimalistic clock. This version of BW Clock is quicker at the cost of the custom font.",
"readme": "README.md",
"icon": "app.png",

View File

@ -6,7 +6,7 @@
"description": "Show the current and upcoming events synchronized from Gadgetbridge",
"icon": "calclock.png",
"type": "clock",
"tags": "clock agenda",
"tags": "clock,agenda",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [

View File

@ -19,3 +19,5 @@
Display Widgets in menus
0.17: Load holidays before events so the latter is not overpainted
0.18: Minor code improvements
0.19: Read events synchronized from Gadgetbridge
0.20: Correct start time of all-day events synchronized from Gadgetbridge

View File

@ -60,6 +60,14 @@ const loadEvents = () => {
date.setSeconds(time.s);
return {date: date, msg: a.msg, type: "e"};
}));
// all events synchronized from Gadgetbridge
events = events.concat((require("Storage").readJSON("android.calendar.json",1) || []).map(a => {
// All-day events always start at 00:00:00 UTC, so we need to "undo" the
// timezone offsetting to make sure that the day is correct.
const offset = a.allDay ? new Date().getTimezoneOffset() * 60 : 0
const date = new Date((a.timestamp+offset) * 1000);
return {date: date, msg: a.title, type: a.allDay ? "o" : "e"};
}));
};
const loadSettings = () => {
@ -221,8 +229,8 @@ const drawCalendar = function(date) {
}, []);
let i = 0;
g.setFont("8x12", fontSize);
for (y = 0; y < rowN - 1; y++) {
for (x = 0; x < colN; x++) {
for (let y = 0; y < rowN - 1; y++) {
for (let x = 0; x < colN; x++) {
i++;
const day = days[i];
const curMonth = day < 15 ? month+1 : day < 50 ? month-1 : month;

View File

@ -1,7 +1,7 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.18",
"version": "0.20",
"description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],

View File

@ -0,0 +1 @@
0.01: New app!

View File

@ -0,0 +1,10 @@
# Shortcuts
An app that allows you to create custom ClockInfos that act as shortcuts to your favourite apps.
## Create a Shortcut
After installing the app, you can open the app to add and manage existing shortcuts. If no shortcuts exist, the app will display a ``[+] Shortcuts`` button as a ClockInfo on your Clock (see screenshots), which when clicked will quickly launch into the app so you can add new shortcuts.
When adding a shortcut, first select the app you wish to use, then select the icon you wish to register to the shortcut. If a keyboard is installed, you'll get the option to rename the shortcut before saving.
Once created, your shortcut will appear on any Clocks with ClockInfo support, and tapping the shortcut will launch the chosen app

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UB/4ACBIM889VAHmqAAwKCrQLH0oLCuBoFhoLCtJ1HtILBtYLH9ILBtALHlILQlRFCwALSnWwgECBY8O1gLJgeoBaojLHZfqAQILIFwMDBY8CFwMO2QLGhwuBlWuBY0K4ED1aDHBYJUBlQLGnRUChQLIKgJHHBYJUBZY8KgZUBBZHCKgILHh2CBZMC0fABZC+CBZBSBC5JUB14MDcY2q1YLIDAILGtY4EAAXpBYNpBY9pBYNauAKFhulBYWqAAwLCqoLHBQQA6A="))

224
apps/clkshortcuts/app.js Normal file
View File

@ -0,0 +1,224 @@
var storage = require("Storage");
var keyboard = "textinput";
try {
keyboard = require(keyboard);
} catch (e) {
keyboard = null;
}
var icons = [
{name:"agenda", img :"GBiBAAAAAAA8AAB+AA/n8B/n+BgAGBgAGBn/mBn/mBgAGBgAGBn/mBn/+BgD/BgHDhn+Bhn8MxgMIxgMIx/8Ew/+BgAHDgAD/AAA8A=="},
{name:"alarm", img:"GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAOBwAcA4A4YcAwYMBgYGBgYGBgYGBgYGBgeGBgHGAwBMA4AcAcA4AOBwAH/gAB+AAAAAAAAAA=="},
{name:"mail", img:"GBiBAAAAAAAAAAAAAAAAAB//+D///DAADDgAHDwAPDcA7DPDzDDnDDA8DDAYDDAADDAADDAADDAADD///B//+AAAAAAAAAAAAAAAAA=="},
{name:"android", img: "GBiBAAAAAAEAgAD/AAD/AAHDgAGBgAMkwAMAwAP/wBv/2BsA2BsA2BsA2BsA2BsA2BsA2Bv/2AP/wADnAADnAADnAADnAADnAAAAAA=="},
{name:"add", img:"GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgYGBgYGBgYGBgYGBn/mBn/mBgYGBgYGBgYGBgYGBgAGBgAGB//+A//8AAAAAAAAAAAAA=="},
{name:"bangle", img:"GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA=="},
{name:"bike", img:"GBiBAAAAAAAAAAAAAAAD+AAD/AADjAABjAfhnAfjyAMDwAcGwB4O+H8M/GGZ5sD7c8fzM8fjM8DDA2GBhn8B/h4AeAAAAAAAAAAAAA=="},
{name:"map", img:"GBiBAAAAAAAAAAAAAADgGAf8+B//+BjHGBjDGBjDGBjDGBjDGBjDGBjDGBjDGBjDGBjDGBjDGBjjGB//+B8/4BgHAAAAAAAAAAAAAA=="},
{name:"play", img:"GBiBAAAAAAAAAAAAAA//8B//+BgAGBjAGBjwGBj8GBjeGBjHmBjB2BjB2BjHmBjeGBj8GBjwGBjAGBgAGB//+A//8AAAAAAAAAAAAA=="},
{name:"fast forward", img:"GBiBAAAAAAAAAAAAAH///v///8AAA8YYA8eeA8f/g8b7w8Y488YYO8YYO8Y488b7w8f/g8eeA8YYA8AAA////3///gAAAAAAAAAAAA=="},
{name:"rewind", img:"GBiBAAAAAAAAAAAAAH///v///8AAA8AYY8B548H/48PfY88cY9wYY9wYY88cY8PfY8H/48B548AYY8AAA////3///gAAAAAAAAAAAA=="},
{name:"timer", img:"GBiBAAAAAAB+AAB+AAAAMAB+OAH/nAOByAcA4A4YcAwYMBgYGBgYGBgYGBgYGBgAGBgAGAwAMA4AcAcA4AOBwAH/gAB+AAAAAAAAAA=="},
{name:"connected", img:"GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBngGBn4GBgcGBgOGBnHGBnzGBgxmBgZmBmZmBmZmBgAGBgAGB//+A//8AAAAAAAAAAAAA=="},
{name:"lock", img:"GBiBAAAAAAA8AAD/AAHDgAGBgAMAwAMAwAMAwAf/4A//8AwAMAwAMAwAMAwYMAw8MAw8MAwYMAwAMAwAMAwAMA//8Af/4AAAAAAAAA=="},
{name:"battery", img:"GBiBAAAAAAAAAAB+AAB+AAHngAPnwAMAwAMAwAMIwAMIwAMYwAM4wAM+wAN8wAMcwAMYwAMQwAMQwAMAwAMAwAP/wAH/gAAAAAAAAA=="},
{name:"game", img:"GBiBAAAAAAAAAAAAAAA8AAB+AABmAABmAAB+AAA8AAAYAAAYAAAYAAMYAA//8B//+BgAGBgAGBgAGBgAGB//+A//8AAAAAAAAAAAAA=="},
{name:"dice", img:"GBiBAAAAAB//8D//+HAAPGMDHmeHnmeHnmMDHmAAHmMDHmeHnmeHnmMDHmAAHmMDHmeHnmeHnmMDHnAAPn///j///h///g///AAAAA=="},
{name:"gear", img:"GBiBAAAAAAAAAAA8AAB+AABmAA3nsA/D8B8A+Dg8HBx+OAznMAzDMAzDMAznMBx+ODg8HB8A+A/D8A3nsABmAAB+AAA8AAAAAAAAAA=="},
{name:"wrench", img:"GBiBAAAAAAAAAAAAAAAHgAAfwAA7gAAzEABjOABj+ABh+ABgGADgMAHAcAOP4AcfgA44AB9wADHgADHAADGAAB8AAA4AAAAAAAAAAA=="},
{name:"calendar", img:"FhgBDADAMAMP/////////////////////8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAP///////"},
{name:"power", img:"GBiBAAAAAAAAAAB+AAH/gAeBwA4YcAwYMBjbGBnbmDGZjDMYzDMYzDMAzDMAzDGBjBnDmBj/GAw8MA4AcAeB4AH/gAB+AAAAAAAAAA=="},
{name:"terminal", img:"GBiBAAAAAAAAAAAAAA//8B//+B//+B//+B//+BgAGBgAGBgAGBmAGBjAGBhgGBhgGBjAGBmPmBgAGBgAGB//+A//8AAAAAAAAAAAAA=="},
{name:"camera", img:"GBiBAAAAAAAAAAD/AAH/gAMAwD8A/H8A/mA8BmD/BmHDhmGBhmMAxmMAxmMAxmMAxmGBhmHDhmD/BmA8BmAABn///j///AAAAAAAAA=="},
{name:"phone", img:"GBiBAAAAAAAAAAOAAA/AABzgADBgADBgADBgABjgABjAABzAAAxgAA5wAAc58AMf+AGHHADgDABwDAA8GAAfGAAH8AAA4AAAAAAAAA=="},
{name:"two prong plug", img:"GBiBAAABgAADwAAHwAAPgACfAAHOAAPkBgHwDwP4Hwf8Pg/+fB//OD//kD//wD//4D//8D//4B//QB/+AD/8AH/4APnwAHAAACAAAA=="},
{name:"steps", img:"GBiBAAcAAA+AAA/AAA/AAB/AAB/gAA/g4A/h8A/j8A/D8A/D+AfH+AAH8AHn8APj8APj8AHj4AHg4AADAAAHwAAHwAAHgAAHgAADAA=="},
{name:"graph", img:"GBiBAAAAAAAAAAAAAAAAAAAAAADAAADAAAHAAAHjAAHjgAPngH9n/n82/gA+AAA8AAA8AAAcAAAYAAAYAAAAAAAAAAAAAAAAAAAAAA=="},
{name:"hills", img:"GBiBAAAAAAAAAAAAAAAAAAAAAAACAAAGAAAPAAEZgAOwwAPwQAZgYAwAMBgAGBAACDAADGAABv///////wAAAAAAAAAAAAAAAAAAAA=="},
{name:"sun", img:"GBiBAAAYAAAYAAAYAAgAEBwAOAx+MAD/AAHDgAMAwAcA4AYAYOYAZ+YAZwYAYAcA4AMAwAHDgAD/AAx+MBwAOAgAEAAYAAAYAAAYAA=="},
{name:"home", img:"GBiBAAAAAAAAAAAAAAH/gAP/wAdg4A5wYA44MBwf+DgP/BgAGBgAGBgAGBnnmBnnmBnnmBnnmBngGBngGB//+B//+AAAAAAAAAAAAA=="},
{name:"bell", img:"GBiBAAAAAAAAAAAfgAB/2ADw+AHAMAOAGAcAGD4ADHgADDgADBwADA4AHAcAGAOAOAHAcAPg4ANxwAM5gAP/AAHvAAAHAAACAAAAAA=="},
{name:"bin", img:"GBiBAAAAAAAAAAB+AB//+B//+AwAMAwAMAxmMAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYANmwAMAwAMAwAP/wAH/gAAAAAAAAA=="},
];
let storedApps;
var showMainMenu = () => {
storedApps = storage.readJSON("clkshortcuts.json", 1) || {};
var mainMenu = {
"": {
title: "Shortcuts",
},
"< Back": () => {
load();
},
"New": () => {
// Select the app
getSelectedApp().then((app) => {
getSelectedIcon().then((icon) => {
promptForRename(app.name).then((name) => {
E.showMessage("Saving...");
storedApps[app.src] = {
name: name, src: app.src, icon: icon
};
storage.writeJSON("clkshortcuts.json", storedApps);
showMainMenu();
}).catch(() => {
E.showMessage("Saving...");
storedApps[app.src] = {
name: app.name, src: app.src, icon: icon
};
storage.writeJSON("clkshortcuts.json", storedApps);
showMainMenu();
} );
}).catch(() => {showMainMenu();});
}).catch(() => {showMainMenu();});
},
};
getStoredAppsArray(storedApps).forEach((app) => {
mainMenu[app.name] = {
onchange: () => {
showEditMenu(app).then((dirty) => {
if (dirty) {
E.showMessage("Saving...");
storage.writeJSON("clkshortcuts.json", storedApps);
}
showMainMenu();
});
},
format: v=>"\0" + atob(app.icon)
};
});
E.showMenu(mainMenu);
};
var showEditMenu = (app) => {
return new Promise((resolve, reject) => {
var editMenu = {
"": {
title: "Edit " + app.name,
},
"< Back": () => {
resolve(false);
},
"Name":{
onchange: () => {
promptForRename(app.name).then((name) => {
storedApps[app.src].name = name;
resolve(true);
}).catch();
},
format: v=>app.name.substring(0, 7)
},
"Icon": {
onchange: () => {
getSelectedIcon().then((icon) => {
storedApps[app.src].icon = icon;
resolve(true);
}).catch(() => resolve(false));
},
format: v=>"\0" + atob(app.icon)
},
"Delete": {
onchange: () => {
delete storedApps[app.src]
resolve(true);
},
format: v=>"\0" + atob("GBiBAAAAAAAAAAB+AB//+B//+AwAMAwAMAxmMAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYANmwAMAwAMAwAP/wAH/gAAAAAAAAA==")
}
};
E.showMenu(editMenu);
});
};
var promptForRename = (name) => {
return new Promise((resolve, reject) => {
if (!keyboard) { reject("No textinput is available"); }
else {
return require("textinput").input({text:name}).then((result) => resolve(result));
}
});
};
var getStoredAppsArray = (apps) => {
var appList = Object.keys(apps);
var storedAppArray = [];
for (var i = 0; i < appList.length; i++) {
var app = "" + appList[i];
storedAppArray.push(
apps[app]
);
}
return storedAppArray;
};
var getSelectedIcon = () => {
return new Promise((resolve, reject) => {
var iconMenu = {
"": {
title: "Select Icon",
},
"< Back": () => {
reject("The user cancelled the operation");
},
};
icons.forEach((icon) => {
iconMenu["\0" + atob(icon.img) + " " + icon.name] = () => {
resolve(icon.img);
};
});
E.showMenu(iconMenu);
});
};
var getAppList = () => {
var appList = storage
.list(/\.info$/)
.map((appInfoFileName) => {
var appInfo = storage.readJSON(appInfoFileName, 1);
return (
appInfo && {
name: appInfo.name,
sortorder: appInfo.sortorder,
src: appInfo.src,
}
);
})
.filter((app) => app && !!app.src);
appList.sort((a, b) => {
var n = (0 | a.sortorder) - (0 | b.sortorder);
if (n) return n;
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
return appList;
};
var getSelectedApp = () => {
return new Promise((resolve, reject) => {
E.showMessage("Loading apps...");
var selectAppMenu = {
"": {
title: "Select App",
},
"< Back": () => {
reject("The user cancelled the operation");
},
};
var appList = getAppList();
appList.forEach((app) => {
selectAppMenu[app.name] = () => {
resolve(app);
};
});
E.showMenu(selectAppMenu);
});
};
showMainMenu();

BIN
apps/clkshortcuts/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,45 @@
(function() {
var storage = require("Storage");
var storedApps = storage.readJSON("clkshortcuts.json", 1) || {};
var items = [];
if (Object.keys(storedApps).length !== 0) {
for (var key in storedApps) {
var source = {
name: storedApps[key].name,
img: storedApps[key].icon,
src: storedApps[key].src,
get : function() {
return {
text : this.name,
img : atob(this.img)
}
},
run: function() { load(this.src);},
show : function() {},
hide : function() {},
}
items.push(source);
}
}
else {
var source = {
name: "Shortcuts",
img: "GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgYGBgYGBgYGBgYGBn/mBn/mBgYGBgYGBgYGBgYGBgAGBgAGB//+A//8AAAAAAAAAAAAA==",
src: "clkshortcuts.app.js",
get : function() {
return {
text : this.name,
img : atob(this.img)
}
},
run: function() { load(this.src);},
show : function() {},
hide : function() {},
};
items = [source];
}
return {
name: "Shortcuts",
items: items
};
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

View File

@ -0,0 +1,17 @@
{ "id": "clkshortcuts",
"name": "Shortcuts",
"shortName": "Shortcuts",
"version": "0.01",
"description": "Add shortcuts to launch your favourite apps straight from the Clock",
"icon": "app.png",
"screenshots": [{"url":"add_shortcuts_screenshot.png"}, {"url":"example_shortcuts_screenshot.png"}],
"tags": "clkinfo,clockinfo",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{ "name": "clkshortcuts.app.js", "url": "app.js" },
{ "name": "clkshortcuts.img", "url": "app-icon.js", "evaluate": true },
{"name":"clkshortcuts.clkinfo.js","url":"clkinfo.js"}
],
"data": [{"name":"clkshortcuts.json"}]
}

View File

@ -13,3 +13,4 @@
0.12: Add drawFilledImage to allow drawing icons with a separately coloured middle
0.13: Cache loaded ClockInfos so if we have clockInfoWidget and a clock, we don't load them twice (saves ~300ms)
0.14: Check for .clkinfocache and use that if exists (from boot 0.64)
0.15: Fix error when displaying a category with only one clockinfo (fix #3728)

View File

@ -283,7 +283,7 @@ exports.addInteractive = function(menu, options) {
//in the worst case we come back to 0
} while(menu[options.menuA].items.length==0);
// When we change, ensure we don't display the same thing as another clockinfo if we can avoid it
while ((options.menuB < menu[options.menuA].items.length) &&
while ((options.menuB < menu[options.menuA].items.length-1) &&
exports.clockInfos.some(m => (m!=options) && m.menuA==options.menuA && m.menuB==options.menuB))
options.menuB++;
}

View File

@ -1,7 +1,7 @@
{ "id": "clock_info",
"name": "Clock Info Module",
"shortName": "Clock Info",
"version":"0.14",
"version":"0.15",
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
"icon": "app.png",
"type": "module",

View File

@ -5,7 +5,7 @@
"version": "0.03",
"description": "Displays RGB565 and RGB888 colors, its name and code in screen.",
"icon": "app.png",
"tags": "Color,input,buttons,touch,UI",
"tags": "color,input,buttons,touch,ui",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [

View File

@ -0,0 +1,6 @@
0.01: New App!
0.02: update to my current preferences.
0.03: update app list
0.04: change app name "mysetup" -> "anotherconf"
0.05: remove apps that are not "core" to the experience.
0.06: change name "anotherconf" -> "confthyttan"

View File

@ -0,0 +1,31 @@
# Thyttan's Default Config
A different default set of apps and configurations. Brings many quality of life improvements. Opinionated based on the creators taste. Read more below before installing.
## Usage
Before installing do this:
1. Backup your current setup (via the "More..." tab of the App Loader) so you can restore it later if you want.
2. Install this app (you'll be prompted about all data being removed from your Bangle)
3. Try it out, switch out apps to your favorites and tweak to your liking!
## Features
There will not be a trace of a "Thyttan's Default Config" app on your watch after installation. Only the apps it installed and the configurations.
On the clock face:
- Swipe right on the screen to open the launcher (Desktop Launcher) - or press the hardware button.
- Swipe left to open a flashlight app.
- Swipe up to open the messages.
- Swipe down for quick access to music and podcast controls.
- (Do a subsequent left or right swipe to enter the listed apps)
- (Check out the "Quick Launch" app readme for more info)
## Requests
Add to the espruino/BangleApps issue tracker and mention @thyttan for bug reports and suggestions.
## Creator
thyttan

BIN
apps/confthyttan/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
{"mode":0,"apps":[{"name":"Run+","src":"runplus.app.js","files":"runplus.info,runplus.app.js,runplus.img,runplus.settings.js,runplus_karvonen"}],"timeout":10}

View File

@ -0,0 +1 @@
{"mode":0,"apps":[{"name":"Calculator","src":"calculator.app.js"},{"name":"SleepLog","src":"sleeplog.app.js"},{"name":"Messages","sortorder":-9,"src":"messagegui.app.js"},{"name":"Messages","sortorder":-9,"src":"messagegui.app.js","files":"messagegui.info,messagegui,messagegui.app.js,messagegui.new.js,messagegui.boot.js,messagegui.img"}],"standardNumSwipeHandlers":5,"standardNumDragHandlers":1}

View File

@ -0,0 +1 @@
{"showClocks":true,"showLaunchers":true,"direct":false,"swipeExit":false,"timeOut":"15s"}

View File

@ -0,0 +1 @@
{"buzzOnCharge":true,"monthFirst":true,"twentyFourH":true,"showAmPm":false,"showSeconds":true,"showWeather":false,"stepGoal":10000,"stepBar":true,"weekBar":true,"mondayFirst":true,"dayBar":true,"redrawOnStep":false}

View File

@ -0,0 +1 @@
{useAppHistory:true,disregardQuicklaunch:true,hideLoading:true}

View File

@ -0,0 +1 @@
{"colors":"011","image":"heart","touchOn":"always","oversize":7,"dragDelay":500,"minValue":0.01,"tapToLock":false,"unlockSide":"","tapSide":"","tapOn":"always","tOut":2000,"minFlash":0.2,"isOn":true,"value":0.9109}

View File

@ -0,0 +1 @@
{vibrateTimeout:10,vibrate:":",vibrateCalls:":::",flash:false}

View File

@ -0,0 +1,68 @@
{ "id": "confthyttan",
"name": "Thyttan's Default Config",
"version":"0.06",
"description": "A different default set of apps and configurations. Brings many quality of life improvements. Opinionated based on the creators taste. Read more below before installing.",
"icon": "app.png",
"type": "defaultconfig",
"tags": "system,configuration,config,anotherconfig,thyttan,defaultconfig",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"dependencies" : {
"sched":"app",
"kbmulti":"app",
"messageicons":"app",
"widmsggrid":"app",
"msgwakefup":"app",
"delaylock":"app",
"notify":"app",
"health":"app",
"widminbate":"app",
"podadrem":"app",
"spotrem":"app",
"android":"app",
"widanclk":"app",
"backswipe":"app",
"torch":"app",
"calculator":"app",
"widbt_notify":"app",
"smpltmr":"app",
"clkinfostopw":"app",
"runplus":"app",
"dtlaunch":"app",
"quicklaunch":"app",
"kineticscroll":"app",
"alarm":"app",
"recorder":"app",
"agenda":"app",
"edgeclk":"app",
"autoreset":"app",
"chargent":"app",
"setting":"app",
"fastload":"app",
"boot":"app",
"ateatimer":"app",
"drained":"app"
},
"storage": [
{"name":"backswipe.json",
"url":"backswipe.json"},
{"name":"autoreset.json",
"url":"autoreset.json"},
{"name":"dtlaunch.json",
"url":"dtlaunch.json"},
{"name":"fastload.json",
"url":"fastload.json"},
{"name":"quicklaunch.json",
"url":"quicklaunch.json"},
{"name":"messages.settings.json",
"url":"messages.settings.json"},
{"name":"widbt_notify.json",
"url":"widbt_notify.json"},
{"name":"recorder.json",
"url":"recorder.json"},
{"name":"edgeclk.settings.json",
"url":"edgeclk.settings.json"},
{"name":"setting.json",
"url":"setting.json"}
]
}

View File

@ -0,0 +1 @@
{lapp:{name:"Show Launcher",sortorder:-12,src:"no source"},rapp:{name:"torch",type:"app",sortorder:-11,src:"torch.app.js"},uapp:{name:"Messages",sortorder:-9,src:"messagegui.app.js"},dapp:{name:"Extension",type:"app",sortorder:-11,src:"quicklaunch.app.js"},tapp:{name:""},dlapp:{name:"PA Remote",src:"podadrem.app.js"},drapp:{name:"Remote for Spotify",src:"spotrem.app.js"},duapp:{name:""},ddapp:{name:"Extension",type:"app",sortorder:-11,src:"quicklaunch.app.js"},dtapp:{name:""},ddlapp:{name:"Alarms",src:"alarm.app.js"},ddrapp:{name:"Run+",src:"runplus.app.js"},dduapp:{name:""},dddapp:{name:"Agenda",src:"agenda.app.js"},ddtapp:{name:""},rlapp:{name:""},rrapp:{name:""},ruapp:{name:""},rdapp:{name:""},rtapp:{name:""},trace:"dr"}

View File

@ -0,0 +1 @@
{recording:false,period:10,record:["gps","hrm","steps"],file:"recorder.log0.csv"}

View File

@ -0,0 +1 @@
{ble:true,blerepl:true,log:false,quiet:0,timeout:10,vibrate:true,beep:true,timezone:2,HID:false,clock:"edgeclk.app.js","12hour":false,firstDayOfWeek:1,brightness:0.5,options:{wakeOnBTN1:true,wakeOnBTN2:true,wakeOnBTN3:true,wakeOnFaceUp:false,wakeOnTouch:false,wakeOnTwist:false,twistThreshold:819.2,twistMaxY:-800,twistTimeout:1000,btnLoadTimeout:700},theme:{fg:65535,bg:0,fg2:65535,bg2:8,fgH:65535,bgH:31,dark:true},clockHasWidgets:true,launcher:"dtlaunch.app.js",touch:{x1:6,y1:14,x2:197,y2:178}}

View File

@ -0,0 +1 @@
{showWidget:true,buzzOnConnect:false,buzzOnLoss:false,hideConnected:false,showMessage:false,nextBuzz:30000}

View File

@ -2,3 +2,4 @@
0.02: Added Settings & readme
0.03: Fix lint warnings
0.04: Fix lint warnings
0.05: Fix on not reading counter defaults in Settings

View File

@ -1,7 +1,7 @@
{
"id": "counter2",
"name": "Counter2",
"version": "0.04",
"version": "0.05",
"description": "Dual Counter",
"readme":"README.md",
"icon": "counter2-icon.png",

View File

@ -18,7 +18,7 @@
"": { "title": "Counter2" },
"< Back": () => back(),
'Default C1': {
value: settings[0],
value: settings.max0,
min: -99, max: 99,
onchange: v => {
settings.max0 = v;
@ -26,7 +26,7 @@
}
},
'Default C2': {
value: settings[2],
value: settings.max1,
min: -99, max: 99,
onchange: v => {
settings.max1 = v;

5
apps/daymoon/ChangeLog Normal file
View File

@ -0,0 +1,5 @@
0.01: First functional release
0.02: move moon down, rotate sunrise/sunset, shift Hours/minutes to corners
0.03: Change day and night to have different dots
0.04: Update ChangeLog, coerce dark theme
0.05: Add more screenshots, fix bug in dot colors

20
apps/daymoon/README.md Normal file
View File

@ -0,0 +1,20 @@
# DayMoon Circadian
This started out with a goal to recreate the Pebble [Fair Circadian watchface](https://setpebble.com/app/fair-circadian) by Matthew Clark for the Bangle.js2.
It ended up with me making a mostly new watchface that has the moon phase more prominent, but keeps the single dial 24 hour clock with daylight and sunset highlighted.
This uses the myLocation app to get your latitude and longitude for proper daylight calculations. If your location isn't set in that app, it will default to Nashua, NH. If your sunrise/sunset times aren't making sense, check that first!
## Future Development
Feature roadmap:
- [x] 0.01 Fix blocking widgets
- [x] 0.03 Day and Night different color markers
- [x] 0.04 Add to App Loader
- [x] 0.05 Add more screenshots with different moon phases
- [ ] 0.06 add Day of week and month display
- [ ] 0.07 Seconds display
- [ ] 0.08 Color Themes (and settings/options)
- [ ] 0.09 Moon display angle represents how it looks in the sky
- [ ] 0.10 custom/bigger/fitted time digits
- [ ] 0.20 clockinfo support?
- [ ] 0.30 Tap/swipe actions?

1
apps/daymoon/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4kA///hADBg/g/9/zuFA4Ocj+Nj+lgP4vP/o8AokAg8Ag0Ao//Mf4AptF2s+ICqNGulmkVkxF0wwsPo+EuUik13s14DBsIu1Iw4XBlGIs41BsAWKgl4vFnu1okVns+GsgyBtOZC5FkOQOHw1ItGGvGEAQWWhIuIxF2w92vAJDxGIs2JxFpC494wUos9HBQtHo2Sk+XRhFyk65ChWqqvAAoN5wUpuyMHxFos6FBCwOq5vM5sAzF3ygMCAAl3vDQBgEPCwOqr285nggFJw9IGA1os2IEIPz/QXB5m84vMGALbBNYsIBANnFwMz1Uzn/MqtbrhiBTgN2JAkHpCzBgGjmfzC4POGAmIo1nuAXEFwIHB+evmYXBrm89vM9nAhFnogvFZgNghU//8/mf69ns8vM93Ag1ovAvEAwOGUgIsB/55B9lc9ld5yRBtF2swXDw92sgXDR4P65h4B93M7gQBoyQEu0iRwKMBXwX/5p1B9nMr0Ao8ivAXDsUilAXC/4XBnQVBrncrfOgFCkUmC4eykUrC4R1B0f/O4PM53O3kAqUikIXDjci7YXB+Z3B1+qCwPsqoDBgG8kXlC4kV7x3B1//C4XMrovCO4O72NVC4fhiI6BhWqIoIXBIYPN5lc4EArtdjYXDhdd8PAC4M/18/F4PM4vF53Agsd7sQC4nB7jLBUgKOB1XFLgIAB4EBMgIzBAAUBiPBWYK+CAANc5wXCgHF8sdE4IvD5mx8JgCAAXO9neFwMAjobBI4kA2vMqOwGAnO9yoBgEF3fBroWEgHsjflWAIwD4tc6ouB5nc8sRC4sO9cd9wPBDAXlFwUO8ve8PQC4sAiPCldbAoIYB5hvC50SlfOCw0AivSkNb2IKF2Mc2Uu9YXHh0V9nrjbjEjsc3sV2qEBAAsigEe53hj3R3vbiMeitbfgK0BCAIADgQCBhtc2O7qOx8si7ns4PNqvgCAQXEDwUO93uj1c2Uilvh8vsaYIQDF4kgAgO+qsRjdSkUrjZKBCAxfEAAXt4PR5kijle5gQIABFdrxeBCBgA/ADg="))

260
apps/daymoon/app.js Normal file
View File

@ -0,0 +1,260 @@
const LOCATION_FILE = "mylocation.json";
let location;
var Utils = require("graphics_utils");
var SunCalc = require("suncalc");
var RADII = {
moon: 40,
arcMin: 48,
arcMax: 63,
dots: 55,
needle: 54,
};
var COL = {
moon: 65535, //
txture: 33792, // 0.5 ,0.5,0
shadow: 8196, // .125,0, .125
day: 40159, //0.6, 0.6,1
night: 6, // 0, 0, 0.2
ndots: 2047, // 0, 1, 1 cyan
ddots: 0,
needle: 63488, // 1, 0, 0 red
stime: 2047
};
const TAU = 2.0 * Math.PI;
const MX = g.getWidth() / 2,
MY = 24 + 3 + RADII.arcMax;
const DAY_MILLIS = 86400000;
const M_POS = {
x: MX,
y: MY,
r: RADII.moon
};
// images
const moon_texture = {
width: 80,
height: 80,
bpp: 1,
transparent: 0,
buffer: require("heatshrink").decompress(atob("ABsRqAJHkEiBA0N0uq1AIEgNVqtRqoJEgUiAAQJEioTBAAIzEl2q12oxATECQdVioJD/eqne60UCHQoADoAJBgf+xWrFIOACYUFCYo8Cj/73f70er0ROHAANUBIM//3///q1WIFAV1qtXCggJB//7CYO6keikBOHKAUDCIInClSgCgonBu4TK1W73ShBMQxkCh5OC//uFIInBi91q5PFCYISC3er//iOwXVE41UCYf+9//9AnCJopVBqEv/+/3//E4P6kUgJw4nDKAP+14TB1Xoq4hBEwYFBqgnB3Wr3e737KB/QnIqp3B32OKAYTBE4Z4BAoYnBEoRSC0fyE5ITBJ4WuCYP4J4J3CeQQFClbvBJgOqn5kBnRPKTwJMB1B4B92qEgQACJ4JTBkYnBYwOilYsBO5NUhYmB9+qxGC9TxBEYTvFqki3Y8B1Uikei3+oionIgGrO4OqwGC9H/xATK1/7E4UAnU7kATIqEAl/uE4WA12u0ATJgSgB/+ikUgnW70EFCY9AgGDE4PowEAlWowEBCZJ4BneggUgkRSBCZEAgEKJoIEBgEIAYQOCKYcVBIMqJgIEBgQSCgAQBqiJDRQIOBEwYAEMgNRiITBqKKBCYJJBE4xQGMQIABlBPHHgInDHQQjEAQJTCHgbFEABg8EBg5SDCgxNDABI="))
};
const needle = {
width: 23,
height: 10,
bpp: 1,
transparent: 0,
buffer: atob("//+B///D///AAAHgAADgAAHAAA9///j//+H//gA=")
};
/*
now use SunCalc.getMoonIllumination()
previously used these:
https://github.com/espruino/BangleApps/blob/master/apps/widmp/widget.js
https://github.com/deirdreobyrne/LunarPhase
modified to be based on millisec instead of sec, and to use tau = 2*pi
*/
// requires the myLocation app
function loadLocation() {
location = require("Storage").readJSON(LOCATION_FILE, 1) || {
"lat": 45,
"lon": -71.3,
"location": "Nashua"
}; //{"lat":51.5072,"lon":0.1276,"location":"London"};
}
function drawMoon(shadowShape) {
g.setColor(0, 0, 0).fillCircle(MX, MY, RADII.arcMax + 3);
g.setColor(COL.moon).fillCircle(MX, MY, RADII.moon - 1);
g.setColor(COL.txture).drawImage(moon_texture, MX, MY, {
rotate: 0
});
// TODO: can set the rotation here to the parallacticAngle from getMoonPosition
g.setColor(COL.shadow).fillPoly(shadowShape);
// TODO: set rotation of the fillPoly? parallactic-mp.angle I think.
// Use g.transformVertices to do the rotation
}
function drawDayRing(times) {
let r_ = RADII.arcMin;
let rm = RADII.arcMax;
let rd = RADII.dots;
let radT = [tToRad(times[0]), tToRad(times[1])];
let hhmm = [require("locale").time(times[0], 1), require("locale").time(times[1], 1)];
g.setColor(COL.day);
Utils.fillArc(g, MX, MY, r_, rm, radT[0], radT[1]);
g.setColor(COL.night);
Utils.fillArc(g, MX, MY, r_, rm, radT[1] - TAU, radT[0]);
// write sunrise/sunset times
g.setFont('6x8').setColor(COL.stime);
g.setFontAlign(0, 1, 3).drawString(hhmm[0], MX - rm - 2, MY);
g.setFontAlign(0, 1, 1).drawString(hhmm[1], MX + rm + 2, MY);
// draw dots
let edges = [];
let isDay = false;
let flag = false;
if (radT[1] > TAU) {
edges = [radT[1] - TAU, radT[0]];
g.setColor(COL.ddots);
isDay = true;
} else {
edges = [radT[0], radT[1]];
g.setColor(COL.ndots);
isDay = false;
}
for (var i = 0; i < 24; i++) {
let a = i * TAU / 24;
if (!flag && a > edges[0] && a < edges[1]) {
//first cross
if (isDay) {
g.setColor(COL.ndots);
} else {
g.setColor(COL.ddots);
}
flag = true;
} else if (flag && a > edges[1]) {
//second cross
if (isDay) {
g.setColor(COL.ddots);
} else {
g.setColor(COL.ndots);
}
flag = false;
}
let dotSize = (i % 3 == 0) ? 2 : 1;
let pX = MX + Math.cos(a) * rd;
let pY = MY + Math.sin(a) * rd;
g.fillCircle(pX, pY, dotSize);
}
let labels = ['6P', '12A', '6A', '12P'];
let qX = [rd - 9, 2, 11 - rd, 2];
let qY = [1, rd - 10, 1, 12 - rd];
g.setFont('4x6').setFontAlign(0, 0, 0).setColor(COL.ndots);
for (var j = 0; j < 4; j++) {
g.drawString(labels[j], MX + qX[j], MY + qY[j]);
}
}
function drawHHMM(d) {
var HM = require("locale").time(d, 1 /*omit seconds*/ ).split(":");
// write digital time
g.setBgColor(0, 0, 0).setColor(1, 1, 1).setFontVector(45);
g.setFontAlign(1, 1, 0).drawString(" " + HM[0], MX - 20, g.getHeight() + 3);
g.setFontAlign(-1, 1, 0).drawString(HM[1] + " ", MX + 30, g.getHeight() + 3);
// TODO: use the meridian text AM/PM or blank for 24 hr.
// var meridian = require("locale").meridian(d);
}
function moonShade(pos, mp) {
pos = pos !== undefined ? pos : M_POS;
mp = mp !== undefined ? mp : SunCalc.getMoonIllumination(new Date());
// pos has x,y, r for the drawing, mp is from SunCalc Moon Illumination
let k = mp.fraction;
// k is the percent along the equator of the terminator
const pts = Math.min(pos.r >> 1, 32);
// this gives r/2 pts on the way down and up, capped at 64 total for polyfill
let a = [],
b = [],
s1 = 1,
s2 = 0;
// scale s1 is 1 or -1 for fixed edge of the shadow; defined via case switches below
// scale s2 factor for the moving edge of the shadow
// need to do some computation to simplify for new/full moon if k 'close enough' to 0 or 1/-1
//
let isWaxing = (mp.phase < 0.5);
s1 = isWaxing ? -1 : 1;
s2 = isWaxing ? 1 - 2 * k : 2 * k - 1;
let tr = (pos.r + 1);
for (var i = 0; i < pts; i++) {
// down stroke on the outer shadow
var t = i * Math.PI / (pts + 1); //pts+1 so we leave the last point for the starting of going up
let cirX = Math.sin(t) * tr;
let cirY = Math.cos(t) * tr;
a.push(pos.x + s1 * cirX); //x
a.push(pos.y + cirY); //y
b.push(pos.x + s2 * cirX); //x for shadow edge
b.push(pos.y - cirY); //y going up for shadow edge
}
return a.concat(b);
}
function tToRad(date) {
date = (date !== undefined) ? new Date(date.getTime()) : new Date();
let milli = date - new Date(date.setHours(0, 0, 0, 0));
return (milli / DAY_MILLIS + 0.25) * TAU;
}
function draw(date) {
var d = date !== undefined ? date : new Date();
var a = tToRad(d),
shape = moonShade(M_POS, SunCalc.getMoonIllumination(d)),
sTimes = SunCalc.getTimes(d, location.lat, location.lon),
daylight = [sTimes.sunrise, sTimes.sunset];
//clear time area
g.clearRect(Bangle.appRect); //g.setColor(0).fillRect(0, 176 - 45, 176, 176);
drawMoon(shape);
drawDayRing(daylight);
drawHHMM(d);
// draw pointer
// TODO: Maybe later make this an overlay that can be removed?? -avoid drawing so much every minute/second
g.setColor(COL.needle).drawImage(needle, MX + RADII.needle * Math.cos(a), MY + RADII.needle * Math.sin(a), {
rotate: a
});
}
/*
const shotTimes = [1720626960000, 1729184400000, 1738298880000, 1717575420000];
let desc =`first quarter -2 days moon at 10:20 in the summer
jun 10 2024 10:56
full moon at 12 noon near fall equinox
Sep 17 2024 12:00
new moon at 11pm in winter
dec 30 2024 23:48
3rd quarter moon at 03:17 am
May 5 2024 03:17`
function screenshots(times) {
let d = new Date();
for (let t of times) {
d.setTime(t);
draw(d);
g.dump();
}
}
*/
// Clear the screen once, at startup
g.reset();
// requires the myLocation app
loadLocation();
g.setBgColor(0, 0, 0).clear();
// draw immediately at first
draw();
// now draw every second
// eventually maybe update the moon just every hour??
var secondInterval = setInterval(draw, 10000); //was 1000
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower', on => {
if (secondInterval) clearInterval(secondInterval);
secondInterval = undefined;
if (on) {
secondInterval = setInterval(draw, 10000); //was 1000
draw(); // draw immediately
}
});
/* Show launcher when middle button pressed
This should be done *before* Bangle.loadWidgets so that
widgets know if they're being loaded into a clock app or not */
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
g.setTheme({
fg: "#fff",
bg: "#000",
fg2: "#fff",
bg2: "#004",
fgH: "#fff",
bgH: "#00f",
dark: true
});
Bangle.drawWidgets();

BIN
apps/daymoon/daymoon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,17 @@
{ "id": "daymoon",
"name": "DayMoon Circadian Clock",
"version": "0.05",
"dependencies": {"mylocation":"app"},
"description": "A 24 hour clockface showing the Moon Phase and portion of the day that the Sun is up inspired by Matthew Clark's *Fair Circadian* Pebble watchface",
"icon": "daymoon.png",
"screenshots": [{"url":"s1.png"},{"url":"s2.png"},{"url":"s3.png"},{"url":"s4.png"}],
"type": "clock",
"tags": "clock,moon,lunar",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme":"README.md",
"storage": [
{"name":"daymoon.app.js","url":"app.js"},
{"name":"daymoon.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/daymoon/s1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
apps/daymoon/s2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
apps/daymoon/s3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
apps/daymoon/s4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,3 @@
0.01: Begin rewrite from old code.
0.02: Changed visuals: uA > mA, info order, battery state indication
0.03: Update app icon

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcBkmSpIC/AS0nwEHCh5yBggROgP4jgROh1ICKWT4Hkz0AC4NPgFypPgAQIRDyBLBCIMAiVAgECpAGBCInwn4RBg4dBoH/yV4j+ACI0Dz05kARB8gRJgARFgYRBgEB5IRKBwICCI4/8CIdJ/kCGoP+CIMPUIIRBkgRIYop9DgJHCPowDBTwUAyAREiSkBCITCOAX4CuwE5kmT4D1BBYSMByShBhwRCgEkWYQRDUIIRCUIWfEALXBCIsDCIMf+QICvEECIILBBAV5CIUcBAYRFpEEBYIRKnARIgFyHwfk+PAGogREgQIBPQMk+EACI9J/hrCyUHCIMHCJEnwAIByEBCIJrFCI/wWYIROwP5CIShECI7FBgjFDPoTFBgTXGBYICCCI6PDAX4C/ARoA="))

157
apps/denseclock/app.js Normal file
View File

@ -0,0 +1,157 @@
// FONTS
/*
Share Tech Mono: https://fonts.google.com/specimen/Share+Tech+Mono
Converted with: https://www.espruino.com/Font+Converter
*/
Graphics.prototype.setFontShareTechMonoBig = function(scale) {
// Actual height 56 (55 - 0)
this.setFontCustom(
atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAB+AAAAAAAAB+AAAAAAAAB+AAAAAAAAB+AAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAHwAAAAAAAA/wAAAAAAAD/wAAAAAAAf/wAAAAAAB//gAAAAAAP/8AAAAAAA//wAAAAAAH/+AAAAAAA//4AAAAAAD//AAAAAAAf/8AAAAAAB//gAAAAAAP/8AAAAAAB//wAAAAAAH/+AAAAAAA//4AAAAAAD//AAAAAAAf/4AAAAAAB//gAAAAAAP/8AAAAAAA//wAAAAAAA/+AAAAAAAA/4AAAAAAAA/AAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf///4AAAAD/////AAAAP/////wAAAf/////4AAA//////8AAA//////8AAB/AAB/j+AAB+AAH/B+AAB8AAP8A+AAB8AA/4A+AAB8AB/gA+AAB8AH/AA+AAB8AP8AA+AAB8A/4AA+AAB8B/gAA+AAB+D/AAB+AAA//+AAP8AAA//////8AAAf/////4AAAP/////wAAAH/////gAAAB////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAeAAAfgAAAA+AAAfAAAAA+AAA/AAAAA+AAA+AAAAA+AAA+AAAAA+AAB+AAAAA+AAB8AAAAA+AAB//////+AAB//////+AAB//////+AAB//////+AAB//////+AAB//////+AAAAAAAAA+AAAAAAAAA+AAAAAAAAA+AAAAAAAAA+AAAAAAAAA+AAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAB8AAAAD+AAB8AAAAP+AAB8AAAAf+AAB8AAAB/+AAB8AAAD/+AAB8AAAH/+AAB8AAAf8+AAB8AAA/4+AAB8AAD/w+AAB8AAH/A+AAB+AAf+A+AAB+AA/4A+AAA////wA+AAA////gA+AAAf//+AA+AAAP//8AA+AAAH//wAA+AAAB/+AAA+AAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAA+AAB8AAAAA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AD8AA+AAB+AH+AB+AAA////gD+AAA//////8AAAf/////8AAAP//P//4AAAH/+H//wAAAA/4D//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAP/wAAAAAAD//wAAAAAB///wAAAAAf///wAAAAH////wAAAB///+HwAAAB///gHwAAAB//wAHwAAAB/4AAHwAAAB+AAAHwAAABAAAAHwAAAAAAP///+AAAAAf///+AAAAAf///+AAAAAf///+AAAAAf///+AAAAAf///+AAAAAAAHwAAAAAAAAHwAAAAAAAAHwAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///wAA+AAB///wAA+AAB///wAA+AAB///wAA+AAB///wAA+AAB///wAA+AAB8AHwAA+AAB8AHwAA+AAB8AHwAA+AAB8AD4AA+AAB8AD4AA+AAB8AD4AB+AAB8AD+AD+AAB8AD///8AAB8AB///8AAB8AA///4AAB8AAf//wAAB4AAP//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAP/////gAAAf/////4AAAf/////4AAA//////8AAA/APgAD+AAB+APgAB+AAB8APgAA+AAB8APgAA+AAB8APgAA+AAB8APgAA+AAB8APgAA+AAB8APgAA+AAB8AHwAA+AAB8AHwAB+AAB8AH+AP8AAB8AH///8AAB8AD///4AAAAAB///wAAAAAA///gAAAAAAP/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAB8AAAAAAAAB8AAAAAAAAB8AAAAAAAAB8AAAAAOAAB8AAAAD+AAB8AAAAf+AAB8AAAD/+AAB8AAA//+AAB8AAH//+AAB8AB///gAAB8AP//8AAAB8D///AAAAB8f//4AAAAB////AAAAAB///wAAAAAB//+AAAAAAB//gAAAAAAB/8AAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//AAAAD/8H//wAAAP//P//4AAAf/////8AAA//////8AAA//////+AAB/AP+AB+AAB8AD8AA+AAB8AD8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AD8AA+AAB+AH+AA+AAA////AD+AAA//////8AAAf/////8AAAP//P//4AAAH/+H//wAAAA/4D//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAAAP//+AAAAAAf///AAAAAAf///AA+AAA////gA+AAB/AAfgA+AAB+AAPgA+AAB8AAPwA+AAB8AAHwA+AAB8AAHwA+AAB8AAHwA+AAB8AAHwA+AAB8AAHwA+AAB8AAHwB+AAB+AAHwB+AAA//////8AAA//////8AAAf/////4AAAP/////wAAAH/////gAAAB////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAB+AAAAB+AAB+AAAAB+AAB+AAAAB+AAB+AAAAB+AAB+AAAAB+AAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'),
46,
32,
60+(scale<<8)+(1<<16)
);
return this;
};
Graphics.prototype.setFontShareTechMono = function(scale) {
// Actual height 38 (37 - 0)
this.setFontCustom(
atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAeAAAAAB4AAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAfAAAAAP8AAAAD/gAAAB/4AAAAf+AAAAP/AAAAH/gAAAB/4AAAA/8AAAAP/AAAAH/gAAAD/wAAAA/8AAAAP+AAAAA/gAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//4AAA///8AAP///8AA////wAHwAfvgAeAH4eABwA/A4AHAHwDgAcB+AOAB4PgB4AHj8AHgAf///+AA////wAB///+AAD///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAOAA8AAA4ADwAADgAeAAAOAB4AAA4AH////gAf///+AB////4AH////gAAAAAOAAAAAA4AAAAADgAAAAAOAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AHAAAfgAcAAD+ABwAA/4AHAAH/gAcAA/OABwAP44AHgB+DgAfAfwOAA//8A4AD//gDgAH/4AOAAH+AA4AAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAOABwBwA4AHAHADgAcAcAOABwBwA4AHAHADgAeA+AOAB8H4B4AD////gAP///8AAf+f/gAAPgf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAP+AAAAP/4AAAf//gAAf//OAAB//A4AAH+ADgAAcAAOAAAAH//4AAA///gAAD//+AAAP//4AAAAHgAAAAAOAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/+ADgAf/4AOAB//gA4AH/+ADgAcA4AOABwDgA4AHAPADgAcA8AeABwD//4AHAH//AAcAP/4AAAAf/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///8AAH///4AA////wAH////gAeB4AeABwHgA4AHAeADgAcB4AOABwHgA4AHAeAHgAcA//+ABwD//wAAAH/+AAAAP/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAABwAAAAAHAAAAgAcAAAeABwAAf4AHAAP/gAcAH/+ABwH/+AAHD//AAAf//AAAB//gAAAH/gAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAP+f/gAD////AAP///+AB//8B4AHgPgDgAcAcAOABwBwA4AHAHADgAcAcAOAB4D4B4AH////gAP///8AAf/f/wAAfw/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAH/+AAAA//8A4AH4HwDgAeAHgOABwAeA4AHAB4DgAcAHgOABwAeB4AHgB4HgAf///+AA////wAB///+AAB///gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AHgAAHgAeAAAeAB4AAB4AHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='),
46,
22,
40+(scale<<8)+(1<<16)
);
return this;
};
Graphics.prototype.setFontShareTechMonoSmall = function(scale) {
// Actual height 23 (22 - 0)
this.setFontCustom(
atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zgP/zgP/zgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAPwAAPwAAAAAAPwAAPwAAPAAAAAAAAAAAAAAAAAAAAAAAAwYAP//gP//gP//gAwYAP//gP//gP//gAwYAAAAAAAAAAAAAAAAAB4AAD+BgH/Bg+HB8+DB8+DB8GD/gGB/AAAcAAAAAAAAAABAAH7AAP7AAMLAAMbAAP7AAHzfAAG/gAGxgAGxgAG/gAGfAAGAAAAAAAAAADz/AH//gP+DgMOBgMMBgMMBgMP/gAP/gAMAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAPwAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wP//8/AA+4AAGgAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAC4AAG/AB+P//8D//wAAAAAAAAAAAAAAAAAAAAAAAADAAADuAAB+AAP4AAfwAAP8AAB+AADsAADAAAAAAAAAAAAAAAAAAAADgAADgAADgAAf8AAf8AAf8AADgAADgAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAD+AAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAADgAADgAADgAADgAADgAADgAADgAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAADgAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAeAAD8AAfwAB+AAP4AA/AAH8AAfgAA8AAAwAAAAAAAAAAAAAAAD/+AH//APB/gMDxgMHBgMeBgP//gH//AD/+AAAAAAAAAAAAAAAAAGAAgGABgOABgOABgP//gP//gAABgAABgAABgAAAAAAAAAAAAAAAAAABgMAHgMAPgMA9gMB5gOHxgH/BgD+BgAABgAAAAAAAAAAAAAAAAAAAAMCBgMGBgMGBgMHBgP/DgH//gD5/AAAAAAAAAAAAAAAAAAAAAAD4AA/4AP/4APwYAMAYAAP/gAP/gAAcAAAYAAAAAAAAAAAAAAAAAAAAAP+BgP+BgMGBgMGBgMHDgMH/gMD/AAAAAAAAAAAAAAAAAAAAAD/+AH//AP//gMMBgMMBgMOBgMP/gMH/AAD+AAAAAAAAAAAAAAAAAMAAAMAAAMADgMAfgMH/AM/4AP/AAPwAAGAAAAAAAAAAAAAAAAAAAD5/AH//gP/DgMHBgMGBgMHBgP/jgH//gD5/AAAAAAAAAAAAAAAAAD+AAH/AgP/hgMBhgMBhgMBhgP//gH//AD/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcDgAcDgAcDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAcD+AcD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAHgAAPwAAMwAAc4AAY4AA4cAA4cABwMAAAAAAAAAAAAAAAAAAMYAAMYAAMYAAMYAAMYAAMYAAMYAAMYAAMYAAAAAAAAAAAAAAAAABwMAA4cAA4cAAY4AAc4AAMwAAPwAAHgAAHgAAAAAAAAAAAAAAAAAAAAAMAAAMABgMDzgMHzgOeAAH8AAD4AAAAAAAAAAAAAAB/8AH//AP//gMHhgMf5gM/9gMwNgM/5gM/8gMAMAP/8AH/8AA/wAAAAAAADgAA/gAP/gD/4AP8YAPAYAP8YAD/4AAP/gAA/gAADgAAAAAAAAAAAAP//gP//gMGBgMGBgMGBgMGBgP/DgH//gD5/AAAAAAAAAAAAAAAAAB/8AH//AP//gOABgMABgMABgMABgMABgMABgAAAAAAAAAAAAAAAAP//gP//gMABgMABgMABgMABgOADgH//gH//AAAAAAAAAAAAAAAAAP//gP//gP//gMGBgMGBgMGBgMGBgMGBgMABgAAAAAAAAAAAAAAAAP//gP//gP//gMHAAMHAAMHAAMHAAMHAAMAAAAAAAAAAAAAAAAAAAD/+AH//AP//gMABgMDBgMDBgMD/gMD/gAD/gAAAAAAAAAAAAAAAAP//gP//gAHAAAHAAAHAAAHAAAHAAP//gP//gAAAAAAAAAAAAAAAAAAAAMABgMABgP//gP//gP//gMABgMABgAAAAAAAAAAAAAAAAAAAAAAAAAABgMABgMABgMABgMAHgP//AP/+AAAAAAAAAAAAAAAAAAAAAP//gP//gAPAAAfgAB/4ADw+APAfgOAHgIABgAAAAAAAAAAAAAAAAAAAAP//gP//gAABgAABgAABgAABgAABgAAAAAAAAAAAAAAAAP//gP//gPgAAP+AAB/gAAHwAB/gAP8AAPgAAP//gP//gAAAAAAAAAAAAP//gP//gPwAAP/AAA/8AAD/gAAPgP//gP//gAAAAAAAAAAAAA/8AH//AH//gOADgMABgMABgMABgOADgH//gH//AA/4AAAAAAAAAAAAAP//gP//gMDgAMDgAMDgAMDgAP/AAH/AAD+AAAAAAAAAAAAAAA/8AH//AH//gOADgMABgMABgMABgOADgH//gH//wA/4wAAAAAAAAH//gP//gP//gMDAAMDAAMDgAOD8AP//AH+PgB4DgAAAgAAAAAAAAAAAAD4AAH+BgP+BgOGBgMHBgMHBgMH/gMD/AAA+AAAAAAAAAAAAAMAAAMAAAMAAAMAAAP//gP//gP//gMAAAMAAAMAAAMAAAAAAAAAAAAAAAP//AP//gAADgAABgAABgAABgAADgP//gP//AAAAAAAAAAAAAMAAAP4AAP/wAA//AAB/gAAHgAD/gB/+AP/AAPwAAIAAAAAAAAAAAP/gAP//gAP/gAAfgAH+AAHwAAD/gAAfgAf/gP//gP8AAAAAAAAAAAAAgOADgPgPgH4+AB/4AAfwAB/4AH4/APgPgOADgAAAgAAAAAAAAMAAAPAAAPwAAD8AAA//gAP/gAf/gD8AAPwAAPAAAIAAAAAAAAAAAAAAAMADgMAPgMA/gMD5gMPhgM+BgP4BgPgBgOABgAAAAAAAAAAAAAAAAAAAAAAAA///+///+wAAGwAAGwAAGAAAAAAAAAAAAAAAAAAAAwAAA+AAAfgAAH8AAA/AAAP4AAB+AAAPwAAD8AAAeAAAGAAAAAAAAAAAAAAAAAAAAwAAGwAAGwAAG///+///+AAAAAAAAAAAAAAAAAAAAAAAAA4AAD4AAPwAAeAAAYAAAeAAAPwAAD4AAAYAAAAAAAAAAAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAAAAAAAAAAAAAAIAAAMAAAOAAAGAAACAAAAAAAAAAAAAAAAAAAAAAAAAOAAY/gAY/gAZxgAZxgAZxgAZxgAf/gAP/gAABgAAAgAAAAAAAAAAAAP//gP//gAcBgAYBgAYBgAYBgAcDgAf/gAP/AAAAAAAAAAAAAAAAAAAAAAP/AAf/gAYBgAYBgAYBgAYBgAYBgAAAAAAAAAAAAAAAAAAAAAP/AAf/gAcDgAYBgAYBgAYBgAYDgP//gP//gAAAAAAAAAAAAAAAAAP+AAf/gAf/gAYxgAYxgAYxgAfxgAPxgAHwAAAAAAAAAAAAAAAAAAYBgAYBgD//gP//gP//gMYBgMYBgMYBgMQAAAAAAAAAAAAAAAAAAAPwGAf/GAYfGAYfGAYfGAYfGAY7mAf7+Afx8AAAAAAAAAAAAAAAAP//gP//gAcAAAYAAAYAAAYAAAcAAAf/gAP/gAAAAAAAAAAAAAAAAAYAAAYAAAYAAMf/gOf/gMf/gAABgAABgAABgAAAAAAAAAAAAAAAAAAAAAAAGAYAGAYAGAYAGMf/+Of/+Mf/8AAAAAAAAAAAAAAAAAAAAP//gP//gP//gADwAAH8AAefAAcHgAYDgAQAgAAAAAAAAAAAAEAAAMAAAMAAAMAAAP//AP//gAADgAABgAABgAABgAAAgAAAAAAAAAf/gAf/gAYAAAYAAAf/gAf/gAf/gAYAAAYAAAf/gAP/gAAAAAAAAAAAAAf/gAf/gAcAAAYAAAYAAAYAAAcAAAf/gAP/gAAAAAAAAAAAAAAAAAP/AAf/gAcDgAYBgAYBgAYBgAcDgAf/gAP/AAAAAAAAAAAAAAAAAAf/+Af/+AcBgAYBgAYBgAYBgAcDgAf/gAP/AAAAAAAAAAAAAAAAAAP/AAf/gAcDgAYBgAYBgAYBgAYDgAf/+Af/+AAAAAAAAAAAAAAAAAYBgAYBgAf/gAf/gAcBgAYBgAYBgAYAAAYAAAAAAAAAAAAAAAAAAAAAAAPhgAfxgAZxgAYxgAYxgAY/gAYfAAAAAAAAAAAAAAAAAAAAAAYAAAYAAAYAAD//AD//gAYBgAYBgAYBgAYBgAAAAAAAAAAAAAAAAAf+AAf/gAf/gAABgAABgAABgAf/gAf/gAf/gAAAAAAAAAAAAAQAAAeAAAf4AAH/AAAfgAADgAAfgAH/AAf4AAeAAAQAAAAAAAAAAAfAAAf/AAD/gAAPgAD/gAH4AAH/gAAPgAD/gAf+AAfAAAAAAAAAAAAAAAYBgAcHgAfPAAH+AAD4AAH+AAfPgAcDgAQBgAAAAAAAAAAAAAQAAAeAAAfwGAH+GAA/uAAD+AAf8AH/AAf4AAeAAAQAAAAAAAAAAAAAAAABgAYHgAYPgAY/gAZ5gAfxgAfBgAeBgAYAAAAAAAAAAAAAAAAAAAAAAADgAADgAf//8/+/+4AAGwAAGwAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//+///+f//+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAGwAAG4AAG/+/+f//8ADgAADgAAAAAAAAAAAAAAAAAAAAAADgAADAAADAAADAAADAAADgAABgAADgAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'),
32,
atob("DQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0H"),
24+(scale<<8)+(1<<16)
);
return this;
};
{
// VARS
let FONT_NAME = "ShareTechMono";
let BIG_FONT_HEIGHT = 60;
//let NORMAL_FONT_HEIGHT = 40;
let SMALL_FONT_HEIGHT = 24;
let timeDrawTimeout;
let infoDrawTimeout;
let lockState = Bangle.isLocked();
let pressure;
// LISTENERS
Bangle.on('lock', function(isLocked) {
lockState = isLocked;
timeDraw();
infoDraw();
});
// DRAW FUNCTIONS
let timeDraw = function() {
g.reset();
g.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y + BIG_FONT_HEIGHT);
var date = new Date();
var timeArray = [date.getHours().toString().padStart(2, "0"),
date.getMinutes().toString().padStart(2, "0")];
if (!lockState) timeArray.push(date.getSeconds().toString().padStart(2, "0"));
var timeString = timeArray.join(":");
g.setFontAlign(0, 0).setColor(g.theme.fg).setFont(FONT_NAME + (lockState ? "Big" : ""));
g.drawString(timeString, Bangle.appRect.x2/2, Bangle.appRect.y + BIG_FONT_HEIGHT/2);
if (timeDrawTimeout) clearTimeout(timeDrawTimeout);
timeDrawTimeout = setTimeout(function() {
timeDrawTimeout = undefined;
timeDraw();
}, (lockState ? 10000 - (Date.now() % 10000) : 1000 - (Date.now() % 1000))); // if locked, every clock's 10s, otherwise every 1s
};
let infoDraw = function() {
g.reset();
var date = new Date();
var dateString = [date.getFullYear().toString().padStart(4,"0"),
(date.getMonth()+1).toString().padStart(2,"0"),
date.getDate().toString().padStart(2,"0")].join("-");
var tzOffset = -(date.getTimezoneOffset())/60;
var tzOffsetString = (tzOffset >= 0 ? "+" + tzOffset : tzOffset);
var batteryString = (Bangle.isCharging() ? "+" : "") + E.getBattery() + "%";
var pressureString = (pressure ? pressure + "hPa" : "(hPa)");
var powerString = (E.getPowerUsage().total / 1000) + "mA";
var stepsString = Bangle.getHealthStatus("day").steps + "ST";
var bluetoothStatus = NRF.getSecurityStatus();
var bluetoothString = (bluetoothStatus.connected ? bluetoothStatus.connected_addr.split(" ")[0].substr(-5) : "N/C");
var infoMatrix = [
[dateString + tzOffsetString ],
[batteryString, pressureString],
[powerString ],
[stepsString, bluetoothString ]
];
g.clearRect(Bangle.appRect.x, Bangle.appRect.y + BIG_FONT_HEIGHT, Bangle.appRect.x2, Bangle.appRect.y2);
g.setFontAlign(0, -1).setColor(g.theme.fg2).setFont(FONT_NAME+"Small");
infoMatrix.forEach((lineArray, lineNumber) => {
g.drawString(lineArray.join(" "), Bangle.appRect.x2/2, Bangle.appRect.y + BIG_FONT_HEIGHT + SMALL_FONT_HEIGHT*lineNumber);
});
Bangle.getPressure().then(baroValue => { pressure=Math.round(baroValue.pressure); });
if (infoDrawTimeout) clearTimeout(infoDrawTimeout);
infoDrawTimeout = setTimeout(function() {
infoDrawTimeout = undefined;
infoDraw();
}, (lockState ? 60000 : 10000)); // if locked, a minute from now, otherwise in 10s
};
// DRAW CALLS
g.clear();
Bangle.setUI({
mode: "clock",
remove: function() {
if (timeDrawTimeout) clearTimeout(timeDrawTimeout);
timeDrawTimeout = undefined;
if (infoDrawTimeout) clearTimeout(infoDrawTimeout);
infoDrawTimeout = undefined;
delete Graphics.prototype.setFontShareTechMono;
delete Graphics.prototype.setFontShareTechMonoBig;
delete Graphics.prototype.setFontShareTechMonoSmall;
}});
Bangle.loadWidgets();
Bangle.drawWidgets();
timeDraw();
infoDraw();
}

BIN
apps/denseclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,18 @@
{ "id": "denseclock",
"name": "Dense Clock",
"shortName":"Dense Clock",
"version":"0.03",
"description": "A clockface dense with text-only information. Switches between showing seconds and minutes when unlocked/locked, in the interest of saving power.",
"icon": "app.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"denseclock.app.js","url":"app.js"},
{"name":"denseclock.img","url":"app-icon.js","evaluate":true}
],
"screenshots": [
{"url":"screenshot_locked.png"},
{"url":"screenshot_unlocked.png"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -6,3 +6,4 @@
0.05: Enhance menu: permit toggling bluetooth
0.06: Display clock in green when charging, with "charging" text
0.07: Correctly restore full power when the charged threshold is reached
0.08: Redisplay immediately on changes to charging status

View File

@ -118,6 +118,7 @@ Bangle.on("charging", function (charging) {
drainedInterval = clearInterval(drainedInterval);
if (charging)
drainedInterval = setInterval(checkCharge, interval * 60 * 1000);
draw();
});
if (!keepStartup) {
var storage = require("Storage");

View File

@ -151,6 +151,7 @@ Bangle.on("charging", charging => {
drainedInterval = clearInterval(drainedInterval) as undefined;
if(charging)
drainedInterval = setInterval(checkCharge, interval * 60 * 1000);
draw(); // redraw to update charging status on screen
});
if(!keepStartup){

View File

@ -1,7 +1,7 @@
{
"id": "drained",
"name": "Drained",
"version": "0.07",
"version": "0.08",
"description": "Switches to displaying a simple clock when the battery percentage is low, and disables some peripherals",
"readme": "README.md",
"icon": "icon.png",

View File

@ -12,3 +12,4 @@
0.11: Use default Bangle formatter for booleans
0.12: Issue newline before GB commands (solves issue with console.log and ignored commands)
0.13: Upgrade to new translation system
0.14: Fix auto-start saved state; fix clearing track number; allow widget clicks

View File

@ -91,8 +91,7 @@ function rScroller(l) {
const w = g.stringWidth(l.label)+40,
y = l.y+l.h/2;
l.offset = l.offset%w;
g.setClipRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
.setColor(l.col).setBgColor(l.bgCol) // need to set colors: iScroll calls this function outside Layout
g.setColor(l.col).setBgColor(l.bgCol) // need to set colors: iScroll calls this function outside Layout
.setFontAlign(-1, 0) // left center
.clearRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
.drawString(l.label, l.x-l.offset+40, y)
@ -128,57 +127,8 @@ function rInfo(l) {
.setFontAlign(0, -1) // center top
.drawString(l.label, l.x+l.w/2, l.y);
}
/**
* Render icon
* @param l
*/
function rIcon(l) {
const x2 = l.x+l.w-1,
y2 = l.y+l.h-1;
switch(l.icon) {
case "pause": {
const w13 = l.w/3;
g.drawRect(l.x, l.y, l.x+w13, y2);
g.drawRect(l.x+l.w-w13, l.y, x2, y2);
break;
}
case "play": {
g.drawPoly([
l.x, l.y,
x2, l.y+l.h/2,
l.x, y2,
], true);
break;
}
case "previous": {
const w15 = l.w*1/5;
g.drawPoly([
x2, l.y,
l.x+w15, l.y+l.h/2,
x2, y2,
], true);
g.drawRect(l.x, l.y, l.x+w15, y2);
break;
}
case "next": {
const w45 = l.w*4/5;
g.drawPoly([
l.x, l.y,
l.x+w45, l.y+l.h/2,
l.x, y2,
], true);
g.drawRect(l.x+w45, l.y, x2, y2);
break;
}
default: { // red X
console.log(`Unknown icon: ${l.icon}`);
g.setColor("#f00")
.drawRect(l.x, l.y, x2, y2)
.drawLine(l.x, l.y, x2, y2)
.drawLine(l.x, y2, x2, l.y);
}
}
}
let layout;
function makeUI() {
Bangle.loadWidgets();
@ -417,7 +367,7 @@ function handleButton2Press() {
let tCommand = {};
/**
* Send command and highlight corresponding control
* @param {string} command - "play"/"pause"/"next"/"previous"/"volumeup"/"volumedown"
* @param {"play"|"pause"|"playpause"|"next"|"previous"|"volumeup"|"volumedown"} command
*/
function sendCommand(command) {
Bluetooth.println("");
@ -433,15 +383,21 @@ function sendCommand(command) {
drawControls();
}
function handleTouch(btn, pos) {
if (pos === undefined || pos.y >= Bangle.appRect.y) {
togglePlay();
}
}
function togglePlay() {
sendCommand(stat==="play" ? "pause" : "play");
sendCommand("playpause");
}
/**
* Setup touch+swipe for Bangle.js 1
*/
function touch1() {
Bangle.on("touch", togglePlay);
Bangle.on("touch", handleTouch);
Bangle.on("swipe", dir => {
sendCommand(dir===1 ? "previous" : "next");
});
@ -450,7 +406,7 @@ function touch1() {
* Setup touch+swipe for Bangle.js 2
*/
function touch2() {
Bangle.on("touch", togglePlay);
Bangle.on("touch", handleTouch);
// swiping
let drag;
Bangle.on("drag", e => {
@ -483,10 +439,9 @@ function startLCDWatch() {
Bangle.on("lcdPower", (on) => {
if (on) {
// redraw and resume scrolling
tick();
layout.render();
fadeOut();
if (offset.offset!==null) {
if (layout.title.offset!==null) { // Making an assumption about what offset.offset was supposed to be
if (!iScroll) {
iScroll = setInterval(scroll, 200);
}

View File

@ -1,6 +1,6 @@
setTimeout( // make other boot code run first, so we override e.g. android.boot.js GB
() => {
const APP = global.__FILE__==="gbmusic.app.js",
const APP = globalThis.__FILE__==="gbmusic.app.js",
a = !!(require("Storage").readJSON("gbmusic.json", 1) || {}).autoStart;
let s, i; // state, info
@ -10,7 +10,7 @@ setTimeout( // make other boot code run first, so we override e.g. android.boot.
* Only runs while other apps are loaded
*/
function check() {
if (s!=="play" || !i || !a || !Bangle.CLOCK) return; // only launch app if we know which song we are playing, and autoLoad is enabled
if ((!s || s.state!=="play") || !i || !a || !Bangle.CLOCK) return; // only launch app if we know which song we are playing, and autoLoad is enabled
delete (i.t);
// store info and launch music app
require("Storage").writeJSON("gbmusic.load.json", {
@ -20,18 +20,19 @@ setTimeout( // make other boot code run first, so we override e.g. android.boot.
load("gbmusic.app.js");
}
global.GB = (_GB => e => {
globalThis.GB = (_GB => e => {
// we eat music events!
switch(e.t) {
case "musicinfo":
i = e;
return APP ? info(e) : check();
return APP ? globalThis.info(e) : check();
case "musicstate":
s = e.state;
return APP ? state(e) : check();
s = e;
return APP ? globalThis.state(e) : check();
default:
// pass on other events
if (_GB) setTimeout(_GB, 0, e);
}
})(global.GB);
})(globalThis.GB);
}, 1);

View File

@ -2,7 +2,7 @@
"id": "gbmusic",
"name": "Gadgetbridge Music Controls",
"shortName": "Music Controls",
"version": "0.13",
"version": "0.14",
"description": "Control the music on your Gadgetbridge-connected phone",
"icon": "icon.png",
"screenshots": [{"url":"screenshot_v1_d.png"},{"url":"screenshot_v1_l.png"},

View File

@ -0,0 +1,2 @@
0.01: New Widget!
0.02: rename, new icon, settings menu!

View File

@ -0,0 +1,26 @@
# Grandfather Clock
A widget that runs in the background and chimes on every (configurable) fraction of an hour, similar to Chimer, and counts out the fractions and the o'clock hour.
## Usage
Once installed, see the App Settings page for options.
Defaults:
- Twelve hour mode is ENABLED.
- Swap meridian is DISABLED. (in the AM, there will be a single buzz after counting the hours. in the PM, there will be two buzzes after counting the hours)
- The attention buzz for the hour chime is 1000ms long.
- The buzz for each hour count is 250ms long.
- The buzz for each fraction count is 250ms long.
- The widget will count out 4 fractions of an hour (a 15 min interval).
- The time between count buzzes is 500ms.
- The meridian buzzes are 50ms long.
- The time between meridian buzzes is 300ms.
## Requests
Drop me a message at @yogsoy on Discord if you need help / discover a bug that I can squash for you.
## Creator
Written by June B (yogsoy), inspired by aaronrolls' Chimer.

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

View File

@ -0,0 +1,18 @@
{ "id": "grandfatherclock",
"name": "Grandfather Clock Widget",
"shortName":"Grandfather Clock",
"version":"0.02",
"description": "A widget that chimes every fraction of an hour (similar to Chimer), and counts out the fractions and the o'clock hour.",
"icon": "icon.png",
"type": "widget",
"tags": "widget",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"grandfatherclock.wid.js","url":"widget.js"},
{"name":"grandfatherclock.settings.js","url":"settings.js"}
],
"data": [
{"name":"grandfatherclock.json"}
]
}

View File

@ -0,0 +1,89 @@
(function(back) {
const configFile = "grandfatherclock.json";
let config = Object.assign({
draw_widget: true,
twelve_hour: true,
swap_meridian: false,
hour_attention_buzz_ms: 1000,
hour_count_buzz_ms: 250,
fraction_count_buzz_ms: 250,
fractions_of_hour: 4, // 4 = 15min intervals, 6 = 10min intervals
wait_ms: 500,
meridian_buzz_ms: 50,
meridian_buzz_wait_ms: 300
}, require('Storage').readJSON("grandfatherclock.json", true) || {});
let writeConfig = function() {
require('Storage').writeJSON(configFile, config);
};
E.showMenu({
"": {"title" : "Grandfather Clock"},
"< Back": () => back(),
"Draw widget": {
value: config.draw_widget,
onchange: v => {
config.draw_widget = v;
writeConfig();
}
},
"12 hour": {
value: config.twelve_hour,
onchange: v => {
config.twelve_hour = v;
writeConfig();
}
},"Swap meridian": {
value: config.swap_meridian,
onchange: v => {
config.swap_meridian = v;
writeConfig();
}
},"Hr attn. buzz length (ms)": {
value: config.hour_attention_buzz_ms,
onchange: v => {
config.hour_attention_buzz_ms = v;
writeConfig();
}
},"Hr count buzz (ms)": {
value: config.hour_count_buzz_ms,
onchange: v => {
config.hour_count_buzz_ms = v;
writeConfig();
}
},"Frac. count buzz (ms)": {
value: config.fraction_count_buzz_ms,
onchange: v => {
config.fraction_count_buzz_ms = v;
writeConfig();
}
},"Fracs. of hour": {
value: config.fractions_of_hour,
onchange: v => {
config.fractions_of_hour = v;
writeConfig();
}
},"Count wait (ms)": {
value: config.wait_ms,
onchange: v => {
config.wait_ms = v;
writeConfig();
}
},"Meridian buzz (ms)": {
value: config.meridian_buzz_ms,
onchange: v => {
config.meridian_buzz_ms = v;
writeConfig();
}
},"Meridian wait (ms)": {
value: config.meridian_buzz_wait_ms,
onchange: v => {
config.meridian_buzz_wait_ms = v;
writeConfig();
}
}
});
})

View File

@ -0,0 +1,81 @@
(() => {
// sensible defaults
let config = Object.assign({
draw_widget: true,
twelve_hour: true,
swap_meridian: false,
hour_attention_buzz_ms: 1000,
hour_count_buzz_ms: 250,
fraction_count_buzz_ms: 250,
fractions_of_hour: 4, // 4 = 15min intervals, 6 = 10min intervals
wait_ms: 500,
meridian_buzz_ms: 50,
meridian_buzz_wait_ms: 300
}, require('Storage').readJSON("grandfatherclock.json", true) || {}); // or, load the app settings file.
WIDGETS["grandfatherclock"] = {
area: "tr",
width: config.draw_widget ? 16 : 0,
draw: function() {
if (config.draw_widget) {
g.reset();
g.drawImage(atob("EBiDASSTJJISSSSZJJJCSSTJ///ISSZP///5CTJ/////ITJ/////ITJ/+B//ITJ/+B//ITJ//+P/ITJ/////ISZP///5CSRJ///ICSQJJJJACSYBJJIBCSYABgABCSYABgABCSYAJAABCSYANgABCSYBtgABCSYNtsABCSYBtgABCSYAMAABCSYAAAABCSZJJJJJCQ=="), this.x, this.y);
}
}
};
let date;
let fractionMs = 3600000 / config.fractions_of_hour;
let chime = function () {
date = new Date();
let hourFrac = Math.floor(date.getMinutes() / (60 / config.fractions_of_hour));
if (hourFrac == 0) { // if it's an o'clock hour
let chimeHour = (config.twelve_hour ? date.getHours() % 12 : date.getHours());
if (chimeHour == 0) (config.twelve_hour ? chimeHour += 12 : chimeHour += 24);
Bangle.buzz(config.hour_attention_buzz_ms).then(() => { // initial buzz
setTimeout(hourChime, config.wait_ms, chimeHour); // wait a period before doing the first chime
});
} else { // if it's a fraction of an hour
fractionChime(hourFrac);
}
queueNextChime();
};
let hourChime = function (hoursLeftToChime) {
hoursLeftToChime--;
Bangle.buzz(config.hour_count_buzz_ms).then(() => { // recursive. buzz and wait to do the next buzz.
if (hoursLeftToChime > 0) {
setTimeout(hourChime, config.wait_ms, hoursLeftToChime);
} else if (config.twelve_hour) { // once finished with the hour count
setTimeout(meridianChime, config.wait_ms, (date.getHours() >= 12)); // if in twelve hour mode, queue up the meridian chime.
}
});
};
let fractionChime = function (fractionsLeftToChime) {
fractionsLeftToChime--;
Bangle.buzz(config.fraction_count_buzz_ms).then(() => { // recursive. buzz and wait to do the next buzz.
if (fractionsLeftToChime > 0) setTimeout(fractionChime, config.wait_ms, fractionsLeftToChime);
});
};
let meridianChime = function (meridian) {
if ((config.swap_meridian ? !meridian : meridian)) { // default: if PM
Bangle.buzz(config.meridian_buzz_ms).then(setTimeout(Bangle.buzz, config.meridian_buzz_wait_ms, config.meridian_buzz_ms)); // buzz once, wait, buzz again.
} else { // default: if AM
Bangle.buzz(config.meridian_buzz_ms); // buzz once.
}
};
let queueNextChime = function () {
let msUntilNextFraction = fractionMs - (Date.now() % fractionMs);
setTimeout(chime, msUntilNextFraction);
};
queueNextChime();
})()

View File

@ -7,7 +7,7 @@
/* eslint-env node */
var imageconverter = require("../../../webtools/imageconverter.js").imageconverter;
var imageconverter = require("../../../webtools/imageconverter.js");
var icons = JSON.parse(require("fs").readFileSync(__dirname+"/icon_names.json"));
const imgOptions = {
mode : "1bit",

View File

@ -261,7 +261,7 @@ var locales = {
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d %B %Y", "1": "%d/%m/%Y" }, // 1 mars 2020 // 01/03/2020
abmonth: "janv,févr,mars,avril,mai,juin,juil,août,sept,oct,nov,déc",
abmonth: "janv,févr,mars,avr,mai,juin,juil,août,sept,oct,nov,déc",
month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
abday: "dim,lun,mar,mer,jeu,ven,sam",
day: "dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi",
@ -423,7 +423,7 @@ var locales = {
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%A %B %d %Y", "1": "%d/%m/%y" }, // dimanche 1 mars 2020 // 01/03/20
abmonth: "janv.,févr.,mars,avril,mai,juin,juil.,août,sept.,oct.,nov.,déc.",
abmonth: "janv,févr,mars,avr,mai,juin,juil,août,sept,oct,nov,déc",
month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
abday: "dim,lun,mar,mer,jeu,ven,sam",
day: "dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi",
@ -471,7 +471,7 @@ var locales = {
ampm: { 0: "AM", 1: "PM" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%A %d %B %Y", "1": "%d/%m/%y" }, // dimanche 1 mars 2020 // 01/03/20
abmonth: "janv.,févr.,mars,avril,mai,juin,juil.,août,sept.,oct.,nov.,déc.",
abmonth: "janv,févr,mars,avr,mai,juin,juil,août,sept,oct,nov,déc",
month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
abday: "dim,lun,mar,mer,jeu,ven,sam",
day: "dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi",
@ -567,7 +567,7 @@ var locales = {
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%A %d %B de %Y", "1": "%d/%m/%Y" }, // dimenge 1 de març de 2020 // 01/03/2020
abmonth: "gen.,febr.,març,abril,mai,junh,julh,ago.,set.,oct.,nov.,dec.",
abmonth: "gen,febr,març,abril,mai,junh,julh,ago,set,oct,nov,dec",
month: "genièr,febrièr,març,abril,mai,junh,julhet,agost,setembre,octòbre,novembre,decembre",
abday: "dg,dl,dm,dc,dj,dv,ds",
day: "dimenge,diluns,dimars,dimècres,dijòus,divendres,dissabte",
@ -612,10 +612,10 @@ var locales = {
speed: "km/h",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "dop.", 1: "pop." },
ampm: { 0: "dop", 1: "pop" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%-d. %b %Y", 1: "%-d.%-m.%Y" }, // "3. jan. 2020" // "3.1.2020"(short)
abmonth: "sij.,velj.,ožu.,tra.,svi,lip.,srp.,kol.,ruj.,lis.,stu.,pro.",
abmonth: "sij,velj,ožu,tra,svi,lip,srp,kol,ruj,lis,stu,pro",
month: "siječanj,veljača,ožujak,travanj,svibanj,lipanj,srpanj,kolovoz,rujan,listopad,studeni,prosinac",
abday: "ned.,pon.,uto.,sri.,čet.,pet.,sub.",
day: "nedjelja,ponedjeljak,utorak,srijeda,četvrtak,petak,subota",
@ -628,7 +628,7 @@ var locales = {
speed: "km/h",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "dop.", 1: "pop." },
ampm: { 0: "dop", 1: "pop" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%-d. %b %Y", 1: "%-d.%-m.%Y" }, // "3. jan. 2020" // "3.1.2020"(short)
abmonth: "jan.,feb.,mar.,apr.,maj,jun.,jul.,avg.,sep.,okt.,nov.,dec.",
@ -728,7 +728,7 @@ var locales = {
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d %B %Y", "1": "%d/%m/%y" },
abmonth: "gen.,febr.,març,abr.,maig,juny,jul.,ag.,set.,oct.,nov.,des.",
abmonth: "gen,febr,març,abr,maig,juny,jul,ag,set,oct,nov,des",
month: "gener,febrer,març,abril,maig,juny,juliol,agost,setembre,octubre,novembre,desembre",
abday: "dg.,dl.,dt.,dc.,dj.,dv.,ds.",
day: "diumenge,dilluns,dimarts,dimecres,dijous,divendres,dissabte",

View File

@ -15,6 +15,6 @@
{"name":"messagesoverlay.settings.js","url":"settings.js"},
{"name":"messagesoverlay.default.json","url":"default.json"}
],
"data": [{"name":"bthrm.json"}],
"data":[{"name":"messagesoverlay.json"}],
"screenshots": [{"url":"screen_call.png"} ,{"url":"screen_message.png"} ]
}

View File

@ -30,3 +30,8 @@
1.28: increased vibration strength, added some comments, & some QOL
1.29: changed image
1.30: changed image, again
1.40: added various settings for controlling when & how to throw dice
1.41: fixed dumb mistake
1.42: okay maby I should've read the *whole* error log
1.43: playing whackamole with ESLint
1.44: fixed (?) settings app

View File

@ -1,12 +1,20 @@
var menu = true; // default to have the selection menu open
var settings = Object.assign({
vibrate: true,
shake: true,
screen: false,
shake_timeout: 200,
shake_duration: 100,
}, require('Storage').readJSON("multidice.json", true) || {});
var menu = true; // defaults to having the menu open
const DICE_ARRAY = [0, 4, 6, 8, 10, 12, 20, 100]; // 0 means nothing selected
const SELECTION_ARRAY = [6, 0, 0, 0, 0, 0, 0, 0]; // default to selecting a single d20
// function to draw the selection menu
function drawMenu() {
stringArr = new Array ("", "", "", "", "", "", "", "");
for (i = 0; i < 8; i++) {
var stringArr = new Array ("", "", "", "", "", "", "", "");
for (var i = 0; i < 8; i++) {
if (SELECTION_ARRAY [i] != 0) {
@ -41,6 +49,7 @@ function touchHandler (button, xy) {
return;
}
var selection;
if (xy.x <= 87) { // left
if (xy.y <= 43) { // first
@ -84,15 +93,30 @@ function touchHandler (button, xy) {
drawMenu();
}
var shaken = false;
var last_shaken = null;
function accelHandler (xyz) {
// if the screen should be on *and* it isn't, return
if (settings.screen && ! Bangle.isBacklightOn()) {
return;
}
if (xyz.diff >= 0.3) {
menu = false;
mutex (rollDice).catch (() => {
shaken = true;
last_shaken = Date.now();
} else if (shaken && last_shaken !== null) {
return; // not necessary, but prevents spamming the logs
});
if (Date.now() - last_shaken > settings.shake_timeout) {
last_shaken = null;
shaken = false;
menu = false;
mutex (rollDice).catch (() => { return; });
}
}
}
@ -123,8 +147,8 @@ function mutex (functionRef) {
// function to roll all selected dice, and display them
function rollDice() {
resultsArr = new Uint8Array (8);
for (i = 0; i < 8; i++) {
var resultsArr = new Uint8Array (8);
for (var i = 0; i < 8; i++) {
if (SELECTION_ARRAY [i] != 0) {
@ -135,7 +159,7 @@ function rollDice() {
g.clear();
g.setFont ("Vector", 40);
for (i = 0; i < 4; i++) {
for (var i = 0; i < 4; i++) {
if (SELECTION_ARRAY [i] != 0) {
@ -143,7 +167,7 @@ function rollDice() {
}
}
for (i = 4; i < 8; i++) {
for (var i = 4; i < 8; i++) {
if (SELECTION_ARRAY [i] != 0) {
@ -157,14 +181,19 @@ function rollDice() {
// triggers the vibration, then pauses before returning
function vibrate() {
if (! settings.vibrate) {
return (Promise.resolve (0));
}
return new Promise ((resolve, reject) => {
return Bangle.buzz (50, 1).then ((value) => {
return Bangle.buzz (settings.shake_duration, 1).then ((value) => {
setTimeout (() => {
resolve (value);
}, 200);
}, 2 * settings.shake_duration + settings.shake_timeout);
});
});
}
@ -177,7 +206,7 @@ function random (max) {
drawMenu();
Bangle.on ('touch', touchHandler);
Bangle.on ('accel', accelHandler);
if (settings.shake) { Bangle.on ('accel', accelHandler); }
setWatch (function() {
menu = false;

View File

@ -1,7 +1,7 @@
{ "id": "multidice",
"name": "multiple dice roller",
"shortName":"multidice",
"version":"1.30",
"version":"1.44",
"description": "roll anywhere from 1-8 dice at the same time",
"icon": "app.png",
"tags": "tool,game",
@ -10,6 +10,8 @@
"allow_emulator": true,
"storage": [
{"name":"multidice.app.js","url":"app.js"},
{"name":"multidice.settings.js","url":"settings.js"},
{"name":"multidice.img","url":"app-icon.js","evaluate":true}
]
],
"data": [{"name": "multidice.json"}]
}

View File

@ -0,0 +1,58 @@
(function(back) {
var settings = Object.assign({
vibrate: true,
shake: true,
screen: false,
shake_timeout: 200,
shake_duration: 100,
}, require('Storage').readJSON("multidice.json", true) || {});
function writeSettings() {
require('Storage').writeJSON("multidice.json", settings);
}
// Show the menu
E.showMenu({
"" : { "title" : "multi dice roll" },
"< Back" : () => back(),
'vibrate on roll?': {
value: !!settings.vibrate,
onchange: v => {
settings.vibrate = v;
writeSettings();
}
},
'allow shaking?': {
value: !!settings.shake,
onchange: v => {
settings.shake = v;
writeSettings();
}
},
'screen on to shake?': {
value: !!settings.screen,
onchange: v => {
settings.screen = v;
writeSettings();
}
},
'shake timeout': {
value: settings.shake_timeout / 5,
min: 10, max: 40,
format: v => v * 5,
onchange: v => {
settings.shake_timeout = v * 5;
writeSettings();
}
},
'shake duration': {
value: settings.shake_duration / 5,
min: 10, max: 40,
format: v => v * 5,
onchange: v => {
settings.shake_duration = v * 5;
writeSettings();
}
},
});
})

56
apps/pacer/README.md Normal file
View File

@ -0,0 +1,56 @@
## Pacer
![icon](app.png)
Run with a virtual partner at your chosen pace, and export the GPX data
from the Bangle.js App Store.
## Usage
Pacer starts up with a menu.
* **Recording** - whether to record the run
* **Units** - imperial or metric
* **Lap** - the multiple of a mile or kilometer to use for splits
* **Dark mode** - use black or white background
* **Eco battery** - display will turn off after 10 seconds
* **Eco storage** - only record GPS position every 10 seconds
* **Steps** - display step count or cadence
* **Pacer** - pace of virtual partner
On selecting **Start**, GPS position will be detected. A run cannot be
started without a GPS fix. The watch touchscreen is disabled while the
app is running.
The app will run on Bangle.js 1 and 2, although use on Bangle.js 2 is not
recommended due to poor GPS accuracy.
On a Bangle.js 1, the top button reverses the screen colours, the middle
button starts, pauses or resumes a run, and the bottom button ends the run.
On a Bangle.js 2, a short press of the button starts, pauses or resumes a
run, and a long press (over 0.5 seconds, but under 2!) ends the run. Note
that holding the button for 2 seconds will exit back to the default clock
app.
## Downloading
GPX tracks can be downloaded using the
[App Loader](https://banglejs.com/apps/?id=pacer). Connect the
Bangle.js and click on the Pacer app's disk icon to see the tracks
available for downloading.
## Tips
For best results, only start a run when the satellite signal strength bar is
green.
Use the [Assisted GPS Updater](https://banglejs.com/apps/#AGPS) to improve
the time taken to get a GPS fix.
## Bugs
The eco settings are unlikely to be useful.
GPS track smoothing is accomplished simply by reducing the frequency with
which readings are taken, depending on signal strength.

1
apps/pacer/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4cA///un/+2Eqee1nV+X26NjtfNGLsf+AQOg+kzAROnskyfACJs5CINACJshCKAjMgfJBoPwgpHLiGSpMk3kHyneO5OECIMl23Aj+AO5QRD7ZSJgPpknZkmNCIJFJhFJk1wgFtCIN4CJFCpMwAgIRCt5HIoVN8B/BCIVmEZFyt6RCCIb7Ih5SCmYRCkjoM7YRB78k6ARO++kzwQKhgRC4GEzARKswRD8mT///Lg8DtmbCILvEEw8DkmzCIUcCIWTN40bkmWUoUCCIWbCJGSrIpC5YoB5x7HCINLRZcAg3bsgwBsARNtgRBlgRLmyNBCIMlwAQJgLkBtuyMwIRHvJUBpP2GoMGCIOwCI1SBQOSr3bA4PJSQKtGSoWSpXYBAMZAwKcGoQRCpoLCZAOSoARFh5HCk4IDtmpnikMAAMOpO8CJ0KpKQKAAlyfoQANqVMCByGBt4ROgWSfhgACjmTvAROimTCB0A8mbYgwAIwmYEZ/07wRPj/wCJ4AI"))

843
apps/pacer/app.js Normal file
View File

@ -0,0 +1,843 @@
Bangle.loadWidgets();
g.clear();
Bangle.drawWidgets();
var cfg;
try {
cfg = require("Storage").readJSON("pacer.json",true)||{};
}
catch(err) {
cfg = {};
}
if (process.env.BOARD == 'BANGLEJS2')
var bangle2 = true;
else
var bangle2 = false;
var laps = ["Off","0.25","0.5","1","2","5","10"];
var fg = 1;
var fixed = false;
var started = false;
var startHidden = false;
var recording = false;
var invID = 0;
var intID = 0;
var startID = 0;
var cadID = 0;
var finID = 0;
var lapID = 0;
var steps = 0;
var sats = 0;
var ctr = 0;
var elapsed_ms = 0;
var finish_ms = 0;
var lap_start_ms = 0;
var lap_ms;
var gps = {fix:0,satellites:0};
var fp;
var start_time;
var current_time;
var paused_time = 0;
var last_time = 0;
var begin_pause;
var next_lap = 0.0;
var skip_ctr = 0;
var skip_max = 0;
var force_write = true;
var show_lap = false;
var lcd_on = true;
var drawSats = true;
var dist = 0.0;
var pdist = 0.0;
var oldDist = 0.0;
var oldLat = -1;
var oldLon = -1;
var cadence = 0;
var pace = 0;
var ppace = 0;
var R = 6371;
var stepTimes = [];
var dists = [];
function pace_str(pval) {
var psecs = 295 + 5 * pval;
return ''+Math.floor(psecs/60)+':'+('0'+psecs%60).substr(-2);
}
function defaults() {
if (typeof(cfg.record) != 'boolean')
cfg.record = true;
if (typeof(cfg.metric) != 'boolean')
cfg.metric = false;
if (typeof(cfg.lap_idx) != 'number')
cfg.lap_idx = 3;
if (typeof(cfg.dark) != 'boolean')
cfg.dark = true;
if (typeof(cfg.eco) != 'boolean')
cfg.eco = false;
if (typeof(cfg.storage) != 'boolean')
cfg.storage = false;
if (typeof(cfg.show_steps) != 'boolean')
cfg.show_steps = false;
if (typeof(cfg.pacer) != 'number')
cfg.pacer = 0;
fg = cfg.dark?1:0;
}
function genFilename() {
var today=new Date();
return ('.pacer'+today.getFullYear()+('0'+(today.getMonth()+1)).substr(-2)+('0'+today.getDate()).substr(-2)+('0'+today.getHours()).substr(-2)+('0'+today.getMinutes()).substr(-2)+('0'+today.getSeconds()).substr(-2)+'.csv');
}
function doCadence() {
if (steps > 0)
clearInterval(cadID);
cadID = setTimeout(function() {
cadence = 0;
}, 2000);
if (recording) {
steps++;
stepTimes.push(Date.now());
stepTimes = stepTimes.slice(-20);
const elapsed = stepTimes[stepTimes.length - 1] - stepTimes[0];
cadence = elapsed ? Math.round(60000 * (stepTimes.length - 1) / elapsed) : 0;
} else
stepTimes = [];
}
function doPace(thistime,thisdist) {
dists.push([thistime,thisdist]);
dists = dists.slice(-30);
const thiselapsed = dists[dists.length - 1][0] - dists[0][0];
const thisdistance = dists[dists.length - 1][1] - dists[0][1];
pace = thisdistance ? ((thiselapsed) / thisdistance) / 1000 : 0;
}
function countStep() {
if (recording)
steps++;
}
function calcCrow(lat1, lon1, lat2, lon2)
{
var dLat = toRad(lat2-lat1);
var dLon = toRad(lon2-lon1);
var lat1r = toRad(lat1);
var lat2r = toRad(lat2);
var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1r) * Math.cos(lat2r);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var d = R * c;
if (isNaN(d))
return 0;
else
return d;
}
function toRad(Value)
{
return Value * Math.PI / 180;
}
function saveGPS(fix) {
var newLat, newLon, newTime, newDist;
try {
newTime = fix.time.getTime();
}
catch(err) {
newTime = NaN;
}
newLat = fix.lat;
newLon = fix.lon;
gps = fix;
if (!cfg.storage) {
if (gps.satellites >= 8)
skip_max = 0;
else if (gps.satellites < 4)
skip_max = 5;
else
skip_max = 8 - gps.satellites;
}
if (recording && cfg.pacer > 0 && skip_ctr >= (cfg.storage ? 9 : skip_max))
pdist = (elapsed_ms / 1000) / ppace;
if (isNaN(newLat) || isNaN(newLon) || isNaN(newTime)) {
skip_ctr = 0;
skip_max = 0;
force_write = true;
} else {
if (oldLat != -1 && recording) {
skip_ctr++;
if (skip_ctr > (cfg.storage ? 9 : skip_max)) {
skip_ctr = 0;
oldDist = dist;
newDist = calcCrow(oldLat, oldLon, newLat, newLon);
dist += newDist;
doPace(newTime,dist);
oldLat = newLat;
oldLon = newLon;
}
} else {
oldLat = newLat;
oldLon = newLon;
}
if (recording && cfg.record && (force_write || skip_ctr == 0)) {
fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n");
last_time = gps.time;
if (force_write) {
skip_ctr = 0;
force_write = false;
}
}
}
}
function drawInvert() {
// not applicable to bangle2
g.drawImage(atob("DQ0BD4HjHwT4L8D+B/A/gfwL4J8EeMD4AA=="),225,26);
}
function drawSatIcon() {
if (bangle2)
g.drawImage(atob("CQkBIDo7i+Pj6O4uAgA="),3,53);
else
g.drawImage(atob("DAwBEAOAcQ84T8D4HwPyHPCOAcAI"),4,66);
}
function drawStepsIcon() {
if (bangle2)
g.drawimage(atob("CQkBBhudzudzgBzsMAA="),3,139);
else
g.drawImage(atob("DAwBAMAcMeOeeeeeeAecAcOYOAGA"),4,197);
}
function drawCadenceIcon() {
if (bangle2)
g.drawImage(atob("CQkBCB4SEBgMBCQ8CAA="),3,139);
else
g.drawImage(atob("DAwBBAAwP4YxxDwDwDwjjGH8DAAg"),4,197);
}
function hideStart() {
g.clearRect(bangle2?162:226,bangle2?81:113,bangle2?174:238,bangle2?93:125);
}
function drawStart() {
hideStart();
g.fillPoly([bangle2?162:226,bangle2?81:113,bangle2?162:226,bangle2?93:125,bangle2?174:238,bangle2?87:119,bangle2?162:226,bangle2?81:113]);
}
function drawPause() {
hideStart();
g.fillRect(bangle2?165:227,bangle2?82:113,bangle2?167:230,bangle2?92:125);
g.fillRect(bangle2?171:234,bangle2?82:113,bangle2?173:237,bangle2?92:125);
}
function drawStop() {
// not applicable to bangle2
g.fillRect(226,202,237,213);
}
function drawExit() {
if (bangle2)
g.drawImage(atob("CQkBwfHdx8HB8d3HwQg="),165,82);
else
g.drawImage(atob("DAwBwD4HcOOcH4DwDwH4OccO4HwD"),226,202);
}
function setColours() {
g.setBgColor(1-fg,1-fg,1-fg);
g.setColor(fg,fg,fg);
}
function setScreenMode() {
g.reset();
setColours();
g.clearRect(0,24,bangle2?175:239,bangle2?151:215);
}
function doLayout() {
setColours();
if (!bangle2)
drawInvert();
drawSatIcon();
drawDist();
drawTime();
if (cfg.pacer == 0)
drawPace();
else {
drawPacer();
drawSmallPace();
}
g.setFont("6x8",bangle2?2:3);
if (cfg.show_steps) {
drawStepsIcon();
g.drawString(steps.toString(),bangle2?15:20,bangle2?134:190,true);
} else {
drawCadenceIcon();
g.drawString(cadence.toString()+" ",bangle2?15:20,bangle2?134:190,true);
}
drawStart();
if (!bangle2)
drawStop();
}
function drawDist() {
g.setFont("6x8",bangle2?4:5); // 3:5?
var dStr = dist.toString();
if (dStr.indexOf('.') == -1)
dStr += '.0';
g.drawString(((' '+(dStr.split('.'))[0])).substr(-2),bangle2?33:53,bangle2?26:35,true);
g.fillRect(bangle2?80:112,bangle2?51:66,bangle2?82:115,bangle2?53:69);
g.drawString(((dStr.split('.'))[1]+'0').substr(0,2),bangle2?86:120,bangle2?26:35,true);
g.setFont("6x8",2);
g.drawString(cfg.metric?"K":"M",bangle2?134:180,bangle2?40:56,true);
}
function drawPacer() {
g.setFont("6x8",bangle2?3:5);
var pstr=(pdist>dist?'-':'+')+(Math.floor(Math.abs(dist-pdist)))%100;
g.drawString(pstr,bangle2?(49-(pstr.length>2?18:0)):(53-(pstr.length>2?30:0)),bangle2?107:145,true);
g.fillRect(bangle2?84:112,bangle2?126:176,bangle2?85:115,bangle2?127:179);
g.drawString(('0'+Math.floor(Math.abs(((dist-pdist)*100)%100))).substr(-2),bangle2?89:120,bangle2?107:145,true);
g.setFont("6x8",bangle2?1:2);
g.drawString(cfg.metric?"K":"M",bangle2?125:180,bangle2?121:166,true);
}
function drawPace() {
g.setFont("6x8",bangle2?3:5);
if (pace > 0 && pace < 6000)
g.drawString((' '+Math.floor(pace/60)).substr(-2),bangle2?49:53,bangle2?107:145,true);
else
g.drawString("--",bangle2?49:53,bangle2?107:145,true);
g.fillRect(bangle2?84:112,bangle2?117:160,bangle2?85:115,bangle2?118:163);
g.fillRect(bangle2?84:112,bangle2?120:166,bangle2?85:115,bangle2?121:169);
if (pace > 0 && pace < 6000)
g.drawString(('0'+Math.floor(pace%60)).substr(-2),bangle2?89:120,bangle2?107:145,true);
else
g.drawString("--",bangle2?89:120,bangle2?107:145,true);
g.setFont("6x8",bangle2?1:2);
g.drawString(cfg.metric?"/K":"/M",bangle2?124:178,bangle2?121:166,true);
}
function drawSmallPace() {
g.setFont("6x8",bangle2?2:3);
if (pace > 0 && pace < 6000)
g.drawString((' '+Math.floor(pace/60)).substr(-2),bangle2?113:136,bangle2?134:190,true);
else
g.drawString("--",bangle2?113:136,bangle2?134:190,true);
if (bangle2) {
g.setPixel(136,140);
g.setPixel(136,142);
} else {
g.fillRect(172,199,173,200);
g.fillRect(172,203,173,204);
}
if (pace > 0 && pace < 6000)
g.drawString(('0'+Math.floor(pace%60)).substr(-2),bangle2?138:176,bangle2?134:190,true);
else
g.drawString("--",bangle2?138:176,bangle2?134:190,true);
g.setFont("6x8",1);
g.drawString(cfg.metric?"/K":"/M",bangle2?164:212,bangle2?141:204,true);
}
function drawTime() {
var seconds;
var minutes;
var hours;
setColours();
g.setFont("6x8",bangle2?5:7);
seconds = parseInt(elapsed_ms/1000) % 60;
minutes = parseInt(elapsed_ms/60000) % 60;
hours = parseInt(elapsed_ms/3600000) % 10;
g.drawString(hours.toString(),bangle2?6:5,bangle2?63:82,true);
g.fillRect(bangle2?34:44,bangle2?79:103,bangle2?36:48,bangle2?81:107);
g.fillRect(bangle2?34:44,bangle2?84:112,bangle2?36:48,bangle2?86:116);
g.drawString(('0'+minutes).substr(-2),bangle2?40:53,bangle2?63:82,true);
g.fillRect(bangle2?98:134,bangle2?79:103,bangle2?100:138,bangle2?81:107);
g.fillRect(bangle2?98:134,bangle2?84:112,bangle2?100:138,bangle2?86:116);
g.drawString(('0'+seconds).substr(-2),bangle2?104:143,bangle2?63:82,true);
}
function drawGPSBox() {
g.drawRect(2,26,bangle2?12:17,bangle2?51:63);
}
function drawGPS() {
g.clearRect(3,27,bangle2?11:16,bangle2?50:62);
if (gps.satellites > 0) {
if (!gps.fix)
g.setColor("#FF0000");
else if (gps.satellites < 4)
g.setColor("#FF5500");
else if (gps.satellites < 6)
g.setColor("#FF8800");
else if (gps.satellites < 8)
g.setColor("#FFCC00");
else
g.setColor("#00FF00");
g.fillRect(3,bangle2?50:62,bangle2?11:16,(bangle2?50:62)-(gps.satellites>12?12:gps.satellites)*(bangle2?2:3)+1);
g.setColor(fg,fg,fg);
}
}
function hideLap() {
show_lap = false;
g.reset();
setColours();
g.clearRect(bangle2?6:5,bangle2?63:82,bangle2?162:224,bangle2?129:182);
if (recording) {
current_time = Date.now();
elapsed_ms = current_time - (start_time + paused_time);
} else
elapsed_ms = begin_pause - (start_time + paused_time);
drawTime();
if (cfg.pacer == 0)
drawPace();
else {
drawPacer();
drawSmallPace();
}
}
function showLap() {
g.clearRect(bangle2?6:5,bangle2?63:82,bangle2?162:224,bangle2?129:182);
g.drawRect(bangle2?21:28,bangle2?68:90,bangle2?147:201,bangle2?124:174);
g.drawRect(bangle2?23:30,bangle2?70:92,bangle2?145:199,bangle2?122:172);
g.setFont("6x8",bangle2?1:2);
g.drawString("Last lap",bangle2?61:68,bangle2?77:102,true);
g.setFont("6x8",bangle2?3:5);
if (lap_ms < 600000) {
g.drawString((''+Math.floor(lap_ms/60000)),bangle2?57:69,bangle2?89:122,true);
g.fillRect(bangle2?74:98,bangle2?99:137,bangle2?75:101,bangle2?100:140);
g.fillRect(bangle2?74:98,bangle2?102:143,bangle2?75:101,bangle2?103:146);
g.drawString(('0'+Math.floor((lap_ms%60000)/1000)).substr(-2),bangle2?78:106,bangle2?89:122,true);
} else if (lap_ms < 3600000) {
g.drawString((''+Math.floor(lap_ms/60000)),bangle2?48:54,bangle2?89:122,true);
g.fillRect(bangle2?83:113,bangle2?99:137,bangle2?84:116,bangle2?100:140);
g.fillRect(bangle2?83:113,bangle2?102:143,bangle2?84:116,bangle2?103:146);
g.drawString(('0'+Math.floor((lap_ms%60000)/1000)).substr(-2),bangle2?87:121,bangle2?89:122,true);
} else {
g.drawString((''+Math.floor(lap_ms/3600000)).substr(-1),bangle2?37:35,bangle2?89:122,true);
g.fillRect(bangle2?54:64,bangle2?99:137,bangle2?55:67,bangle2?100:140);
g.fillRect(bangle2?54:64,bangle2?102:143,bangle2?55:67,bangle2?103:146);
g.drawString(('0'+Math.floor((lap_ms%3600000)/60000)).substr(-2),bangle2?58:72,bangle2?89:122,true);
g.fillRect(bangle2?93:131,bangle2?99:137,bangle2?94:134,bangle2?100:140);
g.fillRect(bangle2?93:131,bangle2?102:143,bangle2?94:134,bangle2?103:146);
g.drawString(('0'+Math.floor((lap_ms%60000)/1000)).substr(-2),bangle2?97:139,bangle2?89:122,true);
}
Bangle.setLCDPower(true);
}
function mainLoop() {
g.reset();
setColours();
current_time = Date.now();
if (started) {
elapsed_ms = current_time - (start_time + paused_time);
if (oldDist != dist) {
drawDist();
if (cfg.lap_idx > 0 && dist >= next_lap ) {
show_lap = true;
next_lap += parseFloat(laps[cfg.lap_idx]);
lap_ms = elapsed_ms - lap_start_ms;
lap_start_ms = elapsed_ms;
Bangle.buzz();
lapID = setTimeout(hideLap,5000);
showLap();
}
}
} else
elapsed_ms = 0;
drawSats = false;
if (recording) {
if (!show_lap)
drawTime();
g.setFont("6x8",bangle2?2:3);
if (cfg.show_steps)
g.drawString(steps.toString(),bangle2?15:20,bangle2?134:190,true);
else
g.drawString(cadence.toString()+" ",bangle2?15:20,bangle2?134:190,true);
} /* else
g.setFont("6x8",3); */
if (!show_lap)
if (cfg.pacer == 0)
drawPace();
else {
drawPacer();
drawSmallPace();
}
if (gps.fix) {
if (!started && startHidden) {
startHidden = false;
if (!bangle2)
startID = setWatch(start, BTN2);
else
startID = setWatch(start, BTN1, {edge: 'falling'});
drawStart();
}
if (!fixed) {
fixed = true;
drawSats = true;
}
} else {
if (!started && !startHidden) {
startHidden = true;
clearWatch(startID);
hideStart();
}
if (fixed) {
fixed = false;
drawSats = true;
}
}
if (gps.satellites != sats) {
sats = gps.satellites;
drawSats = true;
}
if (drawSats)
drawGPS();
if (ctr++%10 == 0) {
g.reset();
Bangle.drawWidgets();
}
}
function restart(e) {
if (bangle2 && (e.time - e.lastTime > 0.5)) {
finish();
} else {
g.reset();
setColours();
paused_time += (Date.now() - begin_pause);
pace = 0;
drawPause();
oldDist = dist;
skip_ctr = 0;
force_write = true;
recording = true;
Bangle.buzz();
if (!bangle2)
setWatch(pause, BTN2);
else
setWatch(pause, BTN1, { edge: 'falling' });
}
}
function pause(e) {
if (bangle2 && (e.time - e.lastTime > 0.5)) {
finish();
}
g.reset();
setColours();
begin_pause = Date.now();
elapsed_ms = begin_pause - (start_time + paused_time);
finish_ms = elapsed_ms;
drawDist();
recording = false;
if (!show_lap)
drawTime();
drawStart();
if (!isNaN(gps.time) && !isNaN(gps.lat) && !isNaN(gps.lon) && !isNaN(gps.alt) && cfg.record && (last_time != gps.time))
fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n");
Bangle.buzz();
dists = [];
if (!bangle2)
setWatch(restart, BTN2);
else
setWatch(restart, BTN1, { edge: 'falling' });
}
function start() {
g.reset();
setColours();
if (cfg.eco){
Bangle.setLCDPower(true);
Bangle.setLCDTimeout(10);
}
if (cfg.record)
fp = require("Storage").open(genFilename(),"w");
start_time = Date.now();
drawPause();
Bangle.buzz();
started = true;
recording = true;
if (!bangle2)
setWatch(pause, BTN2);
else
setWatch(pause, BTN1, { edge: 'falling' });
if (cfg.show_steps)
Bangle.on('step',countStep);
else
Bangle.on('step',doCadence);
clearInterval(intID);
intID = setInterval(mainLoop,200);
if (cfg.lap_idx > 0)
next_lap = parseFloat(laps[cfg.lap_idx]);
}
function endScreen() {
fg = 1-fg;
setScreenMode();
if (!bangle2)
drawInvert();
drawExit();
g.setFont("6x8",bangle2?1:2);
var dStr = dist.toString();
if (dStr.indexOf('.') == -1)
dStr += '.00';
dStr = dStr.slice(0, (dStr.indexOf('.'))+3);
if (bangle2)
g.drawString('Distance: '+dStr+(cfg.metric?'K':'M'),38,43);
else {
//g.drawString('Distance: '+dist.toFixed(2),19,53);
g.drawString('Distance: '+dStr,19,53);
g.setFont("6x8",1);
//g.drawString(cfg.metric?'K':'M',139+12*(dist.toFixed(2).length),60);
g.drawString(cfg.metric?'K':'M',139+12*(dStr.length),60);
g.setFont("6x8",2);
}
g.drawString('Time: '+parseInt(finish_ms/3600000)%10+':'+('0'+parseInt(finish_ms/60000)%60).substr(-2)+':'+('0'+parseInt(finish_ms/1000)%60).substr(-2),bangle2?62:67,bangle2?63:83);
var avgPace = dist?((finish_ms/dist)/1000):0;
var paceStr = 'Avg Pace: '+parseInt(avgPace/60)+':'+('0'+parseInt(avgPace%60)).substr(-2);
if (bangle2)
g.drawString(paceStr+(cfg.metric?'/K':'/M'),38,83);
else {
g.drawString(paceStr,19,113);
g.setFont("6x8",1);
g.drawString(cfg.metric?'/K':'/M',19+12*(paceStr.length),120);
g.setFont("6x8",2);
}
g.drawString("Steps: "+steps,bangle2?56:55,bangle2?103:143);
var avgCadence = steps?(60*steps/(finish_ms/1000)):0;
g.drawString("Cadence: "+parseInt(avgCadence),bangle2?44:31,bangle2?123:173);
g.reset();
Bangle.drawWidgets();
}
function finish() {
if (recording) {
finish_ms = elapsed_ms;
if (!isNaN(gps.time) && !isNaN(gps.lat) && !isNaN(gps.lon) && !isNaN(gps.alt) && cfg.record && (last_time != gps.time))
fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n");
}
recording = false;
Bangle.setGPSPower(0);
Bangle.on('step',function(){});
Bangle.on('GPS',function(){});
clearInterval(lapID);
clearInterval(intID);
if (!bangle2) {
clearWatch(finID);
clearWatch(invID);
}
if (!bangle2) {
setWatch(function() {if (lcd_on) endScreen();}, BTN1, {repeat:true});
setWatch(function() {load();},BTN3);
} else
setWatch(function() {load();},BTN1);
fg = 1-fg;
endScreen();
}
function startScreen() {
clearInterval(intID);
if (!bangle2) {
clearWatch(invID);
}
clearWatch(finID);
setScreenMode();
doLayout();
drawGPSBox();
Bangle.buzz();
if (!bangle2) {
invID = setWatch(invertRunning, BTN1, {repeat:true});
startID = setWatch(start, BTN2);
finID = setWatch(finish, BTN3);
} else
startID = setWatch(start, BTN1, {edge: 'falling'});
fixed = false;
intID = setInterval(mainLoop,1000);
}
function invertRunning() {
// not applicable to bangle2
if (!lcd_on)
return;
fg = 1-fg;
setScreenMode();
if (started)
if (recording) {
current_time = Date.now();
elapsed_ms = current_time - (start_time + paused_time);
} else
elapsed_ms = begin_pause - (start_time + paused_time);
else
elapsed_ms = 0;
drawInvert();
drawSatIcon();
drawGPSBox();
drawGPS();
drawDist();
if (show_lap)
showLap();
else {
drawTime();
if (cfg.pacer == 0)
drawPace();
else {
drawPacer();
drawSmallPace();
}
}
g.setFont("6x8",3);
if (cfg.show_steps) {
drawStepsIcon();
g.drawString(steps.toString(),20,190,true);
} else {
drawCadenceIcon();
g.drawString(cadence.toString()+" ",20,190,true);
}
if (recording)
drawPause();
else if (started || gps.fix)
drawStart();
drawStop();
g.reset();
Bangle.drawWidgets();
}
function drawDots() {
if (ctr % 4 == 0)
g.drawString(" ",bangle2?116:176,bangle2?90:108,true);
else if (ctr % 4 == 1)
g.drawString(".",bangle2?116:176,bangle2?90:108);
else if (ctr % 4 == 2)
g.drawString("..",bangle2?116:176,bangle2?90:108);
else
g.drawString("...",bangle2?116:176,bangle2?90:108);
}
function invertWaiting() {
/* not applicable to bangle2 */
fg = 1-fg;
setScreenMode();
drawInvert();
drawExit();
g.setFont("6x8",2);
g.drawString("Locating",68,88);
g.drawString("Satellites",56,108);
ctr--;
drawDots();
g.reset();
Bangle.drawWidgets();
ctr++;
}
function awaitGPSLoop() {
g.reset();
setColours();
g.setFont("6x8",bangle2?1:2);
drawDots();
if (gps.fix)
startScreen();
if (ctr % 10 == 0) {
g.reset();
Bangle.drawWidgets();
}
ctr++;
}
function awaitGPS() {
Bangle.setOptions({wakeOnTwist:false});
Bangle.setGPSPower(1);
Bangle.on('GPS', saveGPS);
Bangle.setLCDPower(true);
Bangle.setLCDTimeout(0);
g.reset();
setColours();
if (!bangle2) {
drawInvert();
invID = setWatch(invertWaiting, BTN1, {repeat:true});
}
drawExit();
g.setFont("6x8",bangle2?1:2);
// g.drawString("Locating",bangle2?36:68,bangle2?72:88);
// g.drawString("Satellites",bangle2?24:56,bangle2?92:108);
g.drawString("Locating",bangle2?62:68,bangle2?78:88);
g.drawString("Satellites",56,bangle2?90:108);
intID = setInterval(awaitGPSLoop,1000);
if (bangle2)
finID = setWatch(function(){load();},BTN1);
else
finID = setWatch(function(){load();},BTN3);
}
function main() {
require("Storage").write("pacer.json", cfg);
E.showMenu();
if (cfg.eco)
Bangle.on('lcdPower', function(on) {setTimeout(function(){lcd_on = on;}, 500);});
fg = cfg.dark?1:0;
R = cfg.metric?6371:3959;
ppace = 295 + 5 * cfg.pacer;
setScreenMode();
awaitGPS();
}
defaults();
var main_menu = {
"" : { "title" : "Pacer"},
"Start": function() { main(); },
"Recording" : {
value : cfg.record,
format : v => v?"On":"Off",
onchange : v => { cfg.record = v; },
},
"Units" : {
value : cfg.metric,
format : v => v?"Metric":"Imperial",
onchange : v => { cfg.metric = v; },
},
"Lap" : {
value : cfg.lap_idx,
format : v => laps[v],
min : 0, max : 6,
onchange : v => { cfg.lap_idx = v; }
},
"Dark mode" : {
value : cfg.dark,
format : v => v?"On":"Off",
onchange : v => { cfg.dark = v; },
},
"Eco battery" : {
value : cfg.eco,
format : v => v?"On":"Off",
onchange : v => { cfg.eco = v; },
},
"Eco storage" : {
value : cfg.storage,
format : v => v?"On":"Off",
onchange : v => { cfg.storage = v; },
},
"Steps" : {
value : cfg.show_steps,
format : v => v?"Count":"Cadence",
onchange : v => { cfg.show_steps = v; },
},
"Pacer" : {
value : cfg.pacer,
format : v => v==0?"Off":pace_str(v),
min : 0, max : 121,
onchange : v => { cfg.pacer = v; }
},
};
if (!bangle2) {
Bangle.setLCDMode();
}
Bangle.setLCDBrightness(1);
setScreenMode();
E.showMenu(main_menu);

BIN
apps/pacer/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

198
apps/pacer/interface.html Normal file
View File

@ -0,0 +1,198 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<div id="tracks"></div>
<div class="container" id="toastcontainer" style="position:fixed; bottom:8px; left:0px; right:0px; z-index: 100;"></div>
<script src="../../core/lib/interface.js"></script>
<script src="../../core/js/ui.js"></script>
<script src="../../core/js/utils.js"></script>
<script>
var domTracks = document.getElementById("tracks");
var fileCache = new Map();
function saveGPX(track, title) {
// Output GPX
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
<gpx creator="Bangle.js" version="1.1">
<metadata>
<time>${isoTime(track[0][0])}</time>
</metadata>
<trk>
<name>${title}</name>
<trkseg>`;
track.forEach(pt=>{
gpx += `
<trkpt lat="${pt[1]}" lon="${pt[2]}">
<ele>${pt[3]}</ele>
<time>${isoTime(pt[0])}</time>
</trkpt>`;
});
gpx += `
</trkseg>
</trk>
</gpx>`;
Util.saveFile(title+".gpx", "application/gpx+xml", gpx);
showToast("Download finished.", "success");
}
function trackLineToObject(l) {
if (l===undefined) return {};
return l.trim().split(",");
}
function isoTime(t) {
var td = new Date(Number(t));
var ti = td.toISOString();
return ti;
}
function downloadTrack(filename, callback) {
function onData(data) {
var lines = data.trim().split("\n");
var headers = lines.shift().split(",");
var track = lines.map(l=>trackLineToObject(l));
callback(track);
}
const data = fileCache.get(filename);
if (data) {
onData(data);
} else {
Util.showModal(`Downloading ${filename}...`);
Util.readStorageFile(filename, data => {
fileCache.set(filename, data);
onData(data);
Util.hideModal();
});
}
}
function downloadAll(trackList, cb) {
const tracks = trackList.slice();
const downloadOne = () => {
const track = tracks.pop();
if(!track) {
showToast("Finished downloading all.", "success");
return;
}
downloadTrack(
track.filename,
lines => {
cb(lines, `Bangle.js Track ${track.number}`);
downloadOne();
}
);
};
downloadOne();
}
function getTrackList() {
Util.showModal("Loading Track List...");
domTracks.innerHTML = "";
Puck.eval(`require("Storage").list(/^\\.pacer.*\\.csv$/,{sf:1})`,files=>{
var trackList = [];
var promise = Promise.resolve();
files.forEach(filename => {
promise = promise.then(()=>new Promise(resolve => {
var trackNo = filename.match(/^\.pacer(.*)\.csv$/)[1];
Util.showModal(`Loading Track ${trackNo}...`);
Puck.eval(`(${JSON.stringify(filename)})`, trackInfo=>{
console.log(filename," => ",trackInfo);
trackList.push({
filename : filename,
number : trackNo,
});
resolve();
});
}));
});
// ================================================
// When 'promise' completes we now have all the info in trackList
promise.then(() => {
var html = `
<div class="container">
<h2>Tracks</h2>
<div class="columns">\n`;
trackList.forEach(track => {
console.log("track", track);
const trackDate = new Date (track.number.slice(0,4) + '-' + track.number.slice(4,6) + '-' + track.number.slice (6,8))
const trackFullDate = trackDate.toDateString() + ' ' + track.number.slice(8,10) + ':' + track.number.slice(10,12) + ':' + track.number.slice (12,14)
html += `
<div class="column col-12">
<div class="card-header">
<div class="card-title h5">
${trackFullDate} &nbsp;
<button class="btn btn-primary" filename="${track.filename}" trackid="${track.number}" task="downloadgpx">Download GPX</button>
<button class="btn btn-default" filename="${track.filename}" trackid="${track.number}" task="delete">Delete</button>
</div>
<div class="card-subtitle">
&nbsp;
</div>
</div>
</div>
`;
});
if (trackList.length==0) {
html += `
<div class="column col-12">
<div class="card-header">
<div class="card-title h5">No tracks</div>
<div class="card-subtitle text-gray">No GPS tracks found</div>
</div>
</div>
`;
}
html += `
</div><!-- columns -->
<h2>Batch</h2>
<div class="form-group">
<button class="btn btn-primary" task="downloadgpx_all">Download all GPX</button>
</div>
</div>`;
domTracks.innerHTML = html;
Util.hideModal();
var buttons = domTracks.querySelectorAll("button");
for (var i=0;i<buttons.length;i++) {
buttons[i].addEventListener("click",event => {
var button = event.currentTarget;
var filename = button.getAttribute("filename");
var trackid = parseInt(button.getAttribute("trackid"));
var task = button.getAttribute("task");
if (!/_all$/.test(task) && (!filename || trackid===undefined)) return;
switch(task) {
case "delete":
Util.showModal(`Deleting ${filename}...`);
Util.eraseStorageFile(filename,()=>{
Util.hideModal();
getTrackList();
});
break;
case "downloadgpx":
downloadTrack(filename, track => saveGPX(track, `Bangle.js Track ${trackid}`));
break;
case "downloadgpx_all":
downloadAll(trackList, saveGPX);
break;
}
});
}
});
});
}
function onInit() {
getTrackList();
}
</script>
</body>
</html>

20
apps/pacer/metadata.json Normal file
View File

@ -0,0 +1,20 @@
{
"id": "pacer",
"name": "Pacer",
"version": "0.01",
"description": "Run with a virtual partner",
"icon": "app.png",
"tags": "run,running,fitness,outdoors,gps",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"interface": "interface.html",
"screenshots": [{"url":"screenshot.png"}],
"storage": [
{"name":"pacer.app.js","url":"app.js"},
{"name":"pacer.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"pacer.json"},
{"wildcard":".pacer*.csv","storageFile":true}
]
}

BIN
apps/pacer/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -49,7 +49,7 @@ Alarms are stored in an array in `sched.json`, and take the form:
// e.g. repeat every 2 months: { interval: "month", num: 2 }.
// Supported intervals: day, week, month, year
vibrate : "...", // OPTIONAL pattern of '.', '-' and ' ' to use for when buzzing out this alarm (defaults to '..' if not set)
hidden : false, // OPTIONAL if false, the widget should not show an icon for this alarm
hidden : false, // OPTIONAL if true, the widget should not show an icon for this alarm
as : false, // auto snooze
timer : 5*60*1000, // OPTIONAL - if set, this is a timer and it's the time in ms
del : false, // OPTIONAL - if true, delete the timer after expiration

View File

@ -5,7 +5,7 @@
"description": "A basic implementation of the famous consice workout",
"icon": "icon.png",
"type":"app",
"tags": "Training, Workout",
"tags": "training,workout",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"allow_emulator":true,

View File

@ -11,3 +11,4 @@ when fastloading.
0.10: Some refactoring to shorten the code.
0.11: Further refactoring to shorten the code. Fixed search and play that was broken in v0.10.
0.12: Fix some warnings from the linter.
0.13: Move ui-handlers inside setUI-call.

View File

@ -14,7 +14,6 @@ let gfx = function() {
widgetUtils.hide();
R = Bangle.appRect;
const MARIGIN = 8;
// g.drawString(str, x, y, solid)
g.clearRect(R);
g.reset();
@ -100,23 +99,17 @@ let swipeHandler = function(LR, _) {
// Navigation input on the main layout
let setUI = function() {
// Bangle.setUI code from rigrig's smessages app for volume control: https://git.tubul.net/rigrig/BangleApps/src/branch/personal/apps/smessages/app.js
Bangle.setUI(
{mode : "updown",
remove : ()=>{
Bangle.removeListener("touch", touchHandler);
Bangle.removeListener("swipe", swipeHandler);
clearWatch(buttonHandler);
widgetUtils.show();
}
touch: touchHandler,
swipe: swipeHandler,
btn: ()=>load(),
remove : ()=>widgetUtils.show(),
},
ud => {
if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup");
}
);
Bangle.on("touch", touchHandler);
Bangle.on("swipe", swipeHandler);
let buttonHandler = setWatch(()=>{load();}, BTN, {edge:'falling'});
};
// Get back to the main layout

View File

@ -1,7 +1,7 @@
{
"id": "spotrem",
"name": "Remote for Spotify",
"version": "0.12",
"version": "0.13",
"description": "Control spotify on your android device.",
"readme": "README.md",
"type": "app",

View File

@ -5,7 +5,7 @@
"version": "0.08",
"description": "App to test the bangle.js input interface. It displays the user action in text, circle buttons or on/off switch UI elements.",
"icon": "app.png",
"tags": "input,interface,buttons,touch,UI",
"tags": "input,interface,buttons,touch,ui",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [

1
apps/txtreader/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

20
apps/txtreader/README.md Normal file
View File

@ -0,0 +1,20 @@
# txtreader
Very basic text reader with an integrated file selector.
## Features
- select files from storage (.txt)
- display their contents
- browse pages
## Controls
Bangle.js 2
- tap the right side of the screen to flip to the next page
- tap the left side of the screen to flip to the previous page
- exit by pressing the physical button
## Creator
<https://topkekker.rip/>

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkCkQA/AH4A/AAcosd2m9jw04AQlyC5WL1e63YCG3lUC5WPj/x/8f+H/h4CBj/3jYXLv/3/4CCt4FC/IXM/4AI/YXk/WZzOfCgWfAoIXN/F3u9/C4V/AoIvUI6H3F4p3oC6/73e734XTR4wXQ/vd7vfC6f85gAG55HQAAppBC5n5xAAGz4vmzIAGF/33F49/C5v6F4+vC5rrFd6JHCv4XTI4WfF6lms1vF6mq1WvC6Z3WxfdABHVugXKlFUogAHoVCC5QA/AH4A6A"))

88
apps/txtreader/app.js Normal file
View File

@ -0,0 +1,88 @@
function showFileSelector() {
let files = require("Storage").list().filter(f => f.endsWith('.txt'));
let menuItems = {};
files.forEach(file => {
menuItems[file] = () => {
E.showPrompt(`Select ${file}?`).then(confirm => {
if (confirm) {
onFileSelected(file);
} else {
showFileSelector();
}
});
};
});
menuItems['< Back'] = () => { load(); };
E.showMenu(menuItems);
}
function onFileSelected(file) {
const chunkSize = 1024;
let currentOffset = 0;
let currentPage = 1;
let history = [];
function displayText(offset, pageNumber) {
g.clear();
g.setFont("6x8", 1);
g.setColor(g.theme.fg);
g.drawString("Page " + pageNumber, 10, 2);
//g.drawString("Offset " + offset, 60, 2);
g.drawString(file, g.getWidth() - file.length * 6, 2);
var text = require("Storage").read(file, offset, chunkSize);
var lines = text.split("\n");
var y = 15; // Text start, top row reserved for page number
var linesDisplayed = 0; // Lines per page
var totalCharsDisplayed = 0; // Total characters per page
for (var i = 0; i < lines.length; i++) {
var wrappedLines = g.wrapString(lines[i], g.getWidth() - 20);
for (var j = 0; j < wrappedLines.length; j++) {
g.drawString(wrappedLines[j], 10, y);
y += 10; // Move down for the next line
linesDisplayed++;
totalCharsDisplayed += wrappedLines[j].length + (j < wrappedLines.length - 1 ? 0 : 1); // Add newline character for the last wrapped line
if (y >= g.getHeight() - 10) {
// If we run out of space, stop drawing
return { nextOffset: offset + totalCharsDisplayed, linesDisplayed: linesDisplayed };
}
}
}
return null; // No more lines to display
}
// Initial display
var result = displayText(currentOffset, currentPage);
history.push({ offset: currentOffset, linesDisplayed: result.linesDisplayed });
// Handle touch events
Bangle.on('touch', function(button) {
if (button === 2) { // Right side of the screen (next page)
var nextOffset = displayText(currentOffset, currentPage + 1);
if (nextOffset !== null) {
currentOffset = nextOffset.nextOffset;
currentPage++;
history.push({ offset: currentOffset, linesDisplayed: nextOffset.linesDisplayed });
displayText(currentOffset, currentPage);
} else {
currentOffset = 0;
currentPage = 1;
history = [{ offset: currentOffset, linesDisplayed: result.linesDisplayed }];
displayText(currentOffset, currentPage);
}
} else if (button === 1) { // Left side of the screen (previous page)
if (currentPage > 1) {
history.pop(); // Remove current page from history
var previousPage = history[history.length - 1];
currentOffset = previousPage.offset;
currentPage--;
displayText(currentOffset, currentPage);
}
}
});
}
showFileSelector();

View File

@ -0,0 +1,16 @@
{
"id": "txtreader",
"name": "txtreader",
"shortName": "txtreader",
"version": "0.01",
"description": "Basic text reader with pages and a file selector.",
"icon": "txtreader.png",
"screenshots": [{"url":"screenshot_txtreader.png"}],
"tags": "app,tool",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"txtreader.app.js","url":"app.js"},
{"name":"txtreader.img","url":"app-icon.js","evaluate":true}
]
}

Some files were not shown because too many files have changed in this diff Show More