gbmusic: simplify code some more

Remove Ticker classes, shorten some variable names
master
Richard de Boer 2021-03-31 03:22:58 +02:00 committed by Richard de Boer
parent 5283f95320
commit 2de7a2dea0
1 changed files with 535 additions and 563 deletions

View File

@ -2,164 +2,134 @@
/** /**
* Control the music on your Gadgetbridge-connected phone * Control the music on your Gadgetbridge-connected phone
**/ **/
{ let auto = false; // auto close if opened automatically
let autoClose = false; // only if opened automatically let stat = "";
let state = ""; let info = {
let info = {
artist: "", artist: "",
album: "", album: "",
track: "", track: "",
n: 0, n: 0,
c: 0, c: 0,
}; };
const TIMEOUT = 5*1000*60; // auto close timeout: 5 minutes const TOUT = 300000; // auto close timeout: 5 minutes (in ms)
/** ///////////////////////
* Base ticker class, needs children to implement `redraw` // Self-repeating timeouts
*/ ///////////////////////
class Ticker {
constructor(ms) {
this.i = null;
this.ms = ms;
this.active = false;
this.onLCD = (on) => {
if (this.i) {
clearInterval(this.i);
this.i = null;
}
if (on) {
this.i = setInterval(() => {this.tick();}, this.ms);
this.redraw();
}
};
}
start() {
if (this.i) {
clearInterval(this.i);
}
this.i = setInterval(() => {this.tick();}, this.ms);
this.active = true;
Bangle.on("lcdPower", this.onLCD);
}
stop() {
if (this.i) {
clearInterval(this.i);
this.i = null;
}
this.active = false;
Bangle.removeListener("lcdPower", this.onLCD);
}
tick() {
// default: just redraw
if (Bangle.isLCDOn()) {
this.redraw();
}
}
}
/** // Clock
* Draw time and date let tock = -1;
*/ function tick() {
class Clock extends Ticker { if (!Bangle.isLCDOn()) {
constructor() { return;
super(1000);
this.lastTime = -1;
} }
tick() {
// only redraw if time has changed
const now = new Date; const now = new Date;
if (Bangle.isLCDOn() && now.getHours()*60+now.getMinutes()!==this.lastTime) { if (now.getHours()*60+now.getMinutes()!==tock) {
this.redraw();
this.lastTime = now.getHours()*60+now.getMinutes();
}
}
redraw() {
drawDateTime(); drawDateTime();
tock = now.getHours()*60+now.getMinutes();
} }
} setTimeout(tick, 1000); // we only show minute precision anyway
}
/** // Fade out while paused and auto closing
* Keep redrawing music while fading out let fade = null;
*/ function fadeOut() {
class Fader extends Ticker { if (!Bangle.isLCDOn() || !fade) {
constructor() { return;
super(500);
} }
redraw() {
drawMusic(); drawMusic();
} setTimeout(fadeOut, 500);
start() { }
this.since = Date.now(); function brightness() {
super.start(); if (!fade) {
}
stop() {
super.stop();
this.since = Date.now(); // force redraw at 100% brightness
this.redraw();
this.since = null;
}
brightness() {
if (!fadeOut.since) {
return 1; return 1;
} }
return Math.max(0, 1-((Date.now()-fadeOut.since)/TIMEOUT)); return Math.max(0, 1-((Date.now()-fade)/TOUT));
} }
}
/** // Scroll long track names
* Scroll long track names // use an interval to get smooth movement
*/ let offset = null, // scroll Offset: null = no scrolling
class Scroller extends Ticker { scrollI;
constructor() { function scroll() {
super(200); offset += 10;
drawScroller();
}
function scrollStart() {
if (offset!==null) {
return; // already started
} }
tick() { offset = 0;
this.offset += 10;
if (Bangle.isLCDOn()) { if (Bangle.isLCDOn()) {
this.redraw(); if (!scrollI) {
scrollI = setInterval(scroll, 200);
} }
}
redraw() {
drawScroller(); drawScroller();
} }
start() { }
this.offset = 0; function scrollStop() {
super.start(); if (scrollI) {
} clearInterval(scrollI);
scrollI = null;
} }
offset = null;
}
/** /**
* @param {string} text * @param {string} text
* @return {number} Maximum font size to make text fit on screen * @return {number} Maximum font size to make text fit on screen
*/ */
function fitText(text) { function fitText(text) {
if (!text.length) { if (!text.length) {
return Infinity; return Infinity;
} }
// make a guess, then shrink/grow until it fits // make a guess, then shrink/grow until it fits
const getWidth = (size) => g.setFont("Vector", size).stringWidth(text); const test = (s) => g.setFont("Vector", s).stringWidth(text);
let guess = Math.floor(24000/getWidth(100)); let best = Math.floor(24000/test(100));
if (getWidth(guess)===240) { // good guess! if (test(best)===240) { // good guess!
return guess; return best;
} }
if (getWidth(guess)<240) { if (test(best)<240) {
do { do {
guess++; best++;
} while(getWidth(guess)<=240); } while(test(best)<=240);
return guess-1; return best-1;
} }
// width > 240 // width > 240
do { do {
guess--; best--;
} while(getWidth(guess)>240); } while(test(best)>240);
return guess; return best;
} }
/** /**
* @param {string} text
* @return {number} Randomish but deterministic number from 0-360 for text
*/
function textCode(text) {
"ram";
let code = 0;
for(let i = 0; i<text.length; i++) {
code += text.charCodeAt(i);
}
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) {
return ("00"+(Math.round(f*255)).toString(16)).substr(-2);
}
/**
* @param name * @param name
* @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; let h, s, v;
if (name==="num") { if (name==="num") {
// always white // always white
@ -168,13 +138,6 @@
} else { } else {
// make color depend deterministically on info // make color depend deterministically on info
let code = 0; let code = 0;
const textCode = t => {
let c = 0;
for(let i = 0; i<t.length; i++) {
c += t.charCodeAt(i);
}
return c%360;
};
switch(name) { switch(name) {
case "track": case "track":
code += textCode(info.track); code += textCode(info.track);
@ -188,52 +151,43 @@
h = code%360; h = code%360;
s = 0.7; s = 0.7;
} }
v = fadeOut.brightness(); v = brightness();
// dark magic
const 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)};
};
const rgb = hsv2rgb(h, s, v); const rgb = hsv2rgb(h, s, v);
const f2hex = (f) => ("00"+(Math.round(f*255)).toString(16)).substr(-2);
return "#"+f2hex(rgb.r)+f2hex(rgb.g)+f2hex(rgb.b); return "#"+f2hex(rgb.r)+f2hex(rgb.g)+f2hex(rgb.b);
} }
/** /**
* Remember track color until info changes * Remember track color until info changes
* Because we need this every time we move the scroller * Because we need this every time we move the scroller
* @return {string} * @return {string}
*/ */
function trackColor() { function trackColor() {
if (!("track_color" in info) || fadeOut.active) { if (!("track_color" in info) || fade) {
info.track_color = infoColor("track"); info.track_color = infoColor("track");
} }
return info.track_color; return info.track_color;
} }
//////////////////// ////////////////////
// Drawing functions // Drawing functions
//////////////////// ////////////////////
/** /**
* Draw date and time * Draw date and time
* @return {*} * @return {*}
*/ */
function drawDateTime() { function drawDateTime() {
const now = new Date; const now = new Date;
const l = require("locale"); const l = require("locale");
const is12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; const is12 = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
let time; let time;
if (is12hour) { if (is12) {
const date12 = new Date(now.getTime()); const d12 = new Date(now.getTime());
const hours = date12.getHours(); const hour = d12.getHours();
if (hours===0) { if (hour===0) {
date12.setHours(12); d12.setHours(12);
} else if (hours>12) { } else if (hour>12) {
date12.setHours(hours-12); d12.setHours(hour-12);
} }
time = l.time(date12, true)+l.meridian(now); time = l.time(d12, true)+l.meridian(now);
} else { } else {
time = l.time(now, true); time = l.time(now, true);
} }
@ -249,12 +203,12 @@
.setClipRect(35, 198, 199, 214) .setClipRect(35, 198, 199, 214)
.clearRect(31, 198, 199, 214) .clearRect(31, 198, 199, 214)
.drawString(date, 119, 240-26); .drawString(date, 119, 240-26);
} }
/** /**
* Draw track number and total count * Draw track number and total count
*/ */
function drawNum() { function drawNum() {
let num = ""; let num = "";
if ("n" in info && info.n>0) { if ("n" in info && info.n>0) {
num = "#"+info.n; num = "#"+info.n;
@ -265,91 +219,96 @@
g.reset(); g.reset();
g.setFont("Vector", 30) g.setFont("Vector", 30)
.setFontAlign(1, -1) // top right .setFontAlign(1, -1) // top right
.setClipRect(225, 30, 120, 60)
.clearRect(225, 30, 120, 60) .clearRect(225, 30, 120, 60)
.drawString(num, 225, 30); .drawString(num, 225, 30);
} }
/** /**
* Clear rectangle used by track title * Clear rectangle used by track title
*/ */
function clearTrack() { function clearTrack() {
g.clearRect(0, 60, 239, 119); g.clearRect(0, 60, 239, 119);
} }
/** /**
* Draw track title * Draw track title
*/ */
function drawTrack() { function drawTrack() {
let size = fitText(info.track); 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) { if (size>40) {
size = 40; size = 40;
} }
if (size<25) {
// the title is too long: start up the scroller
if (!scroller.active) {
scroller.start();
}
return;
} else if (scroller.active) {
scroller.stop();
}
// stationary track
g.reset(); g.reset();
g.setFont("Vector", size) g.setFont("Vector", size)
.setFontAlign(0, 1) // center bottom .setFontAlign(0, 1) // center bottom
.setColor(trackColor()); .setColor(trackColor());
clearTrack(); clearTrack();
g.drawString(info.track, 119, 109); g.drawString(info.track, 119, 109);
} }
/** /**
* Draw scrolling track title * Draw scrolling track title
*/ */
function drawScroller() { function drawScroller() {
g.reset(); g.reset();
g.setFont("Vector", 40); g.setFont("Vector", 40);
const w = g.stringWidth(info.track)+40; const w = g.stringWidth(info.track)+40;
scroller.offset = scroller.offset%w; offset = offset%w;
g.setFontAlign(-1, 1) // left bottom g.setFontAlign(-1, 1) // left bottom
.setColor(trackColor()); .setColor(trackColor());
clearTrack(); clearTrack();
g.drawString(info.track, -scroller.offset+40, 109) g.drawString(info.track, -offset+40, 109)
.drawString(info.track, -scroller.offset+40+w, 109); .drawString(info.track, -offset+40+w, 109);
} }
/** /**
* Draw track artist and album * Draw track artist and album
*/ */
function drawArtistAlbum() { function drawArtistAlbum() {
// we just use small enough fonts to make these always fit // we just use small enough fonts to make these always fit
// calculate stuff before clear+redraw // calculate stuff before clear+redraw
const artistColor = infoColor("artist"); const aCol = infoColor("artist");
const albumColor = infoColor("album"); const bCol = infoColor("album");
let artistSize = fitText(info.artist); let aSiz = fitText(info.artist);
if (artistSize>30) { if (aSiz>30) {
artistSize = 30; aSiz = 30;
} }
let albumSize = fitText(info.album); let bSiz = fitText(info.album);
if (albumSize>20) { if (bSiz>20) {
albumSize = 20; bSiz = 20;
} }
g.reset(); g.reset();
g.clearRect(0, 120, 240, 189); g.clearRect(0, 120, 240, 189);
let top = 124; let top = 124;
if (info.artist) { if (info.artist) {
g.setFont("Vector", artistSize) g.setFont("Vector", aSiz)
.setFontAlign(0, -1) // center top .setFontAlign(0, -1) // center top
.setColor(artistColor) .setColor(aCol)
.drawString(info.artist, 119, top); .drawString(info.artist, 119, top);
top += artistSize+4; // fit album neatly under artist top += aSiz+4; // fit album neatly under artist
} }
if (info.album) { if (info.album) {
g.setFont("Vector", albumSize) g.setFont("Vector", bSiz)
.setFontAlign(0, -1) // center top .setFontAlign(0, -1) // center top
.setColor(albumColor) .setColor(bCol)
.drawString(info.album, 119, top); .drawString(info.album, 119, top);
} }
} }
const icons = { /**
*
* @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) { pause: function(x, y, s) {
const w1 = s/3; const w1 = s/3;
g.drawRect(x, y, x+w1, y+s); g.drawRect(x, y, x+w1, y+s);
@ -380,26 +339,27 @@
], true); ], true);
g.drawRect(x+w2, y, x+s, y+s); g.drawRect(x+w2, y, x+s, y+s);
}, },
}; })[icon](x, y, s);
function controlColor(control) { }
if (volCmd && control===volCmd) { function controlColor(ctrl) {
if (vCmd && ctrl===vCmd) {
// volume button kept pressed down // volume button kept pressed down
return "#ff0000"; return "#ff0000";
} }
return (control in tCommand) ? "#ff0000" : "#008800"; return (ctrl in tCommand) ? "#ff0000" : "#008800";
} }
function drawControl(control, x, y) { function drawControl(ctrl, x, y) {
g.setColor(controlColor(control)); g.setColor(controlColor(ctrl));
const s = 20; const s = 20;
if (state!==controlState) { if (stat!==controlState) {
g.clearRect(x, y, x+s, y+s); g.clearRect(x, y, x+s, y+s);
} }
icons[control](x, y, s); drawIcon(ctrl, x, y, s);
} }
let controlState; let controlState;
function drawControls() { function drawControls() {
g.reset(); g.reset();
if (state==="play") { if (stat==="play") {
// left touch // left touch
drawControl("pause", 10, 190); drawControl("pause", 10, 190);
// right touch // right touch
@ -417,65 +377,64 @@
g.setFontAlign(1, 1); g.setFontAlign(1, 1);
g.setColor(controlColor("volumedown")); g.setColor(controlColor("volumedown"));
g.drawString("-", 240, 210); g.drawString("-", 240, 210);
controlState = state; controlState = stat;
} }
function drawMusic() { function drawMusic() {
drawNum(); drawNum();
drawTrack(); drawTrack();
drawArtistAlbum(); drawArtistAlbum();
} }
///////////////////////// ////////////////////////
// GB event handlers
/** ///////////////////////
/**
* Update music info * Update music info
* @param event * @param e
*/ */
function setInfo(event) { function musicInfo(e) {
info = event; info = e;
delete (info.t); delete (info.t);
scroller.offset = 0; offset = null;
if (Bangle.isLCDOn()) { if (Bangle.isLCDOn()) {
drawMusic(); drawMusic();
} }
} }
let tQuit; let tXit;
function updateState() { function musicState(e) {
stat = e.state;
// if paused for five minutes, load the clock // if paused for five minutes, load the clock
// (but timeout resets if we get new info, even while paused) // (but timeout resets if we get new info, even while paused)
if (tQuit) { if (tXit) {
clearTimeout(tQuit); clearTimeout(tXit);
} }
tQuit = null; tXit = null;
fadeOut.stop(); fade = null;
if (state!=="play" && autoClose) { delete info.track_color;
if (state==="stop") { // never actually happens with my phone :-( if (stat!=="play" && auto) {
if (stat==="stop") { // never actually happens with my phone :-(
load(); load();
} else { // also quit when paused for a long time } else { // also quit when paused for a long time
tQuit = setTimeout(load, TIMEOUT); tXit = setTimeout(load, TOUT);
fadeOut.start(); fade = Date.now();
fadeOut();
} }
} }
if (Bangle.isLCDOn()) { if (Bangle.isLCDOn()) {
drawControls(); drawControls();
} }
} }
// create tickers ////////////////////
const clock = new Clock(); // Events
const fadeOut = new Fader(); ////////////////////
const scroller = new Scroller();
//////////////////// let tLauncher;
// Events // we put starting of watches inside a function, so we can defer it until we
//////////////////// // asked the user about autoStart
function startLauncherWatch() {
let tLauncher;
// we put starting of watches inside a function, so we can defer it until we
// asked the user about autoStart
function startLauncherWatch() {
// long-press: launcher // long-press: launcher
// short-press: toggle play/pause // short-press: toggle play/pause
setWatch(function() { setWatch(function() {
@ -491,14 +450,14 @@
} }
togglePlay(); togglePlay();
}, BTN2, {repeat: true, edge: "falling"}); }, BTN2, {repeat: true, edge: "falling"});
} }
let tCommand = {}; let tCommand = {};
/** /**
* Send command and highlight corresponding control * Send command and highlight corresponding control
* @param command "play/pause/next/previous/volumeup/volumedown" * @param command "play/pause/next/previous/volumeup/volumedown"
*/ */
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 controlColor
if (command in tCommand) { if (command in tCommand) {
@ -509,54 +468,54 @@
drawControls(); drawControls();
}, 200); }, 200);
drawControls(); drawControls();
} }
// BTN1/3: volume control (with repeat after long-press) // BTN1/3: volume control (with repeat after long-press)
let tVol, volCmd; let tVol, vCmd;
function volUp() { function volUp() {
volStart("up"); volStart("up");
} }
function volDown() { function volDown() {
volStart("down"); volStart("down");
} }
function volStart(dir) { function volStart(dir) {
const command = "volume"+dir; const command = "volume"+dir;
stopVol(); stopVol();
sendCommand(command); sendCommand(command);
volCmd = command; vCmd = command;
tVol = setTimeout(repeatVol, 500); tVol = setTimeout(repeatVol, 500);
} }
function repeatVol() { function repeatVol() {
sendCommand(volCmd); sendCommand(vCmd);
tVol = setTimeout(repeatVol, 100); tVol = setTimeout(repeatVol, 100);
} }
function stopVol() { function stopVol() {
if (tVol) { if (tVol) {
clearTimeout(tVol); clearTimeout(tVol);
tVol = null; tVol = null;
} }
volCmd = null; vCmd = null;
drawControls(); drawControls();
} }
function startVolWatches() { function startVolWatches() {
setWatch(volUp, BTN1, {repeat: true, edge: "rising"}); setWatch(volUp, BTN1, {repeat: true, edge: "rising"});
setWatch(stopVol, BTN1, {repeat: true, edge: "falling"}); setWatch(stopVol, BTN1, {repeat: true, edge: "falling"});
setWatch(volDown, BTN3, {repeat: true, edge: "rising"}); setWatch(volDown, BTN3, {repeat: true, edge: "rising"});
setWatch(stopVol, BTN3, {repeat: true, edge: "falling"}); setWatch(stopVol, BTN3, {repeat: true, edge: "falling"});
} }
// touch/swipe: navigation // touch/swipe: navigation
function togglePlay() { function togglePlay() {
sendCommand(state==="play" ? "pause" : "play"); sendCommand(stat==="play" ? "pause" : "play");
} }
function startTouchWatches() { function startTouchWatches() {
Bangle.on("touch", function(side) { Bangle.on("touch", function(side) {
switch(side) { switch(side) {
case 1: case 1:
sendCommand(state==="play" ? "pause" : "previous"); sendCommand(stat==="play" ? "pause" : "previous");
break; break;
case 2: case 2:
sendCommand(state==="play" ? "next" : "play"); sendCommand(stat==="play" ? "next" : "play");
break; break;
case 3: case 3:
togglePlay(); togglePlay();
@ -565,18 +524,18 @@
Bangle.on("swipe", function(dir) { Bangle.on("swipe", function(dir) {
sendCommand(dir===1 ? "previous" : "next"); sendCommand(dir===1 ? "previous" : "next");
}); });
} }
///////////////////// /////////////////////
// Startup // Startup
///////////////////// /////////////////////
// check for saved music state (by widget) to load // check for saved music stat (by widget) to load
g.clear(); g.clear();
global.gbmusic_active = true; // we don't need our widget global.gbmusic_active = true; // we don't need our widget
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
delete (global.gbmusic_active); delete (global.gbmusic_active);
function startEmulator() { function startEmulator() {
if (typeof Bluetooth==="undefined") { // emulator! if (typeof Bluetooth==="undefined") { // emulator!
Bluetooth = { Bluetooth = {
println: (line) => {console.log("Bluetooth:", line);}, println: (line) => {console.log("Bluetooth:", line);},
@ -585,25 +544,24 @@
GB({"t": "musicinfo", "artist": "Some Artist Name", "album": "The Album Name", "track": "The Track Title Goes Here", "dur": 241, "c": 2, "n": 2}); GB({"t": "musicinfo", "artist": "Some Artist Name", "album": "The Album Name", "track": "The Track Title Goes Here", "dur": 241, "c": 2, "n": 2});
GB({"t": "musicstate", "state": "play", "position": 0, "shuffle": 1, "repeat": 1}); GB({"t": "musicstate", "state": "play", "position": 0, "shuffle": 1, "repeat": 1});
} }
} }
function startWatches() { function startWatches() {
startVolWatches(); startVolWatches();
startLauncherWatch(); startLauncherWatch();
startTouchWatches(); startTouchWatches();
} }
function start() { function start() {
// start listening for music updates // start listening for music updates
const _GB = global.GB; const _GB = global.GB;
global.GB = (event) => { global.GB = (event) => {
// we eat music events! // we eat music events!
switch(event.t) { switch(event.t) {
case "musicinfo": case "musicinfo":
setInfo(event); musicInfo(event);
break; break;
case "musicstate": case "musicstate":
state = event.state; musicState(event);
updateState();
break; break;
default: default:
// pass on other events // pass on other events
@ -614,26 +572,38 @@
} }
}; };
drawMusic(); drawMusic();
updateState(); drawControls();
startWatches(); startWatches();
clock.start(); tick();
startEmulator(); startEmulator();
Bangle.on("lcdPower", function(on) { Bangle.on("lcdPower", (on) => {
if (on) { if (on) {
tick();
drawMusic(); drawMusic();
drawControls(); drawControls();
fadeOut();
if (offset!==null) {
drawScroller();
scrollI = setInterval(scroll, 200);
}
} else {
if (scrollI) {
clearInterval(scrollI);
scrollI = null;
}
} }
}); });
} }
function init() {
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; info = saved.info;
state = saved.state; stat = saved.state;
delete (saved); delete (saved);
autoClose = true; auto = true;
start(); start();
} else { } else {
const s = require("Storage").readJSON("gbmusic.json", 1) || {}; const s = require("Storage").readJSON("gbmusic.json", 1) || {};
@ -653,3 +623,5 @@
} }
} }
} }
init();