From 8018c99b4e3d20d657eed57f328830c4ee8e14fe Mon Sep 17 00:00:00 2001 From: Andre Date: Wed, 4 Jun 2025 21:17:02 -0400 Subject: [PATCH] simplemusic: Initial commit! --- apps/simplemusic/ChangeLog | 1 + apps/simplemusic/README.md | 27 +++++ apps/simplemusic/app-icon.js | 1 + apps/simplemusic/app.js | 203 +++++++++++++++++++++++++++++++++ apps/simplemusic/app.png | Bin 0 -> 2289 bytes apps/simplemusic/metadata.json | 17 +++ 6 files changed, 249 insertions(+) create mode 100644 apps/simplemusic/ChangeLog create mode 100644 apps/simplemusic/README.md create mode 100644 apps/simplemusic/app-icon.js create mode 100644 apps/simplemusic/app.js create mode 100644 apps/simplemusic/app.png create mode 100644 apps/simplemusic/metadata.json diff --git a/apps/simplemusic/ChangeLog b/apps/simplemusic/ChangeLog new file mode 100644 index 000000000..e342e49ed --- /dev/null +++ b/apps/simplemusic/ChangeLog @@ -0,0 +1 @@ +0.01: First release! diff --git a/apps/simplemusic/README.md b/apps/simplemusic/README.md new file mode 100644 index 000000000..9d6859474 --- /dev/null +++ b/apps/simplemusic/README.md @@ -0,0 +1,27 @@ +# Simple Music Controls + +A small app for viewing and controlling music via Gadgetbridge on Android. This is a remix of [rigrig's Gadgetbridge Music App](https://banglejs.com/apps/?id=gbmusic&readme), only it adds on-screen buttons and doesn't run in the background. + +Requires [Gadgetbridge](https://www.espruino.com/Gadgetbridge). + +## Usage + +1. Connect your Bangle.js to Gadgetbridge. +2. Open a music player on your Android phone. +3. Open this app on your Bangle.js. + +## Features + +- Shows the current song title and album +- Provides buttons for changing or pausing the current track. +- Supports swiping + +## Controls + +Use the on-screen buttons to go back to the previous track, play/pause the current track, or skip to the next track. + +Swipe up/down to increase/decrease the volume, or swip left/right to navigate to the previous/next song. + +## Creator + +8bitbuddhist (https://github.com/8bitbuddhist) diff --git a/apps/simplemusic/app-icon.js b/apps/simplemusic/app-icon.js new file mode 100644 index 000000000..f0e006d45 --- /dev/null +++ b/apps/simplemusic/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///vt9AYO7gQdRggLKgoLLoALWqALKqgLKqoLVh8NHhMPgY8Jh8ABZMHgEUQZSRJO4KRJBYMBQZQLKqkBMBB3C4ALTh/1oALJQYILJgHQR5SDJBYKDJBYKDJBYJ3JBYR3IBYMDBY0K0ALBgALFgWq0tPAoJrFhWqqtXBY8qBYKaBgHwBYmq1WVBYUwBY2pqo5BBYkC0tWEgILHquptQLCaY2qrWVBZFay2VqALHqumz4LGgIvB0ufEYwLCsoLJ0tVr6xHRoP//q8Hiv///8BY8XBYKYFGAYLBcBAkBqgHF")) \ No newline at end of file diff --git a/apps/simplemusic/app.js b/apps/simplemusic/app.js new file mode 100644 index 000000000..6b35ea9c8 --- /dev/null +++ b/apps/simplemusic/app.js @@ -0,0 +1,203 @@ +// 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; + } + }); + + // Goad Gadgetbridge into sending us the current track info + sendCommand(Command.volumeup, false); + sendCommand(Command.volumedown, false); +} + +function draw() { + layout.render(); + Bangle.drawWidgets(); +} + +/** + * 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); + Bluetooth.println(JSON.stringify({ t: "music", n: command })); + + switch (command) { + // If this is a play or pause command, display the track and artist + case Command.play: + updateState(PlaybackState.playing); + break; + case Command.pause: + updateState(PlaybackState.paused); + break; + // Reset the duration clock for new tracks + case Command.next: + case Command.previous: + layout.elapsed.label = formatTime(0); + break; + } +} + +/// Track how long the current song has been running. +let elapsedTimer; +let position = 0; +function updateTime() { + position++; + layout.elapsed.label = formatTime(position); + layout.render(); + + if (Debug) console.log("Tick"); +} + +/** + * Get info about the current playing song. + * @param {Object} info - Gadgetbridge musicinfo event + */ +function showTrackInfo(info) { + layout.title.label = info ? info.track : "Track N/A"; + layout.artist.label = info ? info.artist : "Artist N/A"; + layout.duration.label = info ? formatTime(info.dur) : formatTime(0); + draw(); + if (Debug) layout.debug(); +} + +/** + * Updates the current state of the app. + * Called when Gadgetbridge updates (see boot.js) + * @param {*} state + */ +function updateState(state) { + appState.state = state; + position = state.position; + + // Alternate between play and pause symbols + if (state === PlaybackState.playing) { + elapsedTimer = setInterval(updateTime, 1000); + layout.playpause.label = " || "; + } + else if (state === PlaybackState.paused) { + if (elapsedTimer) clearInterval(elapsedTimer); + layout.playpause.label = " > "; + } +} + +/** + * Listen for Gadgetbridge events + */ +setTimeout( + () => { + globalThis.GB = (_GB => e => { + switch (e.t) { + case "musicinfo": + return showTrackInfo(e); + case "musicstate": + return updateState(e); + default: + // pass on other events + if (_GB) setTimeout(_GB, 0, e); + } + })(globalThis.GB); + }, 1); + + +// Start the app +initialize(); + +// Render the screen +g.clear(); +draw(); \ No newline at end of file diff --git a/apps/simplemusic/app.png b/apps/simplemusic/app.png new file mode 100644 index 0000000000000000000000000000000000000000..40bd83ade54b2506e6c4aea8de237bcdf28dd3a7 GIT binary patch literal 2289 zcmVhC3 zTgyPLk5ROR+M%^N;~#BD+Zl#Zt5Q4D7K<&QGs*xm6d78mzJmXE}{xzPYn zaKnPfPQ9Kg)swj~4k8KL``3S(`2mpe3 z%`@sGe0&Lfj`Yqmz`=355LtDjL1B_5IdVfB`1QC@cY^_a$q&vZD6y(@wVK|XmUvUcrT(R==5Uv>*GB5f$y3Jz_rE6c@6Z={Ymr)=MbG`hy zG65iD($87qG25_g`r;eMlX6Z1IMAfhl0we@X1!N_@OwoC*WT-SeNr@utF1iElp@osnFGDLNe{JEP!Rh+TR_B-*@#f z6!DQkg~~8qD!h%tECv%ipt<(P3xx@j3-*VOo*rK7kV|ri*B_I@96mffE`!7Q~lwY4r9ZmK5M~yA1mJmjdtSK z@w@@RTIU({)e9l>Tz8+fX37*v1(WvX6#)2fda|v@`Xjp1c@1^mO1Nb7EJUhLdH1`y@{9YuL<|p zcfTiz$Vc*dqpj(gcZirG;}L@Dp$0G$QfyYzi|?iq(JXNM3@x%T0P4cCO79?{haF)a^d;JwFiW?M2!MYyrN_v!xDh8*rJF;Co#hn*V9MS5XosOUVZ-?z z0}|*sDFU$!0!he%^PWhBJ zRAsJq#(oy@x@s9#&i6xAu>fx!Nki-9p~ZC33@@e`1+6hrQv`4OP#=>GN$}hNKzr5! zU`4r&Q6-I$XEb6xlC)v#;S{`mEDh&ECiqbU-@VfZV}m?wKc04JHZ~~0tw9me3Q`CG zU>1mJIYAXb9H!P`zR2aQ15gY)kitn{?QX5l*bqxO(A{srrh`e?cUo~P(z1FVw1rK$ z&}+IInW_?j?RP=b**=(p#G@>1dj0iaM>WOnWF0_J0e3K|3}qmNlVNqA;|M&uC;&gb zzX+Dq`Cvz5no$j^%SD$~&D}#Z8spp*iwAxRcM|{a}EP6b86|#WU*o-vN8GpYDf0lhl^i9fv&@9;xHYsilnf~qOh+m zv8gLMVHue-Nsc}JjleS##~N=-{YH3Es3$A@|I!q?wmq|@|Ka9K#;>|#)|6z%ap}ZX zTJ%b8sbpuMl|g}iz1DN^$tC_@E}6aM