Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/qmsched/ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@
0.08: Support new Bangle.js 2 menu
0.09: Use default Bangle formatter for booleans
0.10: Allow using theme files
0.11: Add day-specific scheduling
Added clock info for fast switching
Consolidated getMode into lib.js
99 changes: 60 additions & 39 deletions apps/qmsched/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ let bSettings = STORAGE.readJSON('setting.json',true)||{};
let current = 0|bSettings.quiet;
delete bSettings; // we don't need any other global settings

const allDays = 0b1111111; // bits 0–6 = Sun, Mon, Tue, Wed, Thu, Fri, Sat
const dayNames = ["Su","Mo","Tu","We","Th","Fr","Sa"];

function formatDays(mask) {
if (!mask || mask === allDays) return "Every day";
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatDays treats a 0 bitmask as "Every day" because of the !mask check. However the scheduler logic treats a 0 mask as "no active days" (won’t ever trigger). Consider distinguishing undefined/missing from 0, and either prevent persisting days:0 or display it as "No days".

Suggested change
if (!mask || mask === allDays) return "Every day";
if (mask == null) return "Every day"; // undefined/null => default to every day
if (mask === 0) return "No days";
if (mask === allDays) return "Every day";

Copilot uses AI. Check for mistakes.
if (mask === 0b0111110) return "Mon-Fri";
if (mask === 0b1000001) return "Weekends";
return dayNames.filter((_, i) => mask & (1 << i)).join(",");
}



Expand Down Expand Up @@ -49,6 +58,7 @@ function loadSettings() {
scheds: settings,
};
// store new format

save();
// and clean up qmOptions from global settings file
delete bOptions.qmOptions;
Expand Down Expand Up @@ -101,7 +111,6 @@ function applyTheme() {
*/
function showThemeMenu(back, quiet){
const option = quiet ? "quietTheme" : "normalTheme";
function cl(x) { return g.setColor(x).getColor(); }
var themesMenu = {
'':{title:/*LANG*/'Theme', back: back},
/*LANG*/'Default': ()=>{
Expand All @@ -126,6 +135,7 @@ function showThemeMenu(back, quiet){
* Library uses this to make the app update itself
* @param {int} mode New Quite Mode
*/
//eslint-disable-next-line no-unused-vars
function setAppQuietMode(mode) {
if (mode === current) return;
current = mode;
Expand All @@ -146,69 +156,80 @@ function showMainMenu() {
};
scheds.sort((a, b) => (a.hr-b.hr));
scheds.forEach((sched, idx) => {
menu[formatTime(sched.hr)] = () => { showEditMenu(idx); };
menu[formatTime(sched.hr)].format = () => modeNames[sched.mode]+' >'; // this does nothing :-(
const label = formatTime(sched.hr) + " " + formatDays(sched.days ?? allDays);
menu[label] = () => { showEditMenu(idx); };
});
menu[/*LANG*/"Add Schedule"] = () => showEditMenu(-1);
menu[/*LANG*/"Switch Theme"] = {
value: !!get("switchTheme"),
onchange: v => v ? set("switchTheme", v) : unset("switchTheme"),
};
menu[/*LANG*/"Options"] = () => showOptionsMenu();

m = E.showMenu(menu);
}

}
function showDaysMenu(mask, onDone) {
let cur = mask;
let menu = {"": {title: "Active Days"}};
menu["< Back"] = () => onDone(cur);
Comment thread
RKBoss6 marked this conversation as resolved.
dayNames.forEach((name, i) => {
menu[name] = {
value: !!(cur & (1 << i)),
onchange: v => {
if (v) cur |= (1 << i);
else cur &= ~(1 << i);
},
};
});
E.showMenu(menu);
}
function showEditMenu(index) {
const isNew = index<0;
let hrs = 12, mins = 0;
let mode = 1;
let days = allDays;
if (!isNew) {
const s = scheds[index];
hrs = 0|s.hr;
mins = Math.round((s.hr-hrs)*60);
mode = s.mode;
days = ("days" in s) ? s.days : allDays;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

days = ("days" in s) ? s.days : allDays; preserves days:0 for existing schedules. Since a 0 mask means "no days" to the boot scheduler, it’s safer to normalize/validate here (eg treat 0 as allDays or require at least one selected day before saving).

Suggested change
days = ("days" in s) ? s.days : allDays;
let loadedDays = ("days" in s) ? s.days : allDays;
days = loadedDays || allDays;

Copilot uses AI. Check for mistakes.
}
let menu = {"": {"title": (isNew ? /*LANG*/"Add Schedule" : /*LANG*/"Edit Schedule")}};
menu[B2 ? "< Back" : /*LANG*/"< Cancel"] = () => showMainMenu();
menu[/*LANG*/"Hours"] = {
value: hrs,
min:0, max:23, wrap:true,
onchange: v => {hrs = v;},
};
menu[/*LANG*/"Minutes"] = {
value: mins,
min:0, max:55, step:5, wrap:true,
onchange: v => {mins = v;},
};
menu[/*LANG*/"Switch to"] = {
value: mode,
min:0, max:2, wrap:true,
format: v => modeNames[v],
onchange: v => {mode = v;},
};
function getSched() {
return {
hr: hrs+(mins/60),
mode: mode,

function show() {
let menu = {"": {"title": (isNew ? "Add Schedule" : "Edit Schedule")}};
menu[B2 ? "< Back" : "< Cancel"] = () => showMainMenu();
Comment on lines +201 to +202
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In showEditMenu, the new title/back strings ("Add Schedule", "Edit Schedule", "< Cancel") are user-facing but no longer have /*LANG*/ markers. Please restore translation markers for these labels.

Suggested change
let menu = {"": {"title": (isNew ? "Add Schedule" : "Edit Schedule")}};
menu[B2 ? "< Back" : "< Cancel"] = () => showMainMenu();
let menu = {"": {"title": (isNew ? /*LANG*/"Add Schedule" : /*LANG*/"Edit Schedule")}};
menu[B2 ? "< Back" : /*LANG*/"< Cancel"] = () => showMainMenu();

Copilot uses AI. Check for mistakes.
menu[/*LANG*/"Hours"] = { value: hrs, min:0, max:23, wrap:true, onchange: v => {hrs = v;} };
menu[/*LANG*/"Minutes"] = { value: mins, min:0, max:55, step:5, wrap:true, onchange: v => {mins = v;} };
menu[/*LANG*/"Days: "+formatDays(days)] = () => {
showDaysMenu(days, newDays => {
days = newDays || allDays;
show(); // rebuild with updated days key
});
};
}
menu[B2 ? /*LANG*/"Save" : /*LANG*/"> Save"] = function() {
if (isNew) {
scheds.push(getSched());
} else {
scheds[index] = getSched();
menu[/*LANG*/"Switch to"] = { value: mode, min:0, max:2, wrap:true, format: v => modeNames[v], onchange: v => {mode = v;} };

function getSched() {
return { hr: hrs+(mins/60), mode: mode, days: days };
}
save();
showMainMenu();
};
if (!isNew) {
menu[B2 ? /*LANG*/"Delete" : /*LANG*/"> Delete"] = function() {
scheds.splice(index, 1);
menu[B2 ? /*LANG*/"Save" : /*LANG*/"> Save"] = function() {
if (isNew) scheds.push(getSched());
else scheds[index] = getSched();
save();
showMainMenu();
};
if (!isNew) {
menu[B2 ? /*LANG*/"Delete" : /*LANG*/"> Delete"] = function() {
scheds.splice(index, 1);
save();
showMainMenu();
};
}
m = E.showMenu(menu);
}
m = E.showMenu(menu);

show();
}

function showOptionsMenu() {
Expand Down
38 changes: 22 additions & 16 deletions apps/qmsched/boot.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
// apply Quiet Mode schedules
(function qm() {
if (Bangle.qmTimeout) clearTimeout(Bangle.qmTimeout); // so the app can eval() this file to apply changes right away
if (Bangle.qmTimeout) clearTimeout(Bangle.qmTimeout);
delete Bangle.qmTimeout;
let bSettings = require('Storage').readJSON('setting.json',true)||{};
const curr = 0|bSettings.quiet;
delete bSettings;
if (curr) require("qmsched").applyOptions(curr); // no need to re-apply default options

if (curr) require("qmsched").applyOptions(curr);
let settings = require('Storage').readJSON('qmsched.json',true)||{};
let scheds = settings.scheds||[];
if (!scheds.length) {return;}
Comment on lines 9 to 10
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

boot.js assumes qmsched.json is an object with scheds, but app.js still supports migrating an older format where qmsched.json is a plain array. If a user upgrades and reboots before opening the app (so migration hasn’t run), settings will be an array and settings.scheds will be undefined, causing schedules to be skipped entirely. Consider detecting Array.isArray(settings) here and treating it as the schedule list for backward compatibility.

Suggested change
let scheds = settings.scheds||[];
if (!scheds.length) {return;}
let scheds = Array.isArray(settings) ? settings : (settings.scheds || []);
if (!scheds || !scheds.length) {return;}

Copilot uses AI. Check for mistakes.
const now = new Date(),
hr = now.getHours()+(now.getMinutes()/60)+(now.getSeconds()/3600); // current (decimal) hour
const days = 0b1111111;
const now = new Date();
const hr = now.getHours()+(now.getMinutes()/60)+(now.getSeconds()/3600);
scheds.sort((a, b) => a.hr-b.hr);
const tday = scheds.filter(s => s.hr>hr), // scheduled for today
tmor = scheds.filter(s => s.hr<=hr); // scheduled for tomorrow
const next = tday.length ? tday[0] : tmor[0],
mode = next.mode;
let t = 3600000*(next.hr-hr); // timeout in milliseconds
if (t<0) {t += 86400000;} // scheduled for tomorrow: add a day
/* update quiet mode at the correct time. */
let best = null, bestT = Infinity;
for (let i = 0; i < scheds.length; i++) {
const s = scheds[i];
const mask = s.days !== undefined ? s.days : days;
for (let d = 0; d < 7; d++) {
const dayBit = 1 << ((now.getDay() + d) % 7);
if (!(mask & dayBit)) continue;
let t = 3600000 * (s.hr - hr) + d * 86400000;
if (t <= 0) continue;
if (t < bestT) { bestT = t; best = s; }
break;
}
}
if (!best) {return;}
Bangle.qmTimeout=setTimeout(() => {
require("qmsched").setMode(mode);
qm(); // schedule next update
}, t);
require("qmsched").setMode(best.mode);
qm();
}, bestT);
})();
34 changes: 34 additions & 0 deletions apps/qmsched/clkinfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
(function() {
return {
name: "qmsched",
items: [
{ name : "Quiet Mode Toggle",
get : function() {
const mode = require("qmsched").getMode()
let txt="Off";
let img=atob("GBgBAAAAAAAAAAAAABwAABwAAH8AAP+AAf/AA//gA//gA//gB//wA//gA//gA//gA//gA//gA//gABwAAD4AABwAAAgAAAAAAAAA");
if(mode==1){
txt="Alarms";
img=atob("GBgBAAAAAAAAAAgAAP+AA//gB//wD//4D//4H//8H//8AAAAAAAAIADiAADgAADgH/nwH/nwD/P4D/P4B/hAA/zgAPnwAAjgAABA")
Comment on lines +8 to +12
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clockinfo get() path builds img by decoding base64 with atob(...) on each call. Since these icons are constant, consider decoding once (module-level consts) and reusing them to reduce allocations/CPU during redraws.

Copilot uses AI. Check for mistakes.
}
if(mode==2){
txt="Silent"
img=atob("GBgBAAAAAAAAAAgAAP+AA//gB//wD//4D//4H//8H//8GAAMGAAMOAAOGAAMH//8H//8H//8D//4D//4B//wA//gAP+AAAgAAAAA");
}

return {
text : txt,
img : img}},
show : function() {},
hide : function() {},
run : function() {
let mode=require("qmsched").getMode();
mode = (mode + 1) % 3;
require("qmsched").setMode(mode);
this.emit("redraw");
}

}
]
};
})
8 changes: 8 additions & 0 deletions apps/qmsched/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,13 @@ exports.setMode = function(mode) {
));
exports.applyOptions(mode);
if (typeof WIDGETS === "object" && "qmsched" in WIDGETS) WIDGETS["qmsched"].draw();
//eslint-disable-next-line no-undef
if (global.setAppQuietMode) setAppQuietMode(mode); // current app knows how to update itself
};
/**
* Get the active quiet mode
* Returns: (int) active quiet mode
*/
exports.getMode = function(){
Comment thread
RKBoss6 marked this conversation as resolved.
return (require("Storage").readJSON("setting.json", 1) || {}).quiet|0;
};
9 changes: 5 additions & 4 deletions apps/qmsched/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@
"id": "qmsched",
"name": "Quiet Mode Schedule and Widget",
"shortName": "Quiet Mode",
"version": "0.10",
"description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.",
"version": "0.11",
"description": "Schedule quiet modes and options to activate at certain times, and provides a clockinfo and widget for changing/viewing modes.",
"icon": "app.png",
"screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},
{"url":"screenshot_b2_main.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_lcd.png"}],
"tags": "tool,widget",
"tags": "tool,widget,clkinfo",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"qmsched","url":"lib.js"},
{"name":"qmsched.app.js","url":"app.js"},
{"name":"qmsched.boot.js","url":"boot.js"},
{"name":"qmsched.img","url":"icon.js","evaluate":true},
{"name":"qmsched.wid.js","url":"widget.js"}
{"name":"qmsched.wid.js","url":"widget.js"},
{"name":"qmsched.clkinfo.js","url":"clkinfo.js"}
],
"data": [{"name":"qmsched.json"}]
}
4 changes: 2 additions & 2 deletions apps/qmsched/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
area: "tl",
width: ((require("Storage").readJSON("setting.json", 1) || {}).quiet|0) ? 24 : 0,
draw: function() {
const mode = (require("Storage").readJSON("setting.json", 1) || {}).quiet|0;
const mode = require("qmsched").getMode();
Comment thread
RKBoss6 marked this conversation as resolved.
if (mode===0) { // Off
if (this.width!==0) {
this.width = 0;
Expand Down Expand Up @@ -33,4 +33,4 @@
.drawLine(x+5, y-3, x+3, y-5).drawLine(x-5, y-3, x-3, y-5); // wriggles
},
};
})();
})();
Loading