diff --git a/apps.json b/apps.json index e5e9f8f027..80ce2b9e57 100644 --- a/apps.json +++ b/apps.json @@ -5062,5 +5062,70 @@ {"name":"ltherm.app.js","url":"app.js"}, {"name":"ltherm.img","url":"icon.js","evaluate":true} ] + }, + { + "id": "omgbc", + "name": "Oh My Gadget Bridge Core", + "icon": "omgbc.png", + "version": "0.02", + "description": "A custom implementation of gadget bridge, expose an API for other app", + "type": "bootloader", + "tags": "tools, system", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"omgbc.boot.js","url":"boot.js"}, + {"name":"omgbc.img","url":"icon.js","evaluate":true}, + {"name":"omgbc.settings.js","url":"settings.js"} + ] + }, + { + "id": "omgbhbf", + "name": "Oh My Gadget Bridge (Health, Battery, Find)", + "version": "0.02", + "description": "Implement Health, Battery, Find", + "icon": "omgbhbf.png", + "type": "bootloader", + "tags": "tools, system", + "supports": ["BANGLEJS"], + "dependencies": {"omgbc":"app"}, + "readme": "README.md", + "storage": [ + {"name":"omgbhbf.boot.js","url":"boot.js"}, + {"name":"omgbhbf.img","url":"icon.js","evaluate":true}, + {"name":"omgbhbf.settings.js","url":"settings.js"} + ] + }, + { + "id": "simplyclock", + "name": "Simply Clock", + "version": "1.01", + "description": "Just simply clock", + "icon": "icon.png", + "type": "clock", + "tags": "clock", + "allow_emulator": true, + "supports": ["BANGLEJS"], + "dependencies": {"omgbc":"app"}, + "storage": [ + {"name":"simplyclock.app.js","url":"app.js"}, + {"name":"simplyclock.img","url":"icon.js","evaluate":true} + ] + }, + { + "id": "swipeclock", + "name": "Swipe Clock", + "version": "1.00", + "description": "Swipe clock", + "icon": "icon.png", + "type": "clock", + "tags": "clock", + "allow_emulator": true, + "supports": ["BANGLEJS"], + "dependencies": {"omgbc":"app"}, + "storage": [ + {"name":"swipeclock.app.js","url":"app.js"}, + {"name":"swipeclock.img","url":"icon.js","evaluate":true} + ] } ] diff --git a/apps/omgbc/ChangeLog b/apps/omgbc/ChangeLog new file mode 100644 index 0000000000..5560f00bce --- /dev/null +++ b/apps/omgbc/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/omgbc/README.md b/apps/omgbc/README.md new file mode 100644 index 0000000000..28682caf21 --- /dev/null +++ b/apps/omgbc/README.md @@ -0,0 +1,132 @@ +# Oh My Gadget Bridge Core + +Manage event of gadget bridge (alternative to Android bangle app). +Dispatch many event in bangle and another app can listen here events + +## How to install in my app +```js +// apps.json +{ + "id": "myapp", + "name": "My app", + "shortName": "app", + "version": "0.01", + "description": "bip bip", + "dependencies": { "omgbc": "app" }, +} + +// myapp/app.js +global.GadgetBridge.onEvent("notify", (event) => { + // Your code +}); +``` + +## Features + +### Api describe +```ts +global.GadgetBridge: { + onEvent(eventName: EventName, callback: Callback, option?: Option) + removeEventListener(eventName: EventName, callback: Callback) + sendEvent(message: string) + musicControl(command: string) // play/pause/next/previous/volumeup/volumedown + messageResponse(msg: Object, opened: boolean) // opened true or false, msg is the message to which you wish to reply + callResponse(call: Object, accepted: boolean) // accepted true or false, call is the call to which you wish to reply +} + +type EventName = String; // See list of events +interface Callback { + (event: any, stopPropagation: () => void): void +} +interface Option { + layer: number // default Infinity +} +``` + +List of events: +- notify +- find +- musicstate +- musicinfo +- call +- connect +- disconnect + +## Exemples + +For listen a gadgetbridge event : +```js +// file boot.js +function boot() { + global.GadgetBridge.onEvent("notify", (event) => { + // Your code + }); +} +setTimeout(boot, 1000); // Necessary to make sure that all are well loaded +``` + +For stop listening + +```js +// file boot.js +function boot() { + var onNotify = (event) => { + // Your code + }; + global.GadgetBridge.onEvent("notify", onNotify); + global.GadgetBridge.removeEventListener("notify", onNotify); +} +setTimeout(boot, 1000); +``` + +### To Know + +Event manager sort callback by layer. If you want start your callback behind another you must add lower layer. + +For exemple: +```js +// file boot.js +function boot() { + global.GadgetBridge.onEvent("notify", (event) => { + console.log("from boot.js"); + }, { layer: 2 }); +} +setTimeout(boot, 1000); + +// file another.boot.js +function boot() { + global.GadgetBridge.onEvent("notify", (event) => { + console.log("from another.boot.js"); + }, { layer: 1 }); +} +setTimeout(boot, 1000); + +// logs +// -> "from another.boot.js" +// -> "from boot.js" +``` + +Event manager also proposes to stop the propagation (callbacks with a higher layer will not be called). This is useful if you want to disable another behavior + +For exemple: +```js +// file boot.js +function boot() { + global.GadgetBridge.onEvent("notify", (event) => { + console.log("It's never call"); + }, { layer: 2 }); +} +setTimeout(boot, 1000); + +// file another.boot.js +function boot() { + global.GadgetBridge.onEvent("notify", (event, stopPropagation) => { + console.log("from another.boot.js"); + stopPropagation(); + }, { layer: 1 }); +} +setTimeout(boot, 1000); + +// logs +// -> "from another.boot.js" +``` \ No newline at end of file diff --git a/apps/omgbc/boot.js b/apps/omgbc/boot.js new file mode 100644 index 0000000000..ff5fa2b226 --- /dev/null +++ b/apps/omgbc/boot.js @@ -0,0 +1,267 @@ +(function() { + var onCallbacks = {}; + + function dispatchEvent(eventName, data) { + if (!onCallbacks[eventName]) { + console.log(eventName + " not found in callbacks"); + return; + } + var stop = false; + const stopCallback = () => stop = true; + + onCallbacks[eventName].forEach(eventCallback => { + if (stop) return; + console.log(stop); + eventCallback.callback(data, stopCallback); + }); + } + + // Disposed public API + global.GadgetBridge = { + send: (message) => { + Bluetooth.println(""); + Bluetooth.println(JSON.stringify(message)); + }, + onEvent: (eventName, callback, option = {}) => { + if (!option) { + option = {}; + } + if (!onCallbacks[eventName]) { + onCallbacks[eventName] = []; + } + if (!option.layer) option.layer = Infinity; + + onCallbacks[eventName].push({callback: callback, layer: option.layer}); + onCallbacks[eventName].sort((a, b) => a.layer - b.layer); + }, + removeEventListener: (eventName, callback) => { + if (!onCallbacks[eventName]) return; + + const index = onCallbacks[eventName].findIndex(event => event.callback === callback); + if (index == -1) return; + onCallbacks[eventName].splice(index, 1); + }, + musicControl: cmd => { + // play/pause/next/previous/volumeup/volumedown + global.GadgetBridge.send({ t: "music", n: cmd }); + }, + messageResponse: (msg, response) => { + if (!isFinite(msg.id)) return; + global.GadgetBridge.send({ t: "notify", n: response ? "OPEN" : "DISMISS", id: msg.id }); + }, + + findPhone: (search) => { + global.GadgetBridge.send({ t: "findPhone", n: search}) + }, + // Call response + callResponse: (msg, response) => { + if (msg.id != "call") return; + global.GadgetBridge.send({ t: "call", n:response ? "ACCEPT" : "REJECT" }); + }, + "__private__" : { // Just for debbuging in WebIde + dispatchEvent: dispatchEvent + }, + isConnected: false, + }; + + NRF.on("connect", () => { + GadgetBridge.isConnected = true; + dispatchEvent("connect", {}) + }); + NRF.on("disconnect", () => { + GadgetBridge.isConnected = false; + dispatchEvent("disconnect", {}) + }); + + var HANDLERS = { + // {t:"notify",id:int, src,title,subject,body,sender,tel:string} add + "notify" : function(event) { + dispatchEvent("notify", Object.assign(event, { t: "add", positive: true, negative: true })); + }, + // {t:"notify~",id:int, title:string} // modified + "notify~" : function(event) { + event.t = "modify"; + dispatchEvent("notify", event); + }, + // {t:"notify-",id:int} // remove + "notify-" : function(event) { + event.t = "remove"; + dispatchEvent("notify", event); + }, + // {t:"find", n:bool} // find my phone + "find" : (event) => dispatchEvent("find", event), + // {t:"musicstate", state:"play/pause",position,shuffle,repeat} + "musicstate" : function(event) { + dispatchEvent("musicstate", { t: "modify", id: "music", title: "Music", state: event.state }); + }, + // {t:"musicinfo", artist,album,track,dur,c(track count),n(track num} + "musicinfo" : function(event) { + dispatchEvent("musicinfo", Object.assign(event, { t: "modify", id: "music", title: "Music" })); + }, + // {"t":"call","cmd":"incoming/end","name":"Bob","number":"12421312"}) + "call" : function(event) { + dispatchEvent("call", Object.assign(event, + { + t: event.cmd == "incoming", + id:"call", + src:"Phone", + positive:true, + negative:true, + title:event.name || "Call", + body:"Incoming call\n"+event.number + } + )); + }, + }; + + var _GB = global.GB; + global.GB = (event) => { + // feed a copy to other handlers if there were any + if (_GB) setTimeout(_GB,0,Object.assign({},event)); + + var handler = HANDLERS[event.t]; + if (handler) handler(event); + else dispatchEvent(event.t, event); + }; +})(); + + +/** + * + * function drawMessage(message) { + g.clearRect(0,g.getHeight() / 2 - g.getHeight() / 3, g.getWidth(), g.getHeight() / 2 + g.getHeight() / 4); + // Draw Message rect + g.setColor(1,1,1); + g.fillRect( + 0, + g.getHeight() / 2 - g.getHeight() / 3, + g.getWidth(), + g.getHeight() / 2 + g.getHeight() / 4 + ); + + drawHeader(message); + drawMessageTxt(message); + drawActionsButtons(); +} + +function drawHeader(message) { + g.setFont("6x8", 2.5).setFontAlign(-1,-1); + g.setColor(0,0,0); + + g.drawString( + message.sender.trim(), + 10, + g.getHeight() / 2 - g.getHeight() / 3 + 10 + ); + if (message.src) { + g.setFont("6x8", 1).setFontAlign(-1, 0); + g.drawString( + message.src.trim(), + g.getWidth() - g.stringWidth(message.src.trim()) - 5, + g.getHeight() / 2 - g.getHeight() / 3 + 7 + ); + } +} + +function drawMessageTxt(message) { + const width = g.getWidth() - 10;// Available width + const height = (g.getHeight() / 2 + g.getHeight() / 4) - (g.getHeight() / 2 - g.getHeight() / 3) - 50;// Available height + const lineHeight = 6 * 2; + const nbLine = Math.ceil(height / (lineHeight+4)); + g.setFont("6x8", 2).setFontAlign(-1,-1); + + const words = message.body.replace("\n", "").split(" "); + var currentWords = 0; + for (var i = 0; i < nbLine; i++) { + if (currentWords >= words.length) continue; + var line = ""; + for(let x = currentWords; x < words.length; x++) { + var word = words[x]; + if (g.stringWidth(line + " " + word) > width) { + break; + } else { + line += " " + word; + currentWords = x; + } + } + currentWords++; + g.drawString( + line, + 5, + g.getHeight() / 2 - g.getHeight() / 3 + 40 + ((lineHeight + 4) * i) + ); + } +} + +function drawActionsButtons() { + g.setFont("6x8", 2.5).setFontAlign(0,0); + + // Draw Button actions + g.setColor(0.278, 0.886, 0.235); + g.fillCircle( + g.getWidth() / 2 - g.getWidth() / 3.8, + g.getHeight() / 2 + g.getHeight() / 4 + 20, + 35 + ); + g.setColor(0,0,0); + g.drawString( + "Seen", + g.getWidth() / 2 - g.getWidth() / 3.8, + g.getHeight() / 2 + g.getHeight() / 4 +20 + ); + + g.setColor(0.886, 0.235, 0.466); + g.fillCircle( + g.getWidth() / 2 + g.getWidth() / 3.8, + g.getHeight() / 2 + g.getHeight() / 4 + 20, + 35 + ); + + g.setColor(0,0,0); + g.drawString( + "Not\nseen", + g.getWidth() / 2 + g.getWidth() / 3.8, + g.getHeight() / 2 + g.getHeight() / 4 +20 + ); +} + + + +function onMessage(message) { + const oldMode = Bangle.getLCDMode(); + Bangle.setLCDMode("direct"); + Bangle.setLCDPower(1); + //g.setLCDOffset(g.getHeight()); + drawMessage(message); + Bangle.buzz(); + setTimeout( _ => Bangle.buzz(), 1000); + let isClose = false; + + function close() { + isClose = true; + g.clearRect(0,g.getHeight() / 2 - g.getHeight() / 3, g.getWidth(), g.getHeight()); + Bangle.setLCDMode(oldMode); + //g.setLCDOffset(0); + } + + setWatch(() => { + if (!global.GadgetBridge || isClose) return; + global.GadgetBridge.messageResponse(message, true); + close(); + }, BTN4, {edge:"both", repeat: false}); + + setWatch(() => { + if (!global.GadgetBridge || isClose) return; + close(); + global.GadgetBridge.messageResponse(message, false); + }, BTN5, {edge:"both", repeat: false}); +} + +onMessage({ + body: "slkdfjsljdf, sfsdfsdf", + sender: "Victor", + id: 23 +}); + +global.GadgetBridge.onEvent("notify", (message) => onMessage(message)); + */ \ No newline at end of file diff --git a/apps/omgbc/icon.js b/apps/omgbc/icon.js new file mode 100644 index 0000000000..a5eb7845da --- /dev/null +++ b/apps/omgbc/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob(atob("MDAB///////////////////////////////////////////////////////////////////////////////////x///////x///////hP//////iH/////+eI/////8CI/////4BQn////4BBD////xihH////wdCH////wBEP////wGkH////wACX////4ABn////4AAP////8AAP////+AAP/////AAf/////gA//////gB//////wD//////wD//////////////////////////////////////wD//////wD//////wD///////////////////////////////////////////////////////////////////////////"))); \ No newline at end of file diff --git a/apps/omgbc/omgbc.png b/apps/omgbc/omgbc.png new file mode 100644 index 0000000000..6618c1d295 Binary files /dev/null and b/apps/omgbc/omgbc.png differ diff --git a/apps/omgbc/settings.js b/apps/omgbc/settings.js new file mode 100644 index 0000000000..769862233f --- /dev/null +++ b/apps/omgbc/settings.js @@ -0,0 +1,7 @@ +(function(back) { + E.showMenu({ + "" : { "title" : "Custom GadgetBridge" }, + "< Back" : back, + "Connected" : { value : NRF.getSecurityStatus().connected? "Yes" : "No"}, + }); +}) diff --git a/apps/omgbhbf/ChangeLog b/apps/omgbhbf/ChangeLog new file mode 100644 index 0000000000..5560f00bce --- /dev/null +++ b/apps/omgbhbf/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/omgbhbf/README.md b/apps/omgbhbf/README.md new file mode 100644 index 0000000000..3a10f68953 --- /dev/null +++ b/apps/omgbhbf/README.md @@ -0,0 +1,7 @@ +# Oh My Gadget Bridge (Health, Battery, Find) + +### Default Features + - Send Health tracking to GadgetBridge + - Send Battery status to GadgetBridge + - Vibration on find event (It can be disabled, see stop propagations part) + - Send Find my phone event to GadgetBridge diff --git a/apps/omgbhbf/boot.js b/apps/omgbhbf/boot.js new file mode 100644 index 0000000000..8f7a05e2f2 --- /dev/null +++ b/apps/omgbhbf/boot.js @@ -0,0 +1,27 @@ +(function() { + function boot() { + // Battery monitor + function sendBattery() { + global.GadgetBridge.send({ t: "status", bat: E.getBattery(), volt: E.getAnalogVRef(), chg: Bangle.isCharging() }); + } + setInterval(sendBattery, 10*60*1000); + + global.GadgetBridge.onEvent("connect", () => setTimeout(sendBattery, 2000), { layer: 0 }); + + // Health tracking + Bangle.on('health', health => { + global.GadgetBridge.send({ t: "act", stp: health.steps, hrm: health.bpm }); + }); + + // Find event + global.GadgetBridge.onEvent("find", (event) => { + if (Bangle.findDeviceInterval) { + clearInterval(Bangle.findDeviceInterval); + delete Bangle.findDeviceInterval; + } + if (event.n) // Ignore quiet mode: we always want to find our watch + Bangle.findDeviceInterval = setInterval( _ => Bangle.buzz(), 1000); + }); + } + setTimeout(boot, 0); +})(); diff --git a/apps/omgbhbf/icon.js b/apps/omgbhbf/icon.js new file mode 100644 index 0000000000..a5eb7845da --- /dev/null +++ b/apps/omgbhbf/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob(atob("MDAB///////////////////////////////////////////////////////////////////////////////////x///////x///////hP//////iH/////+eI/////8CI/////4BQn////4BBD////xihH////wdCH////wBEP////wGkH////wACX////4ABn////4AAP////8AAP////+AAP/////AAf/////gA//////gB//////wD//////wD//////////////////////////////////////wD//////wD//////wD///////////////////////////////////////////////////////////////////////////"))); \ No newline at end of file diff --git a/apps/omgbhbf/omgbhbf.png b/apps/omgbhbf/omgbhbf.png new file mode 100644 index 0000000000..6618c1d295 Binary files /dev/null and b/apps/omgbhbf/omgbhbf.png differ diff --git a/apps/omgbhbf/settings.js b/apps/omgbhbf/settings.js new file mode 100644 index 0000000000..61995173d2 --- /dev/null +++ b/apps/omgbhbf/settings.js @@ -0,0 +1,9 @@ +(function(back) { + E.showMenu({ + "" : { "title" : "Find My Phone" }, + "< Back" : back, + "Phone Connected" : { value: NRF.getSecurityStatus().connected? "Yes" : "No"}, + "On" : _=> global.GadgetBridge.findPhone(true), + "Off" : _=> global.GadgetBridge.findPhone(false), + }); +}) diff --git a/apps/simplyclock/ChangeLog b/apps/simplyclock/ChangeLog new file mode 100644 index 0000000000..7cbedbecb4 --- /dev/null +++ b/apps/simplyclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: Init App +0.02: Fix clear hours +1.01: Add music management \ No newline at end of file diff --git a/apps/simplyclock/app.js b/apps/simplyclock/app.js new file mode 100644 index 0000000000..8e5291aa63 --- /dev/null +++ b/apps/simplyclock/app.js @@ -0,0 +1,205 @@ +var modeUi = "watch"; +var WIDTH = g.getWidth(); +var SIZE = Math.floor(WIDTH / (7*6)); +var watchUiState = { + nightMode: false, + forceDraw: true, + width: WIDTH, + size: SIZE, + x: (WIDTH / 2) - SIZE * 6, + y: (g.getHeight()/2) - SIZE *7 +} +var stat = "play"; +var doubleTouch = false; +var lastAction; +var actionTimeout; +var drawInterval; + +function main() { + require("Font7x11Numeric7Seg").add(Graphics); + + // Only update when display turns on + if (process.env.BOARD!="SMAQ3") // hack for Q3 which is always-on + + g.clear(); + drawLoop(1000); + Bangle.setUI("clock"); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + listenEvent(); +} + + +function drawWatch() { + var d = new Date(); + var x = watchUiState.x; + var y = watchUiState.y; + var size = watchUiState.size; + var width = watchUiState.width; + + g.reset(); + if (watchUiState.nightMode) { + g.setColor("#ff1100"); + } + + g.setFont("7x11Numeric7Seg",size).setFontAlign(1,-1); + if ((d.getMinutes() == 0 && d.getSeconds() == 0) || watchUiState.forceDraw) { + g.clearRect(x - size * 14, y, x - size , y + size * 11); + g.drawString(d.getHours(), x, y); + } + + g.setFontAlign(-1,-1); + + if (d.getSeconds() % 2 == 0) g.drawString(":", x,y); + else g.clearRect(x, y, x + size *4, y + size * 12); + + if (d.getSeconds() == 0|| watchUiState.forceDraw) { + g.clearRect(x + size *4 , y, x + size * 17 , y + size * 11); + g.drawString(("0"+d.getMinutes()).substr(-2),x+size*4,y); + } + + // draw seconds + g.setFont("7x11Numeric7Seg",size/2); + g.clearRect(x+size*18,y + size*7,x+size*18 + size *8 ,y + size*12 ); + g.drawString(("0"+d.getSeconds()).substr(-2),x+size*18,y + size*7); + + if ((d.getHours() == 0 && d.getMinutes() == 0) || watchUiState.forceDraw) { + // date + var date = d.toString().split(" "); + g.setFont("6x8", size/1.5).setFontAlign(0,-1); + g.clearRect(width/4, y + size * 16, width/ 1.33 , y + size * 20); + g.drawString(date[2] + " " + date[1], width / 2 , y + size * 16); + } + + + if ((d.getMinutes() % 2 == 0 && d.getSeconds() == 0) || watchUiState.forceDraw) { + var bat = E.getBattery(); + var color = bat <= 20 || watchUiState.nightMode ? "#ff1100" : "#ffffff"; + + if (Bangle.isCharging()) { + color ="#ffc413"; + } + + g.setColor(color); + g.setFont("6x8", size/1.5).setFontAlign(0,-1); + g.clearRect(0, y - size * 10, width , y - size * 6); + g.drawString(bat + "%", width / 8 + 10, y - size * 10); + } + + g.clearRect(width * 0.30, y - size * 10 , width ,y - size * 2); + if (lastAction && !lastAction.startsWith("volume")) { + g.setFont("6x8", size/1.5).setFontAlign(0,-1); + g.drawString(lastAction, width * 0.68, y - size * 10); + } + watchUiState.forceDraw = false; +} + +function drawVolume() { + g.reset(); + g.setFont("6x8", SIZE / 1.5).setFontAlign(-1,-1); + + g.drawString("Volume", WIDTH / 4, watchUiState.y - SIZE * 5); + + g.setFont("6x8", SIZE * 1.5).setFontAlign(0,-1); + + g.drawString("-", WIDTH / 4, watchUiState.y + SIZE * 3); + g.drawString("+", WIDTH / 2 + WIDTH /4, watchUiState.y + SIZE * 3); + + g.clearRect(WIDTH/6, watchUiState.y + SIZE * 16, WIDTH , watchUiState.y + SIZE * 21); + if (lastAction) { + if (lastAction == "volumeup") { + g.setFont("6x8", SIZE / 1.5).setFontAlign(-1,-1); + g.drawString("Volume up", WIDTH / 5, watchUiState.y + SIZE * 16); + } + else if (lastAction == "volumedown") { + g.setFont("6x8", SIZE / 1.5).setFontAlign(-1,-1); + g.drawString("Volume down", WIDTH / 6, watchUiState.y + SIZE * 16); + } + } +} + +function draw() { + if (modeUi == "volume") { + drawVolume(); + } else { + drawWatch(); + } +} + + +function listenEvent() { + setWatch(() => { + if (modeUi == "volume") { return; } + + watchUiState.nightMode = !watchUiState.nightMode; + if (watchUiState.nightMode) Bangle.setLCDBrightness(0.2); + else Bangle.setLCDBrightness(1); + + watchUiState.forceDraw = true; + drawWatch(); // needed for update color + }, BTN1, {edge:"rising", debounce:50, repeat:true}); + + setWatch(() => { + modeUi = modeUi == "watch" ? "volume" : "watch"; + watchUiState.forceDraw = true; + g.clearRect(0, watchUiState.y - SIZE * 10, WIDTH, g.getHeight()); + drawLoop(1000); + }, BTN3, {edge:"rising", debounce: 50, repeat:true}); + + setWatch(() => { + if (modeUi != "volume") { + if (BTN5.read()) { + togglePlay(); + setAction(stat); + doubleTouch = true; + setTimeout(() => doubleTouch = false, 200); + } else { + setAction("previous"); + } + } else { + setAction("volumedown"); + } + }, BTN4, {edge:"rising", debounce: 50, repeat:true}); + + setWatch(() => { + if (modeUi != "volume") { + if (doubleTouch) return; + setAction("next"); + + } else { + setAction("volumeup"); + } + }, BTN5, {edge:"rising", debounce: 50, repeat:true}); + + Bangle.on('lcdPower', function(on) { + if (drawInterval) clearInterval(drawInterval); + g.clearRect(0, watchUiState.y - SIZE * 10, WIDTH, g.getHeight()); + if (on) { + modeUi = "watch"; + watchUiState.forceDraw = true; + drawLoop(1000); + } + }); +} + +function togglePlay() { + stat = ( stat === "play" ? "pause" : "play"); +} + +function setAction(action) { + if (actionTimeout) clearTimeout(actionTimeout); + actionTimeout = setTimeout(() => lastAction = null, 1000); + lastAction = action; + + global.GadgetBridge.musicControl(action); + draw(); +} + +function drawLoop(time) { + if (drawInterval) clearInterval(drawInterval); + drawInterval = setInterval(draw, time); + draw(); +} + +main(); \ No newline at end of file diff --git a/apps/simplyclock/icon.js b/apps/simplyclock/icon.js new file mode 100644 index 0000000000..d5d9aaf682 --- /dev/null +++ b/apps/simplyclock/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mUygP/AC5BlH4MAn/gAwN/4EP/AFBsEMhkBwEAjEDgYJBgEGgHA4EYDwOAmEwBIIYyj/wgf+AoMH/kA/4eBJXwYLVxgAjh//AC3w")) diff --git a/apps/simplyclock/icon.png b/apps/simplyclock/icon.png new file mode 100644 index 0000000000..cb08aec5e1 Binary files /dev/null and b/apps/simplyclock/icon.png differ diff --git a/apps/swipeclock/ChangeLog b/apps/swipeclock/ChangeLog new file mode 100644 index 0000000000..59e6934ed3 --- /dev/null +++ b/apps/swipeclock/ChangeLog @@ -0,0 +1 @@ +0.01: Init App diff --git a/apps/swipeclock/app.js b/apps/swipeclock/app.js new file mode 100644 index 0000000000..ddaaec1796 --- /dev/null +++ b/apps/swipeclock/app.js @@ -0,0 +1,277 @@ +var WIDTH = g.getWidth(); +var SIZE = Math.floor(WIDTH / (7*6)); + +function rotationString(fullTrackName, width, sizeOfChar, speed) { + const d = new Date(); + const maxCharVisible = Math.ceil(width / sizeOfChar) + const position =(d.getSeconds() * speed) % (fullTrackName.length - maxCharVisible); + return fullTrackName.length > maxCharVisible + ? fullTrackName.slice(position, position + maxCharVisible + 1) + : fullTrackName +} + +const screens = [ + { + name: "clock", + watchUiState: { + nightMode: false, + forceDraw: true, + width: WIDTH, + size: SIZE, + x: (WIDTH / 2) - SIZE * 6, + y: (g.getHeight()/2) - SIZE *7 + }, + render: () => { + var watchUiState = screens[currentScreen].watchUiState; + var d = new Date(); + var x = watchUiState.x; + var y = watchUiState.y; + var size = watchUiState.size; + var width = watchUiState.width; + + g.reset(); + if (watchUiState.nightMode) { + g.setColor("#ff1100"); + } + + g.setFont("7x11Numeric7Seg",size).setFontAlign(1,-1); + if ((d.getMinutes() == 0 && d.getSeconds() == 0) || watchUiState.forceDraw) { + g.clearRect(x - size * 14, y, x - size , y + size * 11); + g.drawString(d.getHours(), x, y); + } + + g.setFontAlign(-1,-1); + + if (d.getSeconds() % 2 == 0) g.drawString(":", x,y); + else g.clearRect(x, y, x + size *4, y + size * 12); + + if (d.getSeconds() == 0|| watchUiState.forceDraw) { + g.clearRect(x + size *4 , y, x + size * 17 , y + size * 11); + g.drawString(("0"+d.getMinutes()).substr(-2),x+size*4,y); + } + + // draw seconds + g.setFont("7x11Numeric7Seg",size/2); + g.clearRect(x+size*18,y + size*7,x+size*18 + size *8 ,y + size*12 ); + g.drawString(("0"+d.getSeconds()).substr(-2),x+size*18,y + size*7); + + if ((d.getHours() == 0 && d.getMinutes() == 0) || watchUiState.forceDraw) { + // date + var date = d.toString().split(" "); + g.setFont("6x8", size/1.5).setFontAlign(0,-1); + g.clearRect(width/4, y + size * 16, width/ 1.33 , y + size * 20); + g.drawString(date[2] + " " + date[1], width / 2 , y + size * 16); + } + + g.clearRect(width * 0.8, y + size * 18 , width , y + size * 26); + if (musicState && GadgetBridge.isConnected) { + g.setFont("6x8", size/2).setFontAlign(0,-1); + g.drawString(musicState.state === "play" ? "||" : '>', width * 0.9, y + size * 20); + } + + g.clearRect(width * 0.4, y - size * 12 , width , y - size * 8); + if (musicInfo && GadgetBridge.isConnected) { + const fullTrackName = musicInfo.track + " - " + musicInfo.artist; + g.setFont("6x8", size/2).setFontAlign(0,-1); + + g.drawString(rotationString(fullTrackName, width * 0.6, 6 * (size/2), 2), width * 0.68, y - size * 12) + } + + if ((d.getMinutes() % 2 == 0 && d.getSeconds() == 0) || watchUiState.forceDraw) { + var bat = E.getBattery(); + var color = bat <= 20 || watchUiState.nightMode ? "#ff1100" : "#ffffff"; + + if (Bangle.isCharging()) { + color = "#ffc413"; + } + + g.setColor(color); + g.setFont("6x8", size/1.5).setFontAlign(0,-1); + g.clearRect(0, y - size * 10, width , y - size * 6); + g.drawString(bat + "%", width / 8 + 10, y - size * 10); + } + + watchUiState.forceDraw = false; + }, + onButtonClick: (btn) => { + var watchUiState = screens[currentScreen].watchUiState; + if (btn == 1) { + watchUiState.nightMode = !watchUiState.nightMode; + if (watchUiState.nightMode) Bangle.setLCDBrightness(0.2); + else Bangle.setLCDBrightness(1); + + watchUiState.forceDraw = true; + screens[currentScreen].render(); + } else if (btn == 3) { + if (GadgetBridge.isConnected) GadgetBridge.musicControl(musicState && musicState.state === "play" ? "pause" : 'play'); + } + }, + }, +]; + +const musicView = { + name: "music", + watchUiState: { + nightMode: false, + forceDraw: true, + width: WIDTH, + size: SIZE, + x: (WIDTH / 2) - SIZE * 6, + y: (g.getHeight()/2) - SIZE *7 + }, + render: () => { + var watchUiState = musicView.watchUiState; + var y = watchUiState.y; + var size = watchUiState.size; + var width = watchUiState.width; + + g.reset(); + if (watchUiState.nightMode) { + g.setColor("#ff1100"); + } + + g.clearRect(0, y - size * 4 , width , y + size * 4); + if (musicInfo) { + if (musicInfo.track && GadgetBridge.isConnected) { + g.setFont("6x8", size/1.5).setFontAlign(0,-1); + const charSize = 6 * (size/1.5); + const margin = charSize * 2; + g.drawString(rotationString(musicInfo.track, width - margin, charSize, 1), width * 0.5 + (margin / 4), y - size * 4) + } + + g.clearRect(0, y + size * 2 , width , y + size * 6 ); + if (musicInfo.artist && GadgetBridge.isConnected) { + g.setFont("6x8", size/2).setFontAlign(0,-1); + g.drawString(rotationString("BY: " + musicInfo.artist, width, 6 * (size/2), 1), width * 0.5, y + size * 2) + } + } else { + g.setFont("6x8", size/1.5).setFontAlign(0,-1); + const charSize = 6 * (size/1.5); + const margin = charSize * 2; + g.drawString(rotationString("No Song.", width - margin, charSize, 1), width * 0.5 + (margin / 4), y - size * 4) + } + + g.clearRect(0, y + size * 10 , width , y + size * 18 ); + if (musicState && musicState.state && GadgetBridge.isConnected) { + g.setFont("6x8", size/2).setFontAlign(0,-1); + g.drawString(musicState.state == "play" ? "Playing" : "Paused", width * 0.5, y + size * 10) + } + + g.clearRect(0, y + size * 18 , width , y + size * 24); + if (GadgetBridge.isConnected) { + g.setFont("6x8", size/2).setFontAlign(0,-1); + g.drawString("<<", width * 0.25, y + size * 18) + g.drawString(">>", width * 0.75, y + size * 18) + g.drawString("+", width * 0.9, y - size * 12) + g.drawString("-", width * 0.9, y + size * 22) + } + + const d = new Date() + g.setFont("6x8", size/2).setFontAlign(0,-1); + g.clearRect(0, y - size * 10, (6 *(size/2)) * 5 , y - size * 6); + g.drawString(("0"+d.getHours()).substr(-2)+":"+("0"+d.getMinutes()).substr(-2), width / 8 + 10, y - size * 10); + }, + onButtonClick: (btn) => { + if (btn == 1) { + GadgetBridge.musicControl("volumeup"); + } else if (btn == 3) { + GadgetBridge.musicControl("volumedown"); + } else if (btn == 4) { + GadgetBridge.musicControl("previous"); + } else if (btn == 5) { + GadgetBridge.musicControl("next"); + } + }, +}; + +let currentScreen = 0; +let onSwipe = false; +let buttonTimeout; +let musicInfo = null; +let musicState = null; + +function clearScreen() { + g.clear(); + g.clearRect(0,0, g.getHeight(), WIDTH) + Bangle.setUI("clock"); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + if (screens[currentScreen].watchUiState) screens[currentScreen].watchUiState.forceDraw = true; +} + + +function draw() { + screens[currentScreen].render(); +} + +function onButtonClick(button) { + if (onSwipe) return; + + if (buttonTimeout) clearTimeout(buttonTimeout); + buttonTimeout = setTimeout(() => { + screens[currentScreen].onButtonClick(button); + }, 150); +} + +function onScreenSwipe(direction) { + if (screens.length === 1) return; + if (buttonTimeout) clearTimeout(buttonTimeout); + + onSwipe = true; + const newScreenPos = currentScreen + direction; + + if (newScreenPos >= 0) { + currentScreen = newScreenPos % screens.length; + } else { + currentScreen = screens.length + newScreenPos; + } + + clearScreen(); + setTimeout(() => onSwipe = false, 150); +} + +function addMusicView() { + if (screens.find(v => v === musicView)) return; + screens.push(musicView); +} + +function main() { + require("Font7x11Numeric7Seg").add(Graphics); + + // Only update when display turns on + if (process.env.BOARD!="SMAQ3") // hack for Q3 which is always-on + setWatch(() => onButtonClick(1), BTN1, { edge:"rising", debounce: 50, repeat:true }); + setWatch(() => onButtonClick(2), BTN2, { edge:"rising", debounce: 50, repeat:true }); + setWatch(() => onButtonClick(3), BTN3, { edge:"rising", debounce: 50, repeat:true }); + setWatch(() => onButtonClick(4), BTN4, { edge:"rising", debounce: 50, repeat:true }); + setWatch(() => onButtonClick(5), BTN5, { edge:"rising", debounce: 50, repeat:true }); + + Bangle.on('swipe', (direction) => onScreenSwipe(direction)); + + GadgetBridge.onEvent('musicinfo', (info) =>{ + musicInfo = info; + GadgetBridge.__private__.info = info; + }); + GadgetBridge.onEvent('musicstate', (state) => { + musicState = state; + GadgetBridge.__private__.state = state; + }); + GadgetBridge.onEvent('connect', addMusicView); + GadgetBridge.onEvent('disconnect', () => { + const musicViewIndex = screens.findIndex(v => v === musicView); + if (index == -1) return + + if (currentScreen === musicViewIndex) currentScreen = 0; + + screens.splice(musicViewIndex, 1); + }); + + + if (GadgetBridge.isConnected) addMusicView(); + + clearScreen(); + + setInterval(draw, 1000); +} +main(); \ No newline at end of file diff --git a/apps/swipeclock/icon.js b/apps/swipeclock/icon.js new file mode 100644 index 0000000000..d5d9aaf682 --- /dev/null +++ b/apps/swipeclock/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mUygP/AC5BlH4MAn/gAwN/4EP/AFBsEMhkBwEAjEDgYJBgEGgHA4EYDwOAmEwBIIYyj/wgf+AoMH/kA/4eBJXwYLVxgAjh//AC3w")) diff --git a/apps/swipeclock/icon.png b/apps/swipeclock/icon.png new file mode 100644 index 0000000000..cb08aec5e1 Binary files /dev/null and b/apps/swipeclock/icon.png differ