Merge branch 'master' of github.com:espruino/BangleApps

pull/1225/head
Gordon Williams 2022-01-05 16:47:19 +00:00
commit 8ce8f374a7
23 changed files with 1773 additions and 54 deletions

View File

@ -845,7 +845,7 @@
{ {
"id": "weather", "id": "weather",
"name": "Weather", "name": "Weather",
"version": "0.14", "version": "0.15",
"description": "Show Gadgetbridge weather report", "description": "Show Gadgetbridge weather report",
"icon": "icon.png", "icon": "icon.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],
@ -1324,7 +1324,7 @@
"icon": "gesture.png", "icon": "gesture.png",
"type": "app", "type": "app",
"tags": "gesture,ai", "tags": "gesture,ai",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS", "BANGLEJS2"],
"storage": [ "storage": [
{"name":"gesture.app.js","url":"gesture.js"}, {"name":"gesture.app.js","url":"gesture.js"},
{"name":".tfnames","url":"gesture-tfnames.js","evaluate":true}, {"name":".tfnames","url":"gesture-tfnames.js","evaluate":true},
@ -4489,7 +4489,7 @@
"name": "LCARS Clock", "name": "LCARS Clock",
"shortName":"LCARS", "shortName":"LCARS",
"icon": "lcars.png", "icon": "lcars.png",
"version":"0.08", "version":"0.09",
"readme": "README.md", "readme": "README.md",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.", "description": "Library Computer Access Retrieval System (LCARS) clock.",
@ -5123,6 +5123,24 @@
{"name":"ltherm.img","url":"icon.js","evaluate":true} {"name":"ltherm.img","url":"icon.js","evaluate":true}
] ]
}, },
{
"id": "presentor",
"name": "Presentor",
"version": "3.0",
"description": "Use your Bangle to present!",
"icon": "app.png",
"type": "app",
"tags": "tool,bluetooth",
"interface": "interface.html",
"readme":"README.md",
"supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"presentor.app.js","url":"app.js"},
{"name":"presentor.img","url":"app-icon.js","evaluate":true},
{"name":"presentor.json","url":"settings.json"}
]
},
{ {
"id": "slash", "id": "slash",
"name": "Slash Watch", "name": "Slash Watch",
@ -5390,5 +5408,24 @@
"storage": [ "storage": [
{"name":"touchmenu.boot.js","url":"touchmenu.boot.js"} {"name":"touchmenu.boot.js","url":"touchmenu.boot.js"}
] ]
},
{
"id": "puzzle15",
"name": "15 puzzle",
"version": "0.05",
"description": "A 15 puzzle game with drag gesture interface",
"readme":"README.md",
"icon": "puzzle15.app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "app",
"tags": "game",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"puzzle15.app.js","url":"puzzle15.app.js"},
{"name":"puzzle15.settings.js","url":"puzzle15.settings.js"},
{"name":"puzzle15.img","url":"puzzle15.app-icon.js","evaluate":true}
],
"data": [{"name":"puzzle15.json"}]
} }
] ]

View File

@ -5,4 +5,5 @@
0.05: Additional icons for (1) charging and (2) bat < 30%. 0.05: Additional icons for (1) charging and (2) bat < 30%.
0.06: Fix - Alarm disabled, if clock was closed. 0.06: Fix - Alarm disabled, if clock was closed.
0.07: Added settings to adjust data that is shown for each row. 0.07: Added settings to adjust data that is shown for each row.
0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode. 0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode.
0.09: Tab anywhere to open the launcher.

View File

@ -8,13 +8,15 @@ To contribute you can open a PR at this [GitHub Repo]( https://github.com/peerda
* LCARS Style watch face. * LCARS Style watch face.
* Full screen mode - widgets are still loaded. * Full screen mode - widgets are still loaded.
* Supports multiple screens with different data. * Supports multiple screens with different data.
* Tab anywhere to open the launcher.
* [Screen 1] Date + Time + Lock status. * [Screen 1] Date + Time + Lock status.
* [Screen 1] Shows randomly images of real planets. * [Screen 1] Shows randomly images of real planets.
* [Screen 1] Shows different states such as (charging, out of battery, GPS on etc.) * [Screen 1] Shows different states such as (charging, out of battery, GPS on etc.)
* [Screen 1] Swipe up/down to activate an alarm. * [Screen 1] Swipe up/down to activate an alarm.
* [Screen 1] Shows 3 customizable datapoints on the first screen. * [Screen 1] Shows 3 customizable datapoints on the first screen.
* [Screen 1] The lower orange line indicates the battery level. * [Screen 1] The lower orange line indicates the battery level.
* [Screen 2] Display month graphs for steps + hrm on the second screen. * [Screen 2] Display graphs for steps + hrm on the second screen.
* [Screen 2] Switch between day/month via swipe up/down.
## Multiple screens support ## Multiple screens support

View File

@ -24,6 +24,7 @@ let cOrange = "#FF9900";
let cPurple = "#FF00DC"; let cPurple = "#FF00DC";
let cWhite = "#FFFFFF"; let cWhite = "#FFFFFF";
let cBlack = "#000000"; let cBlack = "#000000";
let cGrey = "#9E9E9E";
/* /*
* Global lcars variables * Global lcars variables
@ -147,14 +148,17 @@ function printData(key, y, c){
} }
g.setColor(c); g.setColor(c);
g.fillRect(79, y-2, 87 ,y+18);
g.setFontAlign(1,-1,0);
g.drawString(value, 131, y);
g.setColor(c);
g.setFontAlign(-1,-1,0);
g.fillRect(133, y-2, 165 ,y+18); g.fillRect(133, y-2, 165 ,y+18);
g.fillCircle(161, y+8, 10); g.fillCircle(161, y+8, 10);
g.setColor(cBlack); g.setColor(cBlack);
g.drawString(text, 135, y); g.drawString(text, 135, y);
g.setColor(c);
g.setFontAlign(1,-1,0);
g.drawString(value, 130, y);
} }
function drawHorizontalBgLine(color, x1, x2, y, h){ function drawHorizontalBgLine(color, x1, x2, y, h){
@ -191,13 +195,14 @@ function drawState(){
return; return;
} }
g.clearRect(20, 93, 77, 170); g.clearRect(20, 93, 75, 170);
g.setColor(cWhite); g.setFontAlign(0, 0, 0);
var bat = E.getBattery(); g.setFontAntonioMedium();
var current = new Date();
var hours = current.getHours();
if(!isAlarmEnabled()){ if(!isAlarmEnabled()){
var bat = E.getBattery();
var current = new Date();
var hours = current.getHours();
var iconImg = var iconImg =
Bangle.isCharging() ? iconCharging : Bangle.isCharging() ? iconCharging :
bat < 30 ? iconNoBattery : bat < 30 ? iconNoBattery :
@ -206,16 +211,16 @@ function drawState(){
hours % 4 == 1 ? iconMars : hours % 4 == 1 ? iconMars :
hours % 4 == 2 ? iconMoon : hours % 4 == 2 ? iconMoon :
iconEarth; iconEarth;
g.drawImage(iconImg, 29, 104); g.drawImage(iconImg, 24, 118);
g.setColor(cWhite);
g.drawString("STATUS", 24+25, 108);
} else { } else {
// Alarm within symbol // Alarm within symbol
g.setFontAntonioMedium();
g.setFontAlign(0, 0, 0);
g.setColor(cOrange); g.setColor(cOrange);
g.drawString("ALARM", 29+25, 107); g.drawString("ALARM", 24+25, 108);
g.setColor(cWhite); g.setColor(cWhite);
g.setFontAntonioLarge(); g.setFontAntonioLarge();
g.drawString(getAlarmMinutes(), 29+25, 107+35); g.drawString(getAlarmMinutes(), 24+25, 108+35);
} }
g.setFontAlign(-1, -1, 0); g.setFontAlign(-1, -1, 0);
@ -236,7 +241,7 @@ function drawPosition0(){
var bat = E.getBattery() / 100.0; var bat = E.getBattery() / 100.0;
var batX2 = parseInt((172 - 35) * bat + 35); var batX2 = parseInt((172 - 35) * bat + 35);
drawHorizontalBgLine(cOrange, 35, batX2, 171, 5); drawHorizontalBgLine(cOrange, 35, batX2, 171, 5);
drawHorizontalBgLine(cPurple, batX2+10, 172, 171, 5); drawHorizontalBgLine(cGrey, batX2+10, 172, 171, 5);
// Draw logo // Draw logo
drawLock(); drawLock();
@ -247,7 +252,7 @@ function drawPosition0(){
var currentDate = new Date(); var currentDate = new Date();
var timeStr = locale.time(currentDate,1); var timeStr = locale.time(currentDate,1);
g.setFontAntonioLarge(); g.setFontAntonioLarge();
g.drawString(timeStr, 28, 10); g.drawString(timeStr, 29, 10);
// Write date // Write date
g.setColor(cWhite); g.setColor(cWhite);
@ -255,7 +260,7 @@ function drawPosition0(){
var dayStr = locale.dow(currentDate, true).toUpperCase(); var dayStr = locale.dow(currentDate, true).toUpperCase();
dayStr += " " + currentDate.getDate(); dayStr += " " + currentDate.getDate();
dayStr += " " + currentDate.getFullYear(); dayStr += " " + currentDate.getFullYear();
g.drawString(dayStr, 29, 56); g.drawString(dayStr, 32, 56);
// Draw data // Draw data
g.setFontAlign(-1, -1, 0); g.setFontAlign(-1, -1, 0);
@ -401,7 +406,7 @@ function draw(){
* Step counter via widget * Step counter via widget
*/ */
function getSteps() { function getSteps() {
var steps = 0 var steps = 0;
try { try {
health = require("health"); health = require("health");
} catch(ex) { } catch(ex) {
@ -553,6 +558,10 @@ Bangle.on("drag", e => {
} }
}); });
Bangle.on("touch", e => {
Bangle.showLauncher();
});
/* /*
* Lets start widgets, listen for btn etc. * Lets start widgets, listen for btn etc.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

8
apps/presentor/ChangeLog Normal file
View File

@ -0,0 +1,8 @@
0.1: Start of app.
0.5: BLE keyboard functionality.
1.0: BLE mouse functionality to scroll back/forward.
1.5: Added accelerator style mouse.
2.0: Added touchpad style mouse.
2.1: Initial internal git(hub) release. Added icon and such.
2.2: Begin work on presentation parts.
3.0: Presentation parts!

2
apps/presentor/README.md Normal file
View File

@ -0,0 +1,2 @@
# Presentor
Use your Bangle to present!

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4ASlgADGmIxwLV4wqGQowfWZQwjKw4wJF7ghBmVWmQlELYoweFwYABGAwrHF7IuFGAwrIF7AuHMJADGF0AwHAYovWFxaSHADQuDEgIADYZYucAQOB1fQ1eBmQwiFwlX6AAE1gqBGD6MEmQqBwIICwIGB0rDeWYksFAIuBz+fvQHC6D0dcQssEwIuB4fC4V0M4VXF7YuFDYLqBlnDF4WeHAYugL5N6L4I4BF0IvB0vQvdQGAJeBY4YucmQxEAgJgBvYOBdwYQDFzUzmZ/EllXFIKKBAYVXBwSMaller7fFAoKSBAAOBFQK7dPoJfFEoYADRjgmEeIzFFdUQAOdUIumdRLtDAAIufdRQKCr8zCQjqmE4NeF4YudWxpqCFyovBK5LqiF6DqdR6D0BdTYwJGg5sBdTQwKEAIwHdTQwMSpQueYaAufGBouiGBYukGBIumGA4uoGAouqGAYABF1QAl"))

471
apps/presentor/app.js Normal file
View File

@ -0,0 +1,471 @@
// Presentor by 7kasper (Kasper Müller)
// Version 3.0
const SpecialReport = new Uint8Array([
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x02, // USAGE (Mouse)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x01, // REPORT_ID (1)
0x09, 0x01, // USAGE (Pointer)
0xa1, 0x00, // COLLECTION (Physical)
0x05, 0x09, // USAGE_PAGE (Button)
0x19, 0x01, // USAGE_MINIMUM (Button 1)
0x29, 0x05, // USAGE_MAXIMUM (Button 5)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x95, 0x05, // REPORT_COUNT (5)
0x75, 0x01, // REPORT_SIZE (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x03, // REPORT_SIZE (3)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x09, 0x38, // USAGE (Wheel)
0x15, 0x81, // LOGICAL_MINIMUM (-127)
0x25, 0x7f, // LOGICAL_MAXIMUM (127)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x03, // REPORT_COUNT (3)
0x81, 0x06, // INPUT (Data,Var,Rel)
0x05, 0x0c, // USAGE_PAGE (Consumer Devices)
0x0a, 0x38, 0x02, // USAGE (AC Pan)
0x15, 0x81, // LOGICAL_MINIMUM (-127)
0x25, 0x7f, // LOGICAL_MAXIMUM (127)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x06, // INPUT (Data,Var,Rel)
0xc0, // END_COLLECTION
0xc0, // END_COLLECTION
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x02, // REPORT_ID (2)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x01, // INPUT (Cnst,Ary,Abs)
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
0x29, 0x73, // USAGE_MAXIMUM (Keyboard F24)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x73, // LOGICAL_MAXIMUM (115)
0x95, 0x05, // REPORT_COUNT (5)
0x75, 0x08, // REPORT_SIZE (8)
0x81, 0x00, // INPUT (Data,Ary,Abs)
0xc0 // END_COLLECTION
]);
const MouseButton = {
NONE : 0,
LEFT : 1,
RIGHT : 2,
MIDDLE : 4,
BACK : 8,
FORWARD: 16
};
const kb = require("ble_hid_keyboard");
const Layout = require("Layout");
const Locale = require("locale");
let mainLayout = new Layout({
'type': 'v',
filly: 1,
c: [
{
type: 'txt',
font: '6x8',
label: 'Presentor',
valign: -1,
halign: 0,
col: g.theme.fg1,
// bgCol: g.theme.bg2,
bgCol: '#00F',
fillx: 1,
}, {
type: 'h',
fillx: 1,
c: [
{
type: 'txt',
font: '15%',
label: '00:00',
id: 'Time',
halign: -1,
pad: 3
}, {
fillx: 1
}, {
type: 'txt',
font: '15%',
label: '00:00',
id: 'Timer',
halign: 1,
pad: 3
}
]
}, {
type: 'txt',
font: '10%',
label: '+00:00',
id: 'RestTime',
col: '#fff'
}, {
type: 'txt',
font: '10%',
label: '--------------'
}, {
type: 'txt',
font: '15%',
label: 'Presenting',
id: 'Subject'
}, {
type: 'txt',
font: '6x8',
label: 'Swipe up to start the time.',
id: 'Notes',
col: '#ff0',
fillx: 1,
filly: 1,
valign: 1
}
]
}, {lazy:true});
let settings = {pparts: [], sversion: 0};
let HIDenabled = true;
// Application variables
let pparti = -1;
let ppartBuzzed = false;
let restBuzzed = false;
let lastx = 0;
let lasty = 0;
// Mouse states
let holding = false;
let trackPadMode = false;
// Timeout IDs.
let timeoutId = -1;
let timeoutHolding = -1;
let timeoutDraw = -1;
let homeRoll = 0;
let homePitch = 0;
let mCal = 0;
let mttl = 0;
let cttl = 0;
// BT helper.
let clearToSend = true;
// Presentation Timers
let ptimers = [];
function delay(t, v) {
return new Promise((resolve) => {
setTimeout(resolve, t)
});
}
function formatTimePart(time) {
time = Math.floor(Math.abs(time));
return time < 10 ? `0${time}` : `${time}`;
}
function formatTime(time, doPlus) {
if (time == Infinity) return ' --:-- ';
return `${time < 0 ? '-' : (doPlus ? '+' : '')}${formatTimePart(time/60)}:${formatTimePart(time%60)}`;
}
function loadSettings() {
settings = require("Storage").readJSON('presentor.json');
for (let i = 0; i < settings.pparts.length; i++) {
ptimers[i] = {
active: false,
tracked: -1,
left: settings.pparts[i].minutes * 60 + settings.pparts[i].seconds
};
}
}
function getCurrentTimer() {
if (pparti < 0) return Infinity;
if (!settings.pparts || pparti >= settings.pparts.length) return Infinity;
if (ptimers[pparti].tracked == -1) return Infinity;
ptimers[pparti].left -= (getTime() - ptimers[pparti].tracked);
ptimers[pparti].tracked = getTime();
// if we haven't buzzed yet and timer became negative just buzz here.
// TODO better place?
if (ptimers[pparti].left <= 0 && !ppartBuzzed) {
Bangle.buzz(400)
.then(() => delay(400))
.then(() => Bangle.buzz(400));
ppartBuzzed = true;
}
return ptimers[pparti].left;
}
function getRestTime() {
let rem = 0;
// Add all remaining time from previous presentation parts.
for (let i = 0; i < pparti; i++) {
rem += ptimers[i].left;
}
if (pparti >= 0 && pparti < ptimers.length && ptimers[pparti].left < 0) {
rem += ptimers[pparti].left;
}
// if we haven't buzzed yet and timer became negative just buzz here.
// TODO better place?
if (rem < 0 && !restBuzzed) {
Bangle.buzz(200)
.then(() => delay(400))
.then(() => Bangle.buzz(200))
.then(() => delay(400))
.then(() => Bangle.buzz(200));
restBuzzed = true;
}
return rem;
}
function drawMainFrame() {
var d = new Date();
// update time
mainLayout.Time.label = Locale.time(d,1);
// update timer
mainLayout.Timer.label = formatTime(getCurrentTimer());
let restTime = getRestTime();
mainLayout.RestTime.label = formatTime(restTime, true);
mainLayout.RestTime.col = restTime < 0 ? '#f00' : (restTime > 0 ? '#0f0' : '#fff');
mainLayout.render();
// schedule a draw for the next minute
if (timeoutDraw != -1) clearTimeout(timeoutDraw);
timeoutDraw = setTimeout(function() {
timeoutDraw = -1;
drawMainFrame();
}, 1000 - (Date.now() % 1000));
}
function drawMain() {
g.clear();
mainLayout.forgetLazyState();
drawMainFrame();
// mainLayout.render();
// E.showMessage('Presentor');
}
function doPPart(r) {
pparti += r;
if (pparti < 0) {
pparti = -1;
mainLayout.Subject.label = 'PAUSED';
mainLayout.Notes.label = 'Swipe up to start again.';
return;
}
if (!settings.pparts || pparti >= settings.pparts.length) {
pparti = settings.pparts.length;
mainLayout.Subject.label = 'FINISHED';
mainLayout.Notes.label = 'Good Job!';
return;
}
let ppart = settings.pparts[pparti];
mainLayout.Subject.label = ppart.subject;
mainLayout.Notes.label = ppart.notes;
ptimers[pparti].tracked = getTime();
// We haven't buzzed if there was time left.
ppartBuzzed = ptimers[pparti].left <= 0;
// Always reset buzzstate for the rest timer.
restBuzzed = getRestTime() < 0;
drawMainFrame();
}
NRF.setServices(undefined, { hid : SpecialReport });
// TODO: figure out how to detect HID.
NRF.on('HID', function() {
HIDenabled = true;
});
function moveMouse(x,y,b,wheel,hwheel,callback) {
if (!HIDenabled) return;
if (!b) b = 0;
if (!wheel) wheel = 0;
if (!hwheel) hwheel = 0;
NRF.sendHIDReport([1,b,x,y,wheel,hwheel,0,0], function() {
if (callback) callback();
});
}
// function getSign(x) {
// return ((x > 0) - (x < 0)) || +x;
// }
function scroll(wheel,hwheel,callback) {
moveMouse(0,0,0,wheel,hwheel,callback);
}
// Single click a certain button (immidiatly release).
function clickMouse(b, callback) {
if (!HIDenabled) return;
NRF.sendHIDReport([1,b,0,0,0,0,0,0], function() {
NRF.sendHIDReport([1,0,0,0,0,0,0,0], function() {
if (callback) callback();
});
});
}
function pressKey(keyCode, modifiers, callback) {
if (!HIDenabled) return;
if (!modifiers) modifiers = 0;
NRF.sendHIDReport([2, modifiers,0,keyCode,0,0,0,0], function() {
NRF.sendHIDReport([2,0,0,0,0,0,0,0], function() {
if (callback) callback();
});
});
}
function handleAcc(acc) {
let rRoll = acc.y * -50;
let rPitch = acc.x * -100;
if (mCal > 10) {
//console.log("x: " + (rRoll - homeRoll) + " y:" + (rPitch - homePitch));
moveMouse(acc.y * -50 - homeRoll, acc.x * -100 - homePitch);
} else {
//console.log("homeroll: " +homeRoll +"homepitch: " + homePitch);
homeRoll = rRoll * 0.7 + homeRoll * 0.3;
homePitch = rPitch * 0.7 + homePitch * 0.3;
mCal = mCal + 1;
}
}
Bangle.on('lock', function(on) {
if (on && holding) {
Bangle.setLocked(false);
Bangle.setLCDPower(1);
}
});
function startHolding() {
pressKey(kb.KEY.F10);
holding = true;
Bangle.buzz();
E.showMessage('Holding');
Bangle.on('accel', handleAcc);
Bangle.setLCDPower(1);
}
function stopHolding() {
clearTimeout(timeoutId);
if (holding) {
pressKey(kb.KEY.F10);
homePitch = 0;
homeRoll = 0;
holding = false;
mCal = 0;
Bangle.removeListener('accel', handleAcc);
Bangle.buzz();
drawMain();
} else {
timeoutId = setTimeout(drawMain, 1000);
}
clearTimeout(timeoutHolding);
timeoutHolding = -1;
}
Bangle.on('drag', function(e) {
if (cttl == 0) { cttl = getTime(); }
if (trackPadMode) {
if (lastx + lasty == 0) {
lastx = e.x;
lasty = e.y;
mttl = getTime();
}
if (clearToSend) {
clearToSend = false;
let difX = e.x - lastx, difY = e.y - lasty;
let dT = getTime() - mttl;
let vX = difX / dT, vY = difY / dT;
//let qX = getSign(difX) * Math.pow(Math.abs(difX), 1.2);
//let qY = getSign(difY) * Math.pow(Math.abs(difY), 1.2);
let qX = difX + 0.02 * vX, qY = difY + 0.02 * vY;
moveMouse(qX, qY, 0, 0, 0, function() {
setTimeout(function() {clearToSend = true;}, 50);
});
lastx = e.x;
lasty = e.y;
mttl = getTime();
console.log("Dx: " + (qX) + " Dy: " + (qY));
}
if (!e.b) {
// short press
if (getTime() - cttl < 0.2) {
clickMouse(MouseButton.LEFT);
console.log("click left");
}
// longer press in center
else if (getTime() - cttl < 0.6 && e.x > g.getWidth()/4 && e.x < 3 * g.getWidth()/4 && e.y > g.getHeight() / 4 && e.y < 3 * g.getHeight() / 4) {
clickMouse(MouseButton.RIGHT);
console.log("click right");
}
cttl = 0;
lastx = 0;
lasty = 0;
}
} else {
if(!e.b){
Bangle.buzz(100);
if(lasty > 40){
doPPart(-1);
// E.showMessage('down');
} else if(lasty < -40){
doPPart(1);
// E.showMessage('up');
} else if(lastx > 40){
// E.showMessage('right');
//kb.tap(kb.KEY.RIGHT, 0);
scroll(-1);
} else if(lastx < -40){
// E.showMessage('left');
//kb.tap(kb.KEY.LEFT, 0);
scroll(1);
} else if(lastx==0 && lasty==0 && holding == false){
// E.showMessage('press');
clickMouse(MouseButton.LEFT);
}
stopHolding();
lastx = 0;
lasty = 0;
} else{
lastx = lastx + e.dx;
lasty = lasty + e.dy;
if (timeoutHolding == -1) {
timeoutHolding = setTimeout(startHolding, 500);
}
}
}
});
function onBtn() {
if (trackPadMode) {
trackPadMode = false;
stopHolding();
drawMain();
} else {
clearToSend = true;
trackPadMode = true;
E.showMessage('Mouse');
}
Bangle.buzz();
}
setWatch(onBtn, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat: true});
loadSettings();
drawMain();

BIN
apps/presentor/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,285 @@
<!-- Presentor by 7kasper (Kasper Müller) -->
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0"/>
<meta charset="UTF-8">
<link rel="stylesheet" href="../../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<style>
.qcent {
display: flex;
width: 100%;
justify-content: space-evenly;
}
h1 {
margin-bottom: 0;
}
.iwrap {
max-height: 100%;
}
.iwrap img {
height: 100%;
padding: 10%;
}
hr {
border: 0;
height: 2px;
background-image: -webkit-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
background-image: -moz-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
background-image: -ms-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
background-image: -o-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
}
.fullbtn button {
flex-grow: 1;
margin-left: 1rem;
margin-right: 1rem;
}
.ppartrow {
display: flex;
width: 100%;
justify-content: space-between;
font-weight: bolder;
}
.timerselector {
display: flex;
justify-content: space-between;
inset: 1px solid black;
height: 1.5rem;
}
.timerselector input {
width: 45%;
}
.pp-order {
width: calc(150% / 14);
display: flex;
justify-content: space-between;
align-items: center;
}
.pp-order-r {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.pp-subject {
width: calc(300% / 14);
}
.pp-timer {
width: calc(250% / 14);
}
.pp-notes {
width: calc(700% / 14);
}
input {
height: 1.5rem;
}
.icon-cross {
color: #e85600;
cursor: pointer;
transition: transform 0.2s ease-in-out;
}
.icon-cross:hover {
transform: scale(1.1);
}
.draghandle {
cursor: grab;
}
footer {
display: inline-block;
width: 100%;
text-align: center;
font-size: 0.6rem;
color: grey;
}
</style>
</head>
<body>
<header class="qcent">
<h1>Presentor</h1>
<div class="iwrap"><img src="app.png"/></div>
</header>
<hr/>
<div class="form-group" id="subber">
<div id="pparthdr" class="ppartrow">
<div class="pp-order">#</div>
<div class="pp-subject">Subject</div>
<div class="pp-timer">Time</div>
<div class="pp-notes">Notes</div>
</div>
<div id="loader">Loading...</div>
<div id="subber-data"></div>
</div>
<hr/>
<div class="qcent fullbtn">
<button class="btn btn-default" id="btnSave">Save</button>
<button class="btn btn-error" id="btnClear">Clear</button>
</div>
<footer>Presentor by <a href="https://kaspermuller.nl" target="_blank">Kasper Müller</a></footer>
<script src="../../core/lib/interface.js"></script>
<script>
const subber = document.getElementById('subber-data');
let cmpStr = ''; //compare string to see if there are changes with previous save.
function ppartsAreFilled() {
let pparts = subber.children;
for (let i = 0; i < pparts.length; i++) {
if (!pparts[i].getElementsByClassName('pp-subject')[0].value) return false;
}
return true;
}
function getJSON() {
let ret = {
pparts: [],
sversion: 2.2
}
let jparts = [];
let pparts = subber.children;
for (let i = 0; i < pparts.length; i++) {
let rpart = {};
let ppart = pparts[i];
rpart.subject = ppart.getElementsByClassName('pp-subject')[0].value;
if (!rpart.subject) continue;
rpart.minutes = ppart.getElementsByClassName('pp-timer')[0].children[0].value | 0;
rpart.seconds = ppart.getElementsByClassName('pp-timer')[0].children[2].value | 0;
rpart.notes = ppart.getElementsByClassName('pp-notes')[0].value;
jparts.push(rpart);
}
ret.pparts = jparts;
return JSON.stringify(ret);
}
function save() {
let savestr = getJSON();
Util.showModal('Saving...');
Puck.write(`\x10require('Storage').writeJSON(${JSON.stringify('presentor.json')},${savestr})\n`,()=>{
Util.hideModal();
});
cmpStr = savestr;
}
function loadJSON(str) {
cmpStr = str;
let settings = JSON.parse(str);
let jparts = settings.pparts;
for (let i = 0; i < jparts.length; i++) {
addFormPPart(jparts[i]);
}
addFormPPart(); // add empty element on startup
}
function load() {
Util.showModal('Loading...');
Puck.eval(`require('Storage').read(${JSON.stringify('presentor.json')})`,data => {
Util.hideModal();
loadJSON(data);
});
}
function addFormPPart(partData = {}) {
let part = document.createElement('div');
part.classList.add('ppartrow');
let orderThing = document.createElement('div');
orderThing.classList.add('pp-order', 'pp-order-r');
let orderItem = document.createElement('i');
orderItem.classList.add('icon', 'icon-menu', 'draghandle');
orderThing.appendChild(orderItem);
let deleteItem = document.createElement('i');
deleteItem.classList.add('icon', 'icon-cross');
deleteItem.onclick = () => {
subber.removeChild(part);
// Make sure there stays one form thing left.
if (!subber.hasChildNodes()) addFormPPart();
}
orderThing.appendChild(deleteItem);
part.appendChild(orderThing);
let subjectField = document.createElement('input');
subjectField.classList.add('pp-subject');
subjectField.type = 'text';
subjectField.placeholder = 'Subject';
if (partData.subject) subjectField.value = partData.subject;
part.appendChild(subjectField);
let timeField = document.createElement('div');
timeField.classList.add('pp-timer', 'timerselector');
let minSelector = document.createElement('input');
minSelector.type = 'number';
minSelector.min = 0;
minSelector.step = 1
minSelector.placeholder = 'mm';
if (partData.minutes !== undefined) minSelector.value = partData.minutes;
let colon = document.createElement('p');
colon.innerText = ':';
let secSelector = document.createElement('input');
secSelector.type = 'number';
secSelector.min = 0;
secSelector.max = 59;
secSelector.step = 1
secSelector.placeholder = 'ss';
if (partData.seconds !== undefined) secSelector.value = partData.seconds;
timeField.appendChild(minSelector);
timeField.appendChild(colon);
timeField.appendChild(secSelector);
part.appendChild(timeField);
let notesField = document.createElement('input');
notesField.classList.add('pp-notes');
notesField.type = 'text';
notesField.placeholder = 'Notes (optional)';
if (partData.notes !== undefined) notesField.value = partData.notes;
part.appendChild(notesField);
subber.appendChild(part);
}
Sortable.create(subber, {
handle: '.draghandle',
animation: 150
});
document.getElementById('btnSave').onclick = () => {
save();
alert(getJSON());
}
document.getElementById('btnClear').onclick = () => {
while (subber.firstChild) {
subber.removeChild(subber.lastChild);
}
addFormPPart();
}
document.addEventListener('keyup', () => {
if (ppartsAreFilled()) {
addFormPPart();
}
});
// Simple thing to check if we need to save (TODO optimise me in the future?)
setInterval(() => {
if (cmpStr == getJSON()) {
document.getElementById('btnSave').classList.remove('btn-primary');
document.getElementById('btnSave').classList.add('btn-default');
} else {
document.getElementById('btnSave').classList.remove('btn-default');
document.getElementById('btnSave').classList.add('btn-primary');
}
}, 1000);
document.getElementById('loader').style.display = 'none';
function onInit() {
load();
}
// load from watch first.
// let qq = `[{"subject":"Hello","minutes":55,"seconds":4,"notes":""},{"subject":"dsfafds","minutes":4,"seconds":33,"notes":"fdasdfsafasfsd"},{"subject":"dsadsf","minutes":0,"seconds":4,"notes":""},{"subject":"sdasf","minutes":0,"seconds":0,"notes":""}]`;
// loadJSON(qq);
</script>
</body>
</html>

View File

@ -0,0 +1 @@
{"pparts":[{"subject":"#1","minutes":10,"seconds":0,"notes":"This is a note."},{"subject":"#2","minutes":2,"seconds":50,"notes":"Change in the app!"}],"sversion":2.2}

5
apps/puzzle15/ChangeLog Normal file
View File

@ -0,0 +1,5 @@
0.01: Initial version, UI mechanics ready, no real game play so far
0.02: Lots of enhancements, menu system not yet functional, but packaging should be now...
0.03: Menu logic now generally functioning, splash screen added. The first really playable version!
0.04: Settings dialog, about screen
0.05: Central game end function

57
apps/puzzle15/README.md Normal file
View File

@ -0,0 +1,57 @@
# Puzzle15 - A 15-puzzle for the Bangle.js 2
This is a Bangle.js 2 adoption of the famous 15 puzzle.
## The game
A board of _n_ by _n_ fields is filled with _n^2-1_ numbered stones. So, one field, the "gap", is left free.
Bring them in the correct order so that the gap is finally at the bottom right of the playing field.
The less moves you need, the better you are.
If _n_ is 4, the number of stones is _16-1=15_. Hence the name of the game.
## How to play
If you start the game, it shows a splash screen and then generates a shuffled 4x4 board with a 15 puzzle.
Move the stones with drag gestures on the screen.
If you want to move the stone below the gap upward, drag from the bottom of the screen upward.
The drag gestures can be performed anywhere on the screen, there is no need to start or end them on the stone to be moved.
If you managed to order the stones correctly, a success message appears.
You can continue with another game, go to the game's main menu, or quit the game entirely.
There is a grey menu button right of the board containing the well-known three-bar menu symbol ("Hamburger menu").
It opens the game's main menu directly from within the game.
## The main menu
Puzzle15 has a main menu which can be reached from the in-game menu button or the end-of-game message window.
It features the following options:
* **Continue** - Continue the currently running game. _This option is only shown if the main menu is opened during an open game._
* **Start 3x3**, **Start 4x4**, **Start 5x5** - Start a new game on a board with the respective dimension. Any currently open game is dropped.
* **About** Show a small "About" info box.
* **Exit** Exit Puzzle15 and return to the default watch face.
## Game settings
The game has some global settings which can be accessed on the usual way through the Bangle.js' app settings user interface.
Currently it has the following options:
* **Splash** - Define whether the game should open with a splash screen. **long** shows the splash screen for five seconds, **short** shows it for two seconds. **off** starts the app _without_ a splash screen, it directly comes up with whatever the "Start with" option says.
* **Start with** - What should happen after the splash screen (or, if it is disabled, directly at app start): **3x3**, **4x4** and **5x5** start the game with a board of the respective dimension, **menu** shows the main menu which allows to select the board size.
## Implementation notes
The game engine always generates puzzles which can be solved.
Solvability is detected by counting inversions,
i.e. pairs of stones where the stone at the earlier field (row-wise, left to right, top to bottom) has a number _greater than_ the stone on the later field, with all pairs of stones compared.
The algorithm is described at https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ .
## The splash screen
The Splash screen shows a part of the illustration "The 14-15-puzzle in puzzleland" from Sam Loyd. Other than Puzzle15, it depicts a 15 puzzle with the stones "14" and "15" swapped. This puzzle is indeed *not* solvable.
Have fun!

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgn/AC3+7oAD7e7AAW8BQndBQe79/9DomgHocH74KD/RJE34Xax4XDtvoC4fJ54XDluAC4f2z4XDzm/C4ett4XD34OBF4e/I4m+C4f8r4XChHuC5U98oXEF4cP7/AC5O9mYXC/2/F4cGtwvE/SsBC4Ws7gvD7YCBL4ULO4i/u1QAD7QED1e6AoetCAnf/YeE1wpD/lgBQcKIAgXG14LD/twC5kL3Z+BC4P+LgIXBg272wXD7wXEh7eCC4PWzIXChHtOoIXB/WX54XDh3KmAXC1oLBI4UD+AXC+/rdIIvD5wvD3O4C4cJ4AXC/dUI4kJhgMBC4Ov+AXDh9QC4X2/gvEhvvoAXC81dC4duR4f8wSncC6v8u4AD3ndAAXcy4KDtYKD7vf/oGE2wRDvPNBQfLFAnP/o2EVIIACg7yBAATZBAAe/C7P9g4XCx+wn/6C4Op//AC4MK+cI/+QC4X2/fPC4PM2HKh8H7vpewIXBhvThV5+AXC+/5C4UL2HHC4Pf/P/AIJHB6cAj2wC4X+3AXPhADBF4fX94XB1va1vOC4PXAIX6hfrxvb0CPD7p3C1e6hW2C4LOBAIIXB3eJ3YXEX78GM4IAC9QXG1QAD7QEDJYIFD14oE//7DwgME/twBQcPC70G6EG5dQ1/8VYPtC4ObgfM5IXHr/whvO4Gvy6LBtX9vfugnr3AXHkXggGOC4P97/43X9ukOgnv6BfIC4Oe2AXC6+nI4MOgfI9QXJhssF4f91AXCgnA9IXHr3u1HusGv3Ob//s/t693l3xHJX9v+3YAD7oAE5YKD34XFAC4="))

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,50 @@
// Settings menu for the Puzzle15 app
(function(back) {
var FILE = "puzzle15.json";
// Load settings
var settings = Object.assign({
splashMode: "long",
startWith: "4x4"
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
// Helper method which uses int-based menu item for set of string values
function stringItems(startvalue, writer, values) {
return {
value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
format: v => values[v],
min: 0,
max: values.length - 1,
wrap: true,
step: 1,
onchange: v => {
writer(values[v]);
writeSettings();
}
};
}
// Helper method which breaks string set settings down to local settings object
function stringInSettings(name, values) {
return stringItems(settings[name], v => settings[name] = v, values);
}
var mainmenu = {
"": {
"title": "15 Puzzle"
},
"< Back": () => back(),
"Splash": stringInSettings("splashMode", ["long", "short", "off"]),
"Start with": stringInSettings("startWith", ["3x3", "4x4", "5x5", "menu"])
};
// Actually display the menu
E.showMenu(mainmenu);
});
// end of file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -11,3 +11,4 @@
0.12: Allow hiding the widget 0.12: Allow hiding the widget
0.13: Tweak Bangle.js 2 light theme colors 0.13: Tweak Bangle.js 2 light theme colors
0.14: Use weather condition code for icon selection 0.14: Use weather condition code for icon selection
0.15: Fix widget icon

View File

@ -53,6 +53,16 @@ exports.get = function() {
scheduleExpiry(storage.readJSON('weather.json')||{}); scheduleExpiry(storage.readJSON('weather.json')||{});
/**
*
* @param cond Weather condition, as one of:
* {number} code: (Preferred form) https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
* {string} weather description (in English: breaks for other languages!)
* {object} use cond.code if present, or fall back to cond.txt
* @param x Left
* @param y Top
* @param r Icon Size
*/
exports.drawIcon = function(cond, x, y, r) { exports.drawIcon = function(cond, x, y, r) {
var palette; var palette;
@ -249,32 +259,35 @@ exports.drawIcon = function(cond, x, y, r) {
g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6); g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6);
} }
function chooseIcon(condition) { /*
if (!condition) return () => {}; * Choose weather icon to display based on weather description
condition = condition.toLowerCase(); */
if (condition.includes("thunderstorm")) return drawThunderstorm; function chooseIconByTxt(txt) {
if (condition.includes("freezing")||condition.includes("snow")|| if (!txt) return () => {};
condition.includes("sleet")) { txt = txt.toLowerCase();
if (txt.includes("thunderstorm")) return drawThunderstorm;
if (txt.includes("freezing")||txt.includes("snow")||
txt.includes("sleet")) {
return drawSnow; return drawSnow;
} }
if (condition.includes("drizzle")|| if (txt.includes("drizzle")||
condition.includes("shower")) { txt.includes("shower")) {
return drawRain; return drawRain;
} }
if (condition.includes("rain")) return drawShowerRain; if (txt.includes("rain")) return drawShowerRain;
if (condition.includes("clear")) return drawSun; if (txt.includes("clear")) return drawSun;
if (condition.includes("few clouds")) return drawFewClouds; if (txt.includes("few clouds")) return drawFewClouds;
if (condition.includes("scattered clouds")) return drawCloud; if (txt.includes("scattered clouds")) return drawCloud;
if (condition.includes("clouds")) return drawBrokenClouds; if (txt.includes("clouds")) return drawBrokenClouds;
if (condition.includes("mist") || if (txt.includes("mist") ||
condition.includes("smoke") || txt.includes("smoke") ||
condition.includes("haze") || txt.includes("haze") ||
condition.includes("sand") || txt.includes("sand") ||
condition.includes("dust") || txt.includes("dust") ||
condition.includes("fog") || txt.includes("fog") ||
condition.includes("ash") || txt.includes("ash") ||
condition.includes("squalls") || txt.includes("squalls") ||
condition.includes("tornado")) { txt.includes("tornado")) {
return drawMist; return drawMist;
} }
return drawUnknown; return drawUnknown;
@ -298,7 +311,6 @@ exports.drawIcon = function(cond, x, y, r) {
case 531: return drawShowerRain; case 531: return drawShowerRain;
default: return drawRain; default: return drawRain;
} }
break;
case 6: return drawSnow; case 6: return drawSnow;
case 7: return drawMist; case 7: return drawMist;
case 8: case 8:
@ -308,16 +320,21 @@ exports.drawIcon = function(cond, x, y, r) {
case 802: return drawCloud; case 802: return drawCloud;
default: return drawBrokenClouds; default: return drawBrokenClouds;
} }
break;
default: return drawUnknown; default: return drawUnknown;
} }
} }
if (cond.code && cond.code > 0) { function chooseIcon(cond) {
chooseIconByCode(cond.code)(x, y, r); if (typeof (cond)==="object") {
} else { if ("code" in cond) return chooseIconByCode(cond.code);
chooseIcon(cond.txt)(x, y, r); if ("txt" in cond) return chooseIconByTxt(cond.txt);
} else if (typeof (cond)==="number") {
return chooseIconByCode(cond.code);
} else if (typeof (cond)==="string") {
return chooseIconByTxt(cond.txt);
}
return drawUnknown;
} }
chooseIcon(cond)(x, y, r);
}; };

View File

@ -52,8 +52,8 @@
if (!w) return; if (!w) return;
g.reset(); g.reset();
g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23); g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23);
if (w.txt) { if (w.code||w.txt) {
weather.drawIcon(w.txt, this.x+10, this.y+8, 7.5); weather.drawIcon(w, this.x+10, this.y+8, 7.5);
} }
if (w.temp) { if (w.temp) {
let t = require('locale').temp(w.temp-273.15); // applies conversion let t = require('locale').temp(w.temp-273.15); // applies conversion