gbmusic: Bangle.js 2 support, use Layout library

master
Richard de Boer 2021-11-19 22:37:41 +01:00
parent 100384f2e8
commit f5a8b809d1
8 changed files with 376 additions and 388 deletions

View File

@ -3644,15 +3644,15 @@
"id": "gbmusic", "id": "gbmusic",
"name": "Gadgetbridge Music Controls", "name": "Gadgetbridge Music Controls",
"shortName": "Music Controls", "shortName": "Music Controls",
"version": "0.05", "version": "0.06",
"description": "Control the music on your Gadgetbridge-connected phone", "description": "Control the music on your Gadgetbridge-connected phone",
"icon": "icon.png", "icon": "icon.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_2.png"}], "screenshots": [{"url":"screenshot_v1.png"},{"url":"screenshot_v2.png"}],
"type": "app", "type": "app",
"tags": "tools,bluetooth,gadgetbridge,music", "tags": "tools,bluetooth,gadgetbridge,music",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"allow_emulator": false, "allow_emulator": true,
"storage": [ "storage": [
{"name":"gbmusic.app.js","url":"app.js"}, {"name":"gbmusic.app.js","url":"app.js"},
{"name":"gbmusic.settings.js","url":"settings.js"}, {"name":"gbmusic.settings.js","url":"settings.js"},

View File

@ -3,3 +3,4 @@
0.03: Only auto-start if active app is a clock, auto close after 1 hour of inactivity 0.03: Only auto-start if active app is a clock, auto close after 1 hour of inactivity
0.04: Setting to disable touch controls, minor bugfix 0.04: Setting to disable touch controls, minor bugfix
0.05: Setting to disable double/triple press control, remove touch controls setting, reduce fadeout flicker 0.05: Setting to disable double/triple press control, remove touch controls setting, reduce fadeout flicker
0.06: Bangle.js 2 support

View File

@ -3,7 +3,9 @@
If you have an Android phone with Gadgetbridge, this app allows you to view If you have an Android phone with Gadgetbridge, this app allows you to view
and control music playback. and control music playback.
![Screenshot: playing](screenshot.png) ![Screenshot: paused](screenshot_2.png) | Bangle.js 1 | Bangle.js 2 |
|:-------------------------------------------|:-------------------------------------------|
| ![Screenshot: Bangle 1](screenshot_v1.png) | ![Screenshot: Bangle 2](screenshot_v2.png) |
Download the [latest Gadgetbridge for Android here](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/). Download the [latest Gadgetbridge for Android here](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/).
@ -23,25 +25,27 @@ Automatically load the app when you play music and close when the music stops.
(If the app opened automatically, it closes after music has been paused for 5 minutes.) (If the app opened automatically, it closes after music has been paused for 5 minutes.)
**Simple button**: **Simple button**:
Disable double/triple pressing Button 2: always simply toggle play/pause. Disable double/triple pressing Middle Button: always simply toggle play/pause.
(For music players which handle multiple button presses themselves.) (For music players which handle multiple button presses themselves.)
## Controls ## Controls
### Buttons ### Buttons
* Button 1: Volume up * Button 1 (*Bangle.js 1*): Volume up
* Button 2: * Middle Button:
- Single press: toggle play/pause - Single press: Toggle play/pause
- Double press: next song - Double press: Next song
- Triple press: previous song - Triple press: Previous song
- Long-press: open application launcher - Long-press: open application launcher
* Button 3: Volume down * Button 3 (*Bangle.js 1*): Volume down
### Touch ### Touch
* Left: pause/previous song * Left: Pause/previous song
* Right: next song/resume * Right: Next song/resume
* Center: toggle play/pause * Center: Toggle play/pause
* Swipe: next/previous song * Swipe left/right: Next/previous song
* Swipe up/down (*Bangle.js 2*): Volume up/down
## Creator ## Creator

View File

@ -4,77 +4,9 @@
**/ **/
let auto = false; // auto close if opened automatically let auto = false; // auto close if opened automatically
let stat = ""; let stat = "";
let info = {
artist: "",
album: "",
track: "",
n: 0,
c: 0,
};
const POUT = 300000; // auto close timeout when paused: 5 minutes (in ms) const POUT = 300000; // auto close timeout when paused: 5 minutes (in ms)
const IOUT = 3600000; // auto close timeout for inactivity: 1 hour (in ms) const IOUT = 3600000; // auto close timeout for inactivity: 1 hour (in ms)
const BANGLE2 = process.env.HWVERSION===2;
///////////////////////
// Self-repeating timeouts
///////////////////////
// Clock
let tock = -1;
function tick() {
if (!Bangle.isLCDOn()) {
return;
}
const now = new Date;
if (now.getHours()*60+now.getMinutes()!==tock) {
drawDateTime();
tock = now.getHours()*60+now.getMinutes();
}
setTimeout(tick, 1000); // we only show minute precision anyway
}
// Fade out while paused and auto closing
let fade = null;
function fadeOut() {
if (!Bangle.isLCDOn() || !fade) {
return;
}
drawMusic(false); // don't clear: draw over existing text to prevent flicker
setTimeout(fadeOut, 500);
}
function brightness() {
if (!fade) {
return 1;
}
return Math.max(0, 1-((Date.now()-fade)/POUT));
}
// Scroll long track names
// use an interval to get smooth movement
let offset = null, // scroll Offset: null = no scrolling
iScroll;
function scroll() {
offset += 10;
drawScroller();
}
function scrollStart() {
if (offset!==null) {
return; // already started
}
offset = 0;
if (Bangle.isLCDOn()) {
if (!iScroll) {
iScroll = setInterval(scroll, 200);
}
drawScroller();
}
}
function scrollStop() {
if (iScroll) {
clearInterval(iScroll);
iScroll = null;
}
offset = null;
}
/** /**
* @param {string} text * @param {string} text
@ -85,21 +17,22 @@ function fitText(text) {
return Infinity; return Infinity;
} }
// make a guess, then shrink/grow until it fits // make a guess, then shrink/grow until it fits
const test = (s) => g.setFont("Vector", s).stringWidth(text); const w = Bangle.appRect.w,
let best = Math.floor(24000/test(100)); test = (s) => g.setFont("Vector", s).stringWidth(text);
if (test(best)===240) { // good guess! let best = Math.floor(100*w/test(100));
if (test(best)===w) { // good guess!
return best; return best;
} }
if (test(best)<240) { if (test(best)<w) {
do { do {
best++; best++;
} while(test(best)<=240); } while(test(best)<=w);
return best-1; return best-1;
} }
// width > 240 // width > w
do { do {
best--; best--;
} while(test(best)>240); } while(test(best)>w);
return best; return best;
} }
@ -115,14 +48,6 @@ function textCode(text) {
} }
return code%360; return code%360;
} }
// dark magic
function hsv2rgb(h, s, v) {
const f = (n) => {
const k = (n+h/60)%6;
return v-v*s*Math.max(Math.min(k, 4-k, 1), 0);
};
return {r: f(5), g: f(3), b: f(1)};
}
function f2hex(f) { function f2hex(f) {
return ("00"+(Math.round(f*255)).toString(16)).substr(-2); return ("00"+(Math.round(f*255)).toString(16)).substr(-2);
} }
@ -131,38 +56,218 @@ function f2hex(f) {
* @return {string} Semi-random color to use for given info * @return {string} Semi-random color to use for given info
*/ */
function infoColor(name) { function infoColor(name) {
let h, s, v;
if (name==="num") {
// always white
h = 0;
s = 0;
} else {
// make color depend deterministically on info // make color depend deterministically on info
let code = textCode(info[name]); let code = textCode(layout[name].label);
switch(name) { switch(name) {
case "track": // also use album case "title": // also use album and artist
code += textCode(info.album); code += textCode(layout.album.label);
// fallthrough // fallthrough
case "album": // also use artist case "album": // also use artist
code += textCode(info.artist); code += textCode(layout.artist.label);
} }
h = code%360; let rgb;
s = 0.7; if (g.getBPP()===3) {
// only pick 3-bit colors, always at full brightness
rgb = [code&1, (code&2)/2, (code&4)/4];
if (g.setColor(rgb[0], rgb[1], rgb[2]).getColor()===g.theme.bg) {
// avoid picking the bg color
rgb = rgb.map(c => 1-c);
} }
v = brightness(); return "#"+f2hex(rgb[0])+f2hex(rgb[1])+f2hex(rgb[2]);
const rgb = hsv2rgb(h, s, v); } else {
return "#"+f2hex(rgb.r)+f2hex(rgb.g)+f2hex(rgb.b); // pick any hue, adjust for brightness
const h = code%360, s = 0.7, b = brightness();
return E.HSBtoRGB(h/360, s, b);
}
}
/**
* Render scrolling title
* @param l
*/
function rScroller(l) {
g.setFont("Vector", Math.round(g.getHeight()*l.fsz.slice(0, -1)/100));
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)
.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)
.drawString(l.label, l.x-l.offset+40+w, y);
} }
/** /**
* Remember track color until info changes * Render title
* Because we need this every time we move the scroller * @param l
* @return {string}
*/ */
function trackColor() { function rTitle(l) {
if (!("track_color" in info) || fade) { if (l.offset!==null) {
info.track_color = infoColor("track"); rScroller(l); // already scrolling
return;
} }
return info.track_color; let size = fitText(l.label);
if (size<l.h/2) {
// the title is too long: start the scroller
scrollStart();
} else {
rInfo(l);
}
}
/**
* Render info field
* @param l
*/
function rInfo(l) {
let size = fitText(l.label);
if (size>l.h) {
size = l.h;
}
g.setFont("Vector", size)
.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() {
global.gbmusic_active = true; // we don't need our widget (needed for <2.09 devices)
Bangle.loadWidgets();
Bangle.drawWidgets();
delete (global.gbmusic_active);
const Layout = require("Layout");
layout = new Layout({
type: "v", c: [
{
type: "h", fillx: 1, c: [
{id: "time", type: "txt", label: "88:88", valign: -1, halign: -1, font: "8%", bgCol: g.theme.bg},
{fillx: 1},
{id: "num", type: "txt", label: "88:88", valign: -1, halign: 1, font: "12%", bgCol: g.theme.bg},
BANGLE2 ? {} : {id: "up", type: "txt", label: " +", font: "6x8:2"},
],
},
{id: "title", type: "custom", label: "", fillx: 1, filly: 2, offset: null, font: "Vector:20%", render: rTitle, bgCol: g.theme.bg},
{id: "artist", type: "custom", label: "", fillx: 1, filly: 1, size: 30, render: rInfo, bgCol: g.theme.bg},
{id: "album", type: "custom", label: "", fillx: 1, filly: 1, size: 20, render: rInfo, bgCol: g.theme.bg},
{height: 10},
{
type: "h", c: [
{width: 3},
{id: "prev", type: "custom", height: 15, width: 15, icon: "previous", render: rIcon, bgCol: g.theme.bg},
{id: "date", type: "txt", halign: 0, valign: 1, label: "", font: "8%", fillx: 1, bgCol: g.theme.bg},
{id: "next", type: "custom", height: 15, width: 15, icon: "next", render: rIcon, bgCol: g.theme.bg},
BANGLE2 ? {width: 3} : {id: "down", type: "txt", label: " -", font: "6x8:2"},
],
},
{height: 10},
],
}, {lazy: true});
layout.render();
}
///////////////////////
// Self-repeating timeouts
///////////////////////
// Clock
let tock = -1;
function tick() {
if (!BANGLE2 && !Bangle.isLCDOn()) {
return;
}
const now = new Date();
if (now.getHours()*60+now.getMinutes()!==tock) {
drawDateTime();
tock = now.getHours()*60+now.getMinutes();
}
setTimeout(tick, 1000); // we only show minute precision anyway
}
// Fade out while paused and auto closing
let fade = null;
function fadeOut() {
if (BANGLE2 || !Bangle.isLCDOn() || !fade) {
return;
}
layout.render();
setTimeout(fadeOut, 500);
}
function brightness() {
if (!fade) {
return 1;
}
return Math.max(0, 1-((Date.now()-fade)/POUT));
}
// Scroll long track names
// use an interval to get smooth movement
let iScroll;
function scroll() {
layout.title.offset += 10;
rScroller(layout.title);
}
function scrollStart() {
if (layout.title.offset!==null) {
return; // already started
}
layout.title.offset = 0;
if (BANGLE2 || Bangle.isLCDOn()) {
if (!iScroll) {
iScroll = setInterval(scroll, 200);
}
rScroller(layout.title);
}
}
function scrollStop() {
if (iScroll) {
clearInterval(iScroll);
iScroll = null;
}
layout.title.offset = null;
} }
//////////////////// ////////////////////
@ -172,10 +277,9 @@ function trackColor() {
* Draw date and time * Draw date and time
*/ */
function drawDateTime() { function drawDateTime() {
const now = new Date; const now = new Date();
const l = require("locale"); const l = require("locale");
const is12 = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; const is12 = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
let time;
if (is12) { if (is12) {
const d12 = new Date(now.getTime()); const d12 = new Date(now.getTime());
const hour = d12.getHours(); const hour = d12.getHours();
@ -184,29 +288,35 @@ function drawDateTime() {
} else if (hour>12) { } else if (hour>12) {
d12.setHours(hour-12); d12.setHours(hour-12);
} }
time = l.time(d12, true)+l.meridian(now); layout.time.label = l.time(d12, true)+l.meridian(now);
} else { } else {
time = l.time(now, true); layout.time.label = l.time(now, true);
} }
g.reset(); layout.date.label = require("locale").date(now, true);
g.setFont("Vector", 24) layout.render();
.setFontAlign(-1, -1) // top left
.clearRect(10, 30, 119, 54)
.drawString(time, 10, 30);
const date = require("locale").date(now, true);
g.setFont("Vector", 16)
.setFontAlign(0, 1) // bottom center
.setClipRect(35, 198, 199, 214)
.clearRect(31, 198, 199, 214)
.drawString(date, 119, 240-26);
} }
function drawControls() {
let l = layout;
const cc = a => (a ? "#f00" : "#0f0"); // control color: red for active, green for inactive
if (!BANGLE2) {
l.up.col = cc("volumeup" in tCommand);
l.down.col = cc("volumedown" in tCommand);
}
l.prev.icon = (stat==="play") ? "pause" : "prev";
l.prev.col = cc("prev" in tCommand || "pause" in tCommand);
l.next.icon = (stat==="play") ? "next" : "play";
l.next.col = cc("next" in tCommand || "play" in tCommand);
layout.render();
}
////////////////////////
// GB event handlers
///////////////////////
/** /**
* Draw track number and total count * Mangle track number and total count for display
* @param {boolean} clr - Clear area before redrawing?
*/ */
function drawNum(clr) { function formatNum(info) {
let num = ""; let num = "";
if ("n" in info && info.n>0) { if ("n" in info && info.n>0) {
num = "#"+info.n; num = "#"+info.n;
@ -214,198 +324,26 @@ function drawNum(clr) {
num += "/"+info.c; num += "/"+info.c;
} }
} }
g.reset(); return num;
g.setFont("Vector", 30)
.setFontAlign(1, -1); // top right
if (clr) {
g.clearRect(225, 30, 120, 60);
}
g.drawString(num, 225, 30);
}
/**
* Clear rectangle used by track title
*/
function clearTrack() {
g.clearRect(0, 60, 239, 119);
}
/**
* Draw track title
* @param {boolean} clr - Clear area before redrawing?
*/
function drawTrack(clr) {
let size = fitText(info.track);
if (size<25) {
// the title is too long: start the scroller
scrollStart();
return;
} else {
scrollStop();
}
// stationary track
if (size>40) {
size = 40;
}
g.reset();
g.setFont("Vector", size)
.setFontAlign(0, 1) // center bottom
.setColor(trackColor());
if (clr) {
clearTrack();
}
g.drawString(info.track, 119, 109);
}
/**
* Draw scrolling track title
*/
function drawScroller() {
g.reset();
g.setFont("Vector", 40);
const w = g.stringWidth(info.track)+40;
offset = offset%w;
g.setFontAlign(-1, 1) // left bottom
.setColor(trackColor());
clearTrack();
g.drawString(info.track, -offset+40, 109)
.drawString(info.track, -offset+40+w, 109);
} }
/**
* Draw track artist and album
* @param {boolean} clr - Clear area before redrawing?
*/
function drawArtistAlbum(clr) {
// we just use small enough fonts to make these always fit
// calculate stuff before clear+redraw
const aCol = infoColor("artist");
const bCol = infoColor("album");
let aSiz = fitText(info.artist);
if (aSiz>30) {
aSiz = 30;
}
let bSiz = fitText(info.album);
if (bSiz>20) {
bSiz = 20;
}
g.reset();
if (clr) {
g.clearRect(0, 120, 240, 189);
}
let top = 124;
if (info.artist) {
g.setFont("Vector", aSiz)
.setFontAlign(0, -1) // center top
.setColor(aCol)
.drawString(info.artist, 119, top);
top += aSiz+4; // fit album neatly under artist
}
if (info.album) {
g.setFont("Vector", bSiz)
.setFontAlign(0, -1) // center top
.setColor(bCol)
.drawString(info.album, 119, top);
}
}
/**
*
* @param {string} icon Icon name
* @param {number} x
* @param {number} y
* @param {number} s Icon size
*/
function drawIcon(icon, x, y, s) {
({
pause: function(x, y, s) {
const w1 = s/3;
g.drawRect(x, y, x+w1, y+s);
g.drawRect(x+s-w1, y, x+s, y+s);
},
play: function(x, y, s) {
g.drawPoly([
x, y,
x+s, y+s/2,
x, y+s,
], true);
},
previous: function(x, y, s) {
const w2 = s*1/5;
g.drawPoly([
x+s, y,
x+w2, y+s/2,
x+s, y+s,
], true);
g.drawRect(x, y, x+w2, y+s);
},
next: function(x, y, s) {
const w2 = s*4/5;
g.drawPoly([
x, y,
x+w2, y+s/2,
x, y+s,
], true);
g.drawRect(x+w2, y, x+s, y+s);
},
})[icon](x, y, s);
}
function controlColor(ctrl) {
return (ctrl in tCommand) ? "#ff0000" : "#008800";
}
function drawControl(ctrl, x, y) {
g.setColor(controlColor(ctrl));
const s = 20;
if (stat!==controlState) {
g.clearRect(x, y, x+s, y+s);
}
drawIcon(ctrl, x, y, s);
}
let controlState;
function drawControls() {
g.reset();
if (stat==="play") {
// left touch
drawControl("pause", 10, 190);
// right touch
drawControl("next", 200, 190);
} else {
drawControl("previous", 10, 190);
drawControl("play", 200, 190);
}
g.setFont("6x8", 2);
// BTN1
g.setFontAlign(1, -1);
g.setColor(controlColor("volumeup"));
g.drawString("+", 240, 30);
// BTN2
g.setFontAlign(1, 1);
g.setColor(controlColor("volumedown"));
g.drawString("-", 240, 210);
controlState = stat;
}
/**
* @param {boolean} [clr=true] Clear area before redrawing?
*/
function drawMusic(clr) {
clr = !(clr===false); // undefined means yes
drawNum(clr);
drawTrack(clr);
drawArtistAlbum(clr);
}
////////////////////////
// GB event handlers
///////////////////////
/** /**
* Update music info * Update music info
* @param {Object} e - Gadgetbridge musicinfo event * @param {Object} info - Gadgetbridge musicinfo event
*/ */
function musicInfo(e) { function musicInfo(info) {
info = e; scrollStop();
delete (info.t); layout.title.label = info.track || "";
offset = null; layout.album.label = info.album || "";
if (Bangle.isLCDOn()) { layout.artist.label = info.artist || "";
drawMusic(); // color depends on all labels
} layout.title.col = infoColor("title");
layout.album.col = infoColor("album");
layout.artist.col = infoColor("artist");
layout.num.label = formatNum(info);
layout.render();
rTitle(layout.title); // force redraw of title, or scroller might break
// reset auto exit interval
if (tIxt) { if (tIxt) {
clearTimeout(tIxt); clearTimeout(tIxt);
tIxt = null; tIxt = null;
@ -435,7 +373,6 @@ function musicState(e) {
tIxt = null; tIxt = null;
} }
fade = null; fade = null;
delete info.track_color;
if (auto) { // auto opened -> auto close if (auto) { // auto opened -> auto close
switch(stat) { switch(stat) {
case "stop": // never actually happens with my phone :-( case "stop": // never actually happens with my phone :-(
@ -444,7 +381,7 @@ function musicState(e) {
case "play": case "play":
// if inactive for double song duration (or an hour if unknown), load the clock // if inactive for double song duration (or an hour if unknown), load the clock
// i.e. phone finished playing without bothering to notify the watch // i.e. phone finished playing without bothering to notify the watch
tIxt = setTimeout(load, (info.dur*2000) || IOUT); tIxt = setTimeout(load, (e.dur*2000) || IOUT);
break; break;
case "pause": case "pause":
default: default:
@ -456,8 +393,7 @@ function musicState(e) {
break; break;
} }
} }
if (Bangle.isLCDOn()) { if (BANGLE2 || Bangle.isLCDOn()) {
drawMusic(false); // redraw in case we were fading out but resumed play
drawControls(); drawControls();
} }
} }
@ -473,30 +409,34 @@ function musicState(e) {
*/ */
let tPress, nPress = 0; let tPress, nPress = 0;
function startButtonWatches() { function startButtonWatches() {
let btn = BTN1;
if (!BANGLE2) {
// BTN1/3: volume control // BTN1/3: volume control
// Wait for falling edge to avoid messing with volume while long-pressing BTN3 // Wait for falling edge to avoid messing with volume while long-pressing BTN3
// to reload the watch (and same for BTN2 for consistency) // to reload the watch (and same for BTN2 for consistency)
setWatch(() => { sendCommand("volumeup"); }, BTN1, {repeat: true, edge: "falling"}); setWatch(() => { sendCommand("volumeup"); }, BTN1, {repeat: true, edge: "falling"});
setWatch(() => { sendCommand("volumedown"); }, BTN3, {repeat: true, edge: "falling"}); setWatch(() => { sendCommand("volumedown"); }, BTN3, {repeat: true, edge: "falling"});
btn = BTN2;
}
// BTN2: long-press for launcher, otherwise depends on number of presses // middle button: long-press for launcher, otherwise depends on number of presses
setWatch(() => { setWatch(() => {
if (nPress===0) { if (nPress===0) {
tPress = setTimeout(() => {Bangle.showLauncher();}, 3000); tPress = setTimeout(() => {Bangle.showLauncher();}, 3000);
} }
}, BTN2, {repeat: true, edge: "rising"}); }, btn, {repeat: true, edge: "rising"});
const s = require("Storage").readJSON("gbmusic.json", 1) || {}; const s = require("Storage").readJSON("gbmusic.json", 1) || {};
if (s.simpleButton) { if (s.simpleButton) {
setWatch(() => { setWatch(() => {
clearTimeout(tPress); clearTimeout(tPress);
togglePlay(); togglePlay();
}, BTN2, {repeat: true, edge: "falling"}); }, btn, {repeat: true, edge: "falling"});
} else { } else {
setWatch(() => { setWatch(() => {
nPress++; nPress++;
clearTimeout(tPress); clearTimeout(tPress);
tPress = setTimeout(handleButton2Press, 500); tPress = setTimeout(handleButton2Press, 500);
}, BTN2, {repeat: true, edge: "falling"}); }, btn, {repeat: true, edge: "falling"});
} }
} }
function handleButton2Press() { function handleButton2Press() {
@ -524,7 +464,7 @@ let tCommand = {};
*/ */
function sendCommand(command) { function sendCommand(command) {
Bluetooth.println(JSON.stringify({t: "music", n: command})); Bluetooth.println(JSON.stringify({t: "music", n: command}));
// for controlColor // for control color
if (command in tCommand) { if (command in tCommand) {
clearTimeout(tCommand[command]); clearTimeout(tCommand[command]);
} }
@ -539,18 +479,29 @@ function sendCommand(command) {
function togglePlay() { function togglePlay() {
sendCommand(stat==="play" ? "pause" : "play"); sendCommand(stat==="play" ? "pause" : "play");
} }
function startTouchWatches() { function pausePrev() {
sendCommand(stat==="play" ? "pause" : "previous");
}
function nextPlay() {
sendCommand(stat==="play" ? "next" : "play");
}
/**
* Setup touch+swipe for Bangle.js 1
*/
function touch1() {
Bangle.on("touch", side => { Bangle.on("touch", side => {
if (!Bangle.isLCDOn()) {return;} // for <2v10 firmware if (!Bangle.isLCDOn()) {return;} // for <2v10 firmware
switch(side) { switch(side) {
case 1: case 1:
sendCommand(stat==="play" ? "pause" : "previous"); pausePrev();
break; break;
case 2: case 2:
sendCommand(stat==="play" ? "next" : "play"); nextPlay();
break; break;
case 3: default:
togglePlay(); togglePlay();
break;
} }
}); });
Bangle.on("swipe", dir => { Bangle.on("swipe", dir => {
@ -558,16 +509,56 @@ function startTouchWatches() {
sendCommand(dir===1 ? "previous" : "next"); sendCommand(dir===1 ? "previous" : "next");
}); });
} }
/**
* Setup touch+swipe for Bangle.js 2
*/
function touch2() {
Bangle.on("touch", (side, xy) => {
const ar = Bangle.appRect;
if (xy.x<ar.x+ar.w/3) {
pausePrev();
} else if (xy.x>ar.x+ar.w*2/3) {
nextPlay();
} else {
togglePlay();
}
});
// swiping
let drag;
Bangle.on("drag", e => {
if (!drag) { // start dragging
drag = {x: e.x, y: e.y};
} else if (!e.b) { // released
const dx = e.x-drag.x, dy = e.y-drag.y;
drag = null;
if (Math.abs(dx)>Math.abs(dy)+10) {
// horizontal
sendCommand(dx>0 ? "previous" : "next");
} else if (Math.abs(dy)>Math.abs(dx)+10) {
// vertical
sendCommand(dy>0 ? "volumedown" : "volumeup");
}
}
});
}
function startTouchWatches() {
if (BANGLE2) {
touch2();
} else {
touch1();
}
}
function startLCDWatch() { function startLCDWatch() {
if (BANGLE2) {
return; // always keep drawing
}
Bangle.on("lcdPower", (on) => { Bangle.on("lcdPower", (on) => {
if (on) { if (on) {
// redraw and resume scrolling // redraw and resume scrolling
tick(); tick();
drawMusic(); layout.render();
drawControls();
fadeOut(); fadeOut();
if (offset!==null) { if (offset.offset!==null) {
drawScroller();
if (!iScroll) { if (!iScroll) {
iScroll = setInterval(scroll, 200); iScroll = setInterval(scroll, 200);
} }
@ -585,15 +576,10 @@ function startLCDWatch() {
///////////////////// /////////////////////
// Startup // Startup
///////////////////// /////////////////////
// check for saved music stat (by widget) to load
g.clear(); g.clear();
global.gbmusic_active = true; // we don't need our widget (needed for <2.09 devices)
Bangle.loadWidgets();
Bangle.drawWidgets();
delete (global.gbmusic_active);
function startEmulator() { function startEmulator() {
if (typeof Bluetooth==="undefined") { // emulator! if (typeof Bluetooth==="undefined" || typeof Bluetooth.println==="undefined") { // emulator!
Bluetooth = { Bluetooth = {
println: (line) => {console.log("Bluetooth:", line);}, println: (line) => {console.log("Bluetooth:", line);},
}; };
@ -609,6 +595,7 @@ function startWatches() {
} }
function start() { function start() {
makeUI();
// start listening for music updates // start listening for music updates
const _GB = global.GB; const _GB = global.GB;
global.GB = (event) => { global.GB = (event) => {
@ -628,43 +615,39 @@ function start() {
return; return;
} }
}; };
drawMusic();
drawControls();
startWatches(); startWatches();
tick(); tick();
startEmulator(); startEmulator();
} }
function init() { function init() {
// check for saved music status (by widget) to load
let saved = require("Storage").readJSON("gbmusic.load.json", true); let saved = require("Storage").readJSON("gbmusic.load.json", true);
require("Storage").erase("gbmusic.load.json"); require("Storage").erase("gbmusic.load.json");
if (saved) { if (saved) {
// autoloaded: load state was saved by widget // autoloaded: load state was saved by widget
info = saved.info;
stat = saved.state;
delete saved;
auto = true; auto = true;
start(); start();
} else { musicInfo(saved.info);
delete saved; musicState(saved.state);
return;
}
let s = require("Storage").readJSON("gbmusic.json", 1) || {}; let s = require("Storage").readJSON("gbmusic.json", 1) || {};
if (!("autoStart" in s)) { if ("autoStart" in s) {
// user opened the app, but has not picked a setting yet start();
return;
}
// user opened the app, but has not picked a autoStart setting yet
// ask them about autoloading now // ask them about autoloading now
E.showPrompt( E.showPrompt(
"Automatically load\n"+ "Automatically load\n"+
"when playing music?\n", "when playing music?\n"
).then(choice => { ).then(choice => {
s.autoStart = choice; s.autoStart = choice;
require("Storage").writeJSON("gbmusic.json", s); require("Storage").writeJSON("gbmusic.json", s);
delete s;
setTimeout(start, 0); setTimeout(start, 0);
}); });
} else {
delete s;
start();
}
}
} }
init(); init();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB