197 lines
5.5 KiB
JavaScript
197 lines
5.5 KiB
JavaScript
// For info on interfacing with Gadgetbridge, see https://www.espruino.com/Gadgetbridge
|
|
const Debug = false; // Set to true to show debugging into
|
|
const Layout = require("Layout");
|
|
const PrimaryFont = "Vector:18";
|
|
|
|
const buttonPadding = 10;
|
|
|
|
const Command = {
|
|
next: "next",
|
|
pause: "pause",
|
|
play: "play",
|
|
previous: "previous",
|
|
volumeup: "volumeup",
|
|
volumedown: "volumedown",
|
|
};
|
|
|
|
const PlaybackState = {
|
|
paused: "pause",
|
|
playing: "play"
|
|
};
|
|
|
|
/**
|
|
* Format elapsed time in minutes and seconds.
|
|
* @param {*} time Elapsed time
|
|
* @returns Time string
|
|
*/
|
|
function formatTime(time) {
|
|
let minute = 0, second = 0;
|
|
if (time) {
|
|
minute = Math.floor(time / 60);
|
|
second = time % 60;
|
|
}
|
|
let minuteStr = minute.toString(), secondStr = second.toString();
|
|
|
|
if (minute < 10) minuteStr = `0${minute}`;
|
|
if (second < 10) secondStr = `0${second}`;
|
|
|
|
return `${minuteStr}:${secondStr}`;
|
|
}
|
|
|
|
/**
|
|
* Global playback state tracker.
|
|
* Follows the syntax {t:"musicstate", state:"play/pause",position,shuffle,repeat}
|
|
*/
|
|
let appState = { t: "musicstate", state: PlaybackState.paused, position: 0, shuffle: 0, repeat: 0 };
|
|
|
|
/**
|
|
* Define the screen layout.
|
|
*/
|
|
let layout = new Layout({
|
|
type: "v", c: [
|
|
{ type: "txt", id: "title", halign: -1, fillx: 0, col: g.fg, font: PrimaryFont, label: "Track N/A" },
|
|
{ type: "txt", id: "artist", halign: -1, fillx: 0, col: g.fg, font: PrimaryFont, label: "Artist N/A" },
|
|
{
|
|
type: "h", c: [
|
|
{ type: "txt", id: "elapsed", halign: -1, fillx: 1, col: g.fg, font: PrimaryFont, label: formatTime(0) },
|
|
{ type: "txt", id: "timeSplitter", halign: 0, fillx: 1, col: g.fg, font: PrimaryFont, label: " - " },
|
|
{ type: "txt", id: "duration", halign: 1, fillx: 1, col: g.fg, font: PrimaryFont, label: formatTime(0) }
|
|
]
|
|
},
|
|
{
|
|
type: "h", c: [
|
|
{ type: "btn", id: Command.previous, font: PrimaryFont, col: g.fg2, bgCol: g.bg2, pad: buttonPadding, label: "|<<", cb: l => sendCommand(Command.previous, true) },
|
|
{ type: "btn", id: "playpause", font: PrimaryFont, col: g.fg2, bgCol: g.bg2, pad: buttonPadding, label: " > ", cb: l => sendCommand(appState.state === PlaybackState.paused ? Command.play : Command.pause, true) },
|
|
{ type: "btn", id: Command.next, font: PrimaryFont, col: g.fg2, bgCol: g.bg2, pad: buttonPadding, label: ">>|", cb: l => sendCommand(Command.next, true) }
|
|
]
|
|
},
|
|
]
|
|
}, { lazy: true });
|
|
|
|
/// Set up the app
|
|
function initialize() {
|
|
// Detect whether we're using an emulator.
|
|
if (typeof Bluetooth === "undefined" || typeof Bluetooth.println === "undefined") { // emulator!
|
|
Bluetooth = {
|
|
println: (line) => { console.log("Bluetooth:", line); },
|
|
};
|
|
}
|
|
|
|
// Set up listeners for swiping
|
|
Bangle.on('swipe', function (directionLR, directionUD) {
|
|
switch (directionLR) {
|
|
case -1: // Left
|
|
sendCommand(Command.previous, true);
|
|
break;
|
|
case 1: // Right
|
|
sendCommand(Command.next, true);
|
|
break;
|
|
}
|
|
|
|
switch (directionUD) {
|
|
case -1: // Up
|
|
sendCommand(Command.volumeup, true);
|
|
break;
|
|
case 1: // Down
|
|
sendCommand(Command.volumedown, true);
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Eat music events (๑ᵔ⤙ᵔ๑)
|
|
Bangle.on("message", (type, message)=>{
|
|
if (type.includes("music") && !message.handled) {
|
|
processMusicEvent(message);
|
|
message.handled = true;
|
|
}
|
|
});
|
|
|
|
// Toggle play/pause if the button is pressed
|
|
setWatch(function() {
|
|
sendCommand(appState.state === PlaybackState.paused ? Command.play : Command.pause, true);
|
|
}, BTN, {edge: "falling", debounce: 50, repeat: true});
|
|
|
|
// Goad Gadgetbridge into sending us the current track info
|
|
sendCommand(Command.volumeup, false);
|
|
sendCommand(Command.volumedown, false);
|
|
|
|
// Render the screen
|
|
g.clear();
|
|
draw();
|
|
}
|
|
|
|
function draw() {
|
|
layout.update();
|
|
layout.render();
|
|
}
|
|
|
|
// Track how long the current song has been running.
|
|
let elapsedTimer;
|
|
let position = 0;
|
|
function updateTime() {
|
|
position++;
|
|
layout.elapsed.label = formatTime(position);
|
|
draw();
|
|
|
|
if (Debug) console.log("Tick");
|
|
}
|
|
|
|
function clearTimer() {
|
|
position = 0;
|
|
if (elapsedTimer) {
|
|
clearInterval(elapsedTimer);
|
|
elapsedTimer = undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a command via Bluetooth back to Gadgetbridge.
|
|
* @param {Command} command Which command to execute
|
|
* @param {true|false} buzz Whether to vibrate the motor
|
|
*/
|
|
function sendCommand(command, buzz) {
|
|
if (buzz) Bangle.buzz(50);
|
|
Bangle.musicControl(command);
|
|
}
|
|
|
|
function processMusicEvent(event) {
|
|
if (Debug) console.log("State: " + event.state);
|
|
if (Debug) console.log("Position: " + event.position);
|
|
|
|
position = event.position;
|
|
|
|
switch(event.state) {
|
|
case PlaybackState.playing:
|
|
if (Debug) console.log("Playing");
|
|
appState.state = event.state;
|
|
elapsedTimer = setInterval(updateTime, 1000);
|
|
layout.playpause.label = " || ";
|
|
break;
|
|
case PlaybackState.paused:
|
|
if (Debug) console.log("Paused");
|
|
appState.state = event.state;
|
|
clearTimer();
|
|
layout.playpause.label = " > ";
|
|
break;
|
|
case PlaybackState.previous:
|
|
case PlaybackState.next:
|
|
// Reset position
|
|
position = 0;
|
|
appState.state = PlaybackState.playing;
|
|
break;
|
|
}
|
|
|
|
// Re-render track info on song change
|
|
if (event.track != layout.title.label) {
|
|
position = 0;
|
|
layout.title.label = event ? event.track : "Track N/A";
|
|
layout.artist.label = event ? event.artist : "Artist N/A";
|
|
layout.duration.label = formatTime(event.dur);
|
|
}
|
|
|
|
draw();
|
|
if (Debug) layout.debug();
|
|
}
|
|
|
|
// Start the app and set up listeners
|
|
initialize(); |