From e8b0849d519c27fe91fb6ea004c22bc288a2e13a Mon Sep 17 00:00:00 2001 From: Logan B <3870583+kidneyhex@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:48:36 -0600 Subject: [PATCH 1/4] [messages][messagegui] - fix music info handling --- apps/messagegui/ChangeLog | 1 + apps/messagegui/lib.js | 47 ++++++++++++++++++++++++++++++++--- apps/messagegui/metadata.json | 2 +- apps/messages/ChangeLog | 1 + apps/messages/lib.js | 14 ++++++++--- apps/messages/metadata.json | 2 +- 6 files changed, 58 insertions(+), 9 deletions(-) diff --git a/apps/messagegui/ChangeLog b/apps/messagegui/ChangeLog index 48d5234fe9..26db73da16 100644 --- a/apps/messagegui/ChangeLog +++ b/apps/messagegui/ChangeLog @@ -119,3 +119,4 @@ 0.87: Make choosing of font size more repeatable 0.88: Adjust padding calculation so messages are spaced out properly even when using international fonts 0.89: Fix bugs related to empty titles and bodies +0.90: Persist music info; Fix old music info copying into new music messages diff --git a/apps/messagegui/lib.js b/apps/messagegui/lib.js index 43141531f3..f3aacb2488 100644 --- a/apps/messagegui/lib.js +++ b/apps/messagegui/lib.js @@ -37,8 +37,11 @@ exports.listener = function(type, msg) { const appSettings = require("Storage").readJSON("messages.settings.json", 1) || {}; let loadMessages = (Bangle.CLOCK || msg.important); // should we load the messages app? if (type==="music") { - if (Bangle.CLOCK && msg.state && msg.title && appSettings.openMusic) loadMessages = true; - else return; + if (Bangle.CLOCK && msg.state && msg.title && appSettings.openMusic) { + loadMessages = true; + } else { + if (persistMusicInfo(msg)) return; // handled + } } // Write the message to Bangle.MESSAGES. We'll deal with it in messageTimeout below if (!Bangle.MESSAGES) Bangle.MESSAGES = []; @@ -48,7 +51,9 @@ exports.listener = function(type, msg) { // save messages from RAM to flash if we decide not to launch app // We apply all of Bangle.MESSAGES here in one write if (!Bangle.MESSAGES || !Bangle.MESSAGES.length) return; - let messages = require("messages").getMessages(msg); + // Load saved messages without applying the current msg to avoid + // applying it twice (getMessages(msg) would apply it already). + let messages = require("messages").getMessages(); (Bangle.MESSAGES || []).forEach(m => require("messages").apply(m, messages)); require("messages").write(messages); delete Bangle.MESSAGES; @@ -105,3 +110,39 @@ exports.open = function(msg) { Bangle.load((msg && msg.new && msg.id!=="music") ? "messagegui.new.js" : "messagegui.app.js"); }; + +/** + * Persist info fields from music messages + * @param {*} msg Music message + * @returns true if the message was handled + */ +function persistMusicInfo(msg) { + msg.handled = true; + // if nothing to persist - return that it's handled + if (msg.artist === undefined && msg.track === undefined && msg.album === undefined && msg.dur === undefined) return true; + + const a = msg.artist, t = msg.track, al = msg.album, d = msg.dur; + + // try to find the last music message + const messagesMod = require("messages"); + const messages = messagesMod.getMessages(); + const mIdx = messages.findIndex(m => m.id === "music"); + const stored = mIdx >= 0 ? messages[mIdx] : null; + + if (!stored) { + // new msg, always write + const newEntry = { id: "music", artist: a, track: t, album: al, dur: d }; + messages.unshift(Object.assign({}, newEntry)); + messagesMod.write(messages); + } else { + // existing msg, only write if something changed + if (stored.artist !== a || stored.track !== t || stored.album !== al || stored.dur !== d) { + stored.artist = a; + stored.track = t; + stored.album = al; + stored.dur = d; + messagesMod.write(messages); + } + } + return true; +} \ No newline at end of file diff --git a/apps/messagegui/metadata.json b/apps/messagegui/metadata.json index 8ef38bd063..a1752355d8 100644 --- a/apps/messagegui/metadata.json +++ b/apps/messagegui/metadata.json @@ -2,7 +2,7 @@ "id": "messagegui", "name": "Message UI", "shortName": "Messages", - "version": "0.89", + "version": "0.90", "description": "Default app to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app", diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index 1ea1fd9678..4be47db0c1 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -12,3 +12,4 @@ 0.66: Fix 'Auto-Open Unread Msg' polarity - previously checking the box would ignore unread messages 0.67: Ensure default vibration pattern is longer Add Option to show widgets (Message GUI 0.86 removes them by default) +0.68: Fix perserving music info between messages diff --git a/apps/messages/lib.js b/apps/messages/lib.js index e22e89cc46..819f900851 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -24,14 +24,20 @@ exports.pushMessage = function(event) { if (event.t==="add") { if (event.new===undefined) event.new = true; // Assume it should be new } else if (event.t==="modify") { - const old = exports.getMessages().find(m => m.id===event.id); - if (old) event = Object.assign(old, event); + // For non-music messages, merge with stored message + // For music, skip merging to avoid old info + if (event.id !== "music") { + const old = exports.getMessages().find(m => m.id===event.id); + if (old) event = Object.assign(old, event); + } } // combine musicinfo and musicstate events if (event.id==="music") { if (event.state==="play") event.new = true; // new track, or playback (re)started - event = Object.assign(exports.music, event); + // Merge new msg into current music to preserve info + exports.music = Object.assign(exports.music, event); + event = exports.music; } } // reset state (just in case) @@ -72,7 +78,7 @@ exports.apply = function(event, messages) { messages.splice(mIdx, 1); } else if (event.t==="add") { if (mIdx>=0) messages.splice(mIdx, 1); // duplicate ID! erase previous version - messages.unshift(event); // add at the beginning + messages.unshift(Object.assign({}, event)); // add a copy at the beginning } else if (event.t==="modify") { if (mIdx>=0) messages[mIdx] = Object.assign(messages[mIdx], event); else messages.unshift(event); diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json index 4b25d8398e..85a67c92d7 100644 --- a/apps/messages/metadata.json +++ b/apps/messages/metadata.json @@ -1,7 +1,7 @@ { "id": "messages", "name": "Messages", - "version": "0.67", + "version": "0.68", "description": "Library to handle, load and store message events received from Android/iOS", "icon": "app.png", "type": "module", From e777a50976f352adf1c69acb2d2e82fba513193d Mon Sep 17 00:00:00 2001 From: Logan B <3870583+kidneyhex@users.noreply.github.com> Date: Thu, 27 Nov 2025 23:22:13 -0600 Subject: [PATCH 2/4] [messagegui] display music message first --- apps/messagegui/app.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/messagegui/app.js b/apps/messagegui/app.js index 5b30957304..e37931e098 100644 --- a/apps/messagegui/app.js +++ b/apps/messagegui/app.js @@ -67,10 +67,23 @@ if (Bangle.MESSAGES) { delete Bangle.MESSAGES; } +// Ensure any music message is always kept at the front of the list +function moveMusicToFront() { + for (var i=0;i Date: Fri, 28 Nov 2025 14:07:17 -0600 Subject: [PATCH 3/4] [messages][messagegui] handle if we get musicstate but don't have existing musicinfo; add settings for expiring old music msg --- apps/messagegui/app.js | 22 +++++++++++++++++++++- apps/messages/lib.js | 20 +++++++++++++++++--- apps/messages/settings.js | 6 ++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/apps/messagegui/app.js b/apps/messagegui/app.js index e37931e098..11403a1d47 100644 --- a/apps/messagegui/app.js +++ b/apps/messagegui/app.js @@ -90,7 +90,15 @@ var onMessagesModified = function(type,msg) { require("messages").buzz(msg.src); } if (msg && msg.id=="music") { - if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to + // Track when music actually played so we can expire old music messages + if (msg.state && msg.state=="play") { + // Update the stored music message (if present) with a last-played timestamp + var mm = MESSAGES.find(m=>m && m.id=="music"); + if (mm) mm._lastPlayed = Date.now(); + openMusic = true; + } else if (msg.state && msg.state!="play") { + openMusic = false; // no longer playing music to go back to + } if ((active!=undefined) && (active!="list") && (active!="music")) return; // don't open music over other screens (but do if we're in the main menu) } if (msg && msg.id=="nav" && msg.t=="modify" && active!="map") @@ -531,6 +539,18 @@ function showMessage(msgid, persist) { */ function checkMessages(options) { options=options||{}; + // Remove/ignore stale music messages if they haven't played recently. + var musicTimeout = (settings && settings.musicTimeoutMinutes) ? settings.musicTimeoutMinutes : 5; + if (isFinite(musicTimeout) && musicTimeout>0) { + var now = Date.now(); + MESSAGES = MESSAGES.filter(function(m) { + if (!m || m.id!="music") return true; + if (m.state=="play" || m.state=="show") return true; + if (m._lastPlayed && (now - m._lastPlayed) <= musicTimeout*60000) return true; + // otherwise drop the stale music message + return false; + }); + } // If there's been some user interaction, it's time to stop repeated buzzing if (!options.dontStopBuzz) require("messages").stopBuzz(); diff --git a/apps/messages/lib.js b/apps/messages/lib.js index 819f900851..7cce35c0da 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -35,9 +35,23 @@ exports.pushMessage = function(event) { // combine musicinfo and musicstate events if (event.id==="music") { if (event.state==="play") event.new = true; // new track, or playback (re)started - // Merge new msg into current music to preserve info - exports.music = Object.assign(exports.music, event); - event = exports.music; + // Check if we have music info before combining + const hasMusicInfo = exports.music.track || exports.music.artist || exports.music.album || exports.music.dur; + event = Object.assign(exports.music, event); + + // If this is a musicstate message and we don't have any music message stored + // then set track to "Music" so we can trigger displaying music controls + if (event.state && !event.track) { + const messages = exports.getMessages(); + const hasMusicMsg = messages.length && messages.findIndex(m => m.id === "music") >= 0; + + if (!hasMusicMsg && !hasMusicInfo) { + event.track = "Music"; + event.title = event.title || "Music"; // ensure title is set for messagegui/lib.js check + exports.music.track = "Music"; + exports.music.title = exports.music.title || "Music"; + } + } } } // reset state (just in case) diff --git a/apps/messages/settings.js b/apps/messages/settings.js index 5690dde79a..b37d899881 100644 --- a/apps/messages/settings.js +++ b/apps/messages/settings.js @@ -67,6 +67,12 @@ value: !!settings.openMusic, onchange: v => updateSetting("openMusic", v) }, + /*LANG*/'Music Msg Timeout': { + value: (settings && settings.musicTimeoutMinutes!=null) ? settings.musicTimeoutMinutes : 5, + min: 0, max: 240, step: 1, + format: v => v ? v+/*LANG*/"m" : /*LANG*/"Off", + onchange: v => updateSetting("musicTimeoutMinutes", v) + }, /*LANG*/'Unlock Watch': { value: !!settings.unlockWatch, onchange: v => updateSetting("unlockWatch", v) From 4e6f46e397de24fadc1b57b1b7ee0c8fa47e954b Mon Sep 17 00:00:00 2001 From: Logan B <3870583+kidneyhex@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:44:59 -0600 Subject: [PATCH 4/4] [messages][messagegui] Update to use messages.music as in-memory storage; and then save to json on kill --- apps/messagegui/app.js | 86 +++++++++++++++++------------ apps/messagegui/lib.js | 42 ++------------ apps/messages/lib.js | 121 +++++++++++++++++++++++++++++++++++------ 3 files changed, 158 insertions(+), 91 deletions(-) diff --git a/apps/messagegui/app.js b/apps/messagegui/app.js index 11403a1d47..acf3864c66 100644 --- a/apps/messagegui/app.js +++ b/apps/messagegui/app.js @@ -67,39 +67,51 @@ if (Bangle.MESSAGES) { delete Bangle.MESSAGES; } -// Ensure any music message is always kept at the front of the list -function moveMusicToFront() { - for (var i=0;i m.id === "music"); + if (existingMusicIdx >= 0) { + MESSAGES[existingMusicIdx] = musicMsg; + if (existingMusicIdx !== 0) MESSAGES.unshift(MESSAGES.splice(existingMusicIdx, 1)[0]); + } else { + MESSAGES.unshift(musicMsg); } } -moveMusicToFront(); + +// Load music message from storage if it exists +var musicMsg = require("messages").getMusic(); +if (musicMsg && (musicMsg.track || musicMsg.artist)) { + updateMusicMessage(musicMsg); +} var onMessagesModified = function(type,msg) { if (msg.handled) return; msg.handled = true; - require("messages").apply(msg, MESSAGES); - // Keep music message at the front so it remains the first item - moveMusicToFront(); - // TODO: if new, show this new one - if (msg && msg.id!=="music" && msg.id!=="nav" && msg.new && - !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { - require("messages").buzz(msg.src); - } - if (msg && msg.id=="music") { - // Track when music actually played so we can expire old music messages - if (msg.state && msg.state=="play") { - // Update the stored music message (if present) with a last-played timestamp - var mm = MESSAGES.find(m=>m && m.id=="music"); - if (mm) mm._lastPlayed = Date.now(); + + if (msg.id === "music") { + // For music messages, get the complete state from messages module + updateMusicMessage(require("messages").getMusic()); + var musicMsg = MESSAGES[0]; // music is now at front + + if (musicMsg.state && musicMsg.state=="play") { openMusic = true; - } else if (msg.state && msg.state!="play") { + } else if (musicMsg.state && musicMsg.state!="play") { openMusic = false; // no longer playing music to go back to } if ((active!=undefined) && (active!="list") && (active!="music")) return; // don't open music over other screens (but do if we're in the main menu) + } else { + require("messages").apply(msg, MESSAGES); + // Move music back to front since apply() may have pushed it down + var musicIdx = MESSAGES.findIndex(m => m.id === "music"); + if (musicIdx > 0) { + MESSAGES.unshift(MESSAGES.splice(musicIdx, 1)[0]); + } + } + + // TODO: if new, show this new one + if (msg && msg.id!=="music" && msg.id!=="nav" && msg.new && + !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { + require("messages").buzz(msg.src); } if (msg && msg.id=="nav" && msg.t=="modify" && active!="map") return; // don't show an updated nav message if we're just in the menu @@ -539,18 +551,8 @@ function showMessage(msgid, persist) { */ function checkMessages(options) { options=options||{}; - // Remove/ignore stale music messages if they haven't played recently. - var musicTimeout = (settings && settings.musicTimeoutMinutes) ? settings.musicTimeoutMinutes : 5; - if (isFinite(musicTimeout) && musicTimeout>0) { - var now = Date.now(); - MESSAGES = MESSAGES.filter(function(m) { - if (!m || m.id!="music") return true; - if (m.state=="play" || m.state=="show") return true; - if (m._lastPlayed && (now - m._lastPlayed) <= musicTimeout*60000) return true; - // otherwise drop the stale music message - return false; - }); - } + // Remove/ignore stale music messages if they haven't played recently + checkMusicExpired(); // If there's been some user interaction, it's time to stop repeated buzzing if (!options.dontStopBuzz) require("messages").stopBuzz(); @@ -650,6 +652,20 @@ function returnToClockIfEmpty() { checkMessages({clockIfNoMsg:1,clockIfAllRead:0,ignoreUnread:1,openMusic}); } +function checkMusicExpired() { + var musicTimeout = (settings && settings.musicTimeoutMinutes) ? settings.musicTimeoutMinutes : 5; + if (!isFinite(musicTimeout) || musicTimeout <= 0) return; + + var now = Date.now(); + MESSAGES = MESSAGES.filter(function(m) { + if (!m || m.id!="music") return true; + if (m.state=="play" || m.state=="show") return true; + if (m._lastPlayed && (now - m._lastPlayed) <= musicTimeout*60000) return true; + // otherwise drop the stale music message + return false; + }); +} + function cancelReloadTimeout() { if (!unreadTimeout) return; clearTimeout(unreadTimeout); diff --git a/apps/messagegui/lib.js b/apps/messagegui/lib.js index f3aacb2488..02c07867b4 100644 --- a/apps/messagegui/lib.js +++ b/apps/messagegui/lib.js @@ -37,10 +37,12 @@ exports.listener = function(type, msg) { const appSettings = require("Storage").readJSON("messages.settings.json", 1) || {}; let loadMessages = (Bangle.CLOCK || msg.important); // should we load the messages app? if (type==="music") { + // Music persistence is handled by messages module via pushMessage + msg.handled = true; if (Bangle.CLOCK && msg.state && msg.title && appSettings.openMusic) { loadMessages = true; } else { - if (persistMusicInfo(msg)) return; // handled + return; // handled } } // Write the message to Bangle.MESSAGES. We'll deal with it in messageTimeout below @@ -109,40 +111,4 @@ exports.open = function(msg) { } Bangle.load((msg && msg.new && msg.id!=="music") ? "messagegui.new.js" : "messagegui.app.js"); -}; - -/** - * Persist info fields from music messages - * @param {*} msg Music message - * @returns true if the message was handled - */ -function persistMusicInfo(msg) { - msg.handled = true; - // if nothing to persist - return that it's handled - if (msg.artist === undefined && msg.track === undefined && msg.album === undefined && msg.dur === undefined) return true; - - const a = msg.artist, t = msg.track, al = msg.album, d = msg.dur; - - // try to find the last music message - const messagesMod = require("messages"); - const messages = messagesMod.getMessages(); - const mIdx = messages.findIndex(m => m.id === "music"); - const stored = mIdx >= 0 ? messages[mIdx] : null; - - if (!stored) { - // new msg, always write - const newEntry = { id: "music", artist: a, track: t, album: al, dur: d }; - messages.unshift(Object.assign({}, newEntry)); - messagesMod.write(messages); - } else { - // existing msg, only write if something changed - if (stored.artist !== a || stored.track !== t || stored.album !== al || stored.dur !== d) { - stored.artist = a; - stored.track = t; - stored.album = al; - stored.dur = d; - messagesMod.write(messages); - } - } - return true; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/apps/messages/lib.js b/apps/messages/lib.js index 7cce35c0da..7a3eca47d5 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -1,4 +1,10 @@ exports.music = {}; + +// Track if music has been modified and needs saving +let musicLoaded = false; +let musicDirty = false; +let killHandlerSet = false; + /** * Emit "message" event with appropriate type from Bangle * @param {object} msg @@ -34,24 +40,8 @@ exports.pushMessage = function(event) { // combine musicinfo and musicstate events if (event.id==="music") { - if (event.state==="play") event.new = true; // new track, or playback (re)started - // Check if we have music info before combining - const hasMusicInfo = exports.music.track || exports.music.artist || exports.music.album || exports.music.dur; - event = Object.assign(exports.music, event); - - // If this is a musicstate message and we don't have any music message stored - // then set track to "Music" so we can trigger displaying music controls - if (event.state && !event.track) { - const messages = exports.getMessages(); - const hasMusicMsg = messages.length && messages.findIndex(m => m.id === "music") >= 0; - - if (!hasMusicMsg && !hasMusicInfo) { - event.track = "Music"; - event.title = event.title || "Music"; // ensure title is set for messagegui/lib.js check - exports.music.track = "Music"; - exports.music.title = exports.music.title || "Music"; - } - } + setMusic(event); + event = getMusic(); } } // reset state (just in case) @@ -255,3 +245,98 @@ exports.stopBuzz = function() { if (exports.stopTimeout) clearTimeout(exports.stopTimeout); delete exports.stopTimeout; }; + +/** + * Lazy-load music from messages.music.json if not already loaded + */ +function loadMusic() { + if (musicLoaded) return; + const stored = require("Storage").readJSON("messages.music.json", true); + if (stored) { + exports.music = stored; + } + musicLoaded = true; +} + +/** + * Save music info to messages.music.json if dirty + */ +function saveMusicToFlash() { + if (!musicDirty) return; + + // Debug counter to track saves + incrementSaveCounter("music"); + + // Only save if we have actual music data + if (Object.keys(exports.music).length > 0) { + require("Storage").writeJSON("messages.music.json", exports.music); + } else { + require("Storage").erase("messages.music.json"); + } + + musicDirty = false; +} + +/** + * Set music info - merges music message data into exports.music, saves to flash on kill + * @param {object} msg Music message + */ +function setMusic(msg) { + // Lazy-load existing music data + loadMusic(); + + // Merge in artist/track/album/dur if provided + if (msg.artist !== undefined) exports.music.artist = msg.artist; + if (msg.track !== undefined) exports.music.track = msg.track; + if (msg.album !== undefined) exports.music.album = msg.album; + if (msg.dur !== undefined) exports.music.dur = msg.dur; + + // Merge in state and update _lastPlayed timestamp when playing + if (msg.state !== undefined) { + exports.music.state = msg.state; + if (msg.state === "play") { + exports.music._lastPlayed = Date.now(); + } + } + + // If this is a musicstate message and we don't have track info yet, + // set track to "Music" so we can trigger displaying music controls + if (msg.state && !exports.music.track) { + exports.music.track = "Music"; + exports.music.title = exports.music.title || "Music"; + } + + // Mark as dirty so it gets saved on kill + musicDirty = true; + + // Set up kill handler only once + if (!killHandlerSet) { + E.on("kill", saveMusicToFlash); + killHandlerSet = true; + } +} + +/** + * Get current music info with event metadata + * @returns {object} Music message object + */ +function getMusic() { + loadMusic(); + const event = Object.assign({ id: "music" }, exports.music); + if (event.state === "play") event.new = true; // new track, or playback (re)started + return event; +} + +/** + * Get current music message + * @returns {object} Music message object with id, state, track, artist, etc. + */ +exports.getMusic = getMusic; + +function incrementSaveCounter(name) { + const storage = require("Storage"); + let stats = storage.readJSON("message_stats.json", true) || {}; + if (!stats[name]) stats[name] = 0; + stats[name]++; + storage.writeJSON("message_stats.json", stats); +} \ No newline at end of file