var DEBUG = false; var storage = require("Storage"); var showMainMenu = () => { log("loading patterns"); var storedPatterns = storage.readJSON("ptlaunch.patterns.json", 1) || {}; var mainmenu = { "": { title: "Pattern Launcher", }, "< Back": () => { log("cancel"); load(); }, "Add Pattern": () => { log("creating pattern"); recognizeAndDrawPattern().then((pattern) => { log("got pattern"); log(pattern); log(pattern.length); var confirmPromise = new Promise((resolve) => resolve(true)); if (storedPatterns[pattern]) { log("pattern already exists. show confirmation prompt"); confirmPromise = E.showPrompt("Pattern already exists\nOverwrite?", { title: "Confirm", buttons: { Yes: true, No: false }, }); } confirmPromise.then((confirm) => { log("confirmPromise resolved: " + confirm); if (!confirm) { showMainMenu(); return; } log("selecting app"); getSelectedApp().then((app) => { E.showMessage("Saving..."); log("got app"); log("saving pattern"); storedPatterns[pattern] = { app: { name: app.name, src: app.src }, }; storage.writeJSON("ptlaunch.patterns.json", storedPatterns); showMainMenu(); }); }); }); }, "Manage Patterns": () => { log("selecting pattern through app"); showScrollerContainingAppsWithPatterns().then((selected) => { var pattern = selected.pattern; var appName = selected.appName; if (pattern === "back") { showMainMenu(); } else { E.showPrompt(appName + "\n\npattern:\n" + pattern, { title: "Delete?", buttons: { Yes: true, No: false }, }).then((confirm) => { if (confirm) { E.showMessage("Deleting..."); delete storedPatterns[pattern]; storage.writeJSON("ptlaunch.patterns.json", storedPatterns); showMainMenu(); } else { showMainMenu(); } }); } }); }, Settings: () => { var settings = storedPatterns.settings || {}; var settingsmenu = { "": { title: "Pattern Settings", }, "< Back": () => { log("cancel"); load(); }, }; if (settings.lockDisabled) { settingsmenu["Enable lock"] = () => { settings.lockDisabled = false; storedPatterns.settings = settings; Bangle.setOptions({ lockTimeout: 1000 * 30 }); storage.writeJSON("ptlaunch.patterns.json", storedPatterns); showMainMenu(); }; } else { settingsmenu["Disable lock"] = () => { settings.lockDisabled = true; storedPatterns.settings = settings; storage.writeJSON("ptlaunch.patterns.json", storedPatterns); Bangle.setOptions({ lockTimeout: 1000 * 60 * 60 * 24 * 365 }); showMainMenu(); }; } E.showMenu(settingsmenu); }, }; E.showMenu(mainmenu); }; var recognizeAndDrawPattern = () => { return new Promise((resolve) => { E.showMenu(); g.clear(); drawCirclesWithPattern([]); var pattern = []; var isFinished = false; var finishHandler = () => { if (pattern.length === 0 || isFinished) { return; } log("Pattern is finished."); isFinished = true; Bangle.removeListener("drag", dragHandler); Bangle.removeListener("tap", finishHandler); resolve(pattern.join("")); }; setWatch(() => finishHandler(), BTN); // setTimeout(() => Bangle.on("tap", finishHandler), 250); var positions = []; var getPattern = (positions) => { var circles = [ { x: 25, y: 25, i: 0 }, { x: 87, y: 25, i: 1 }, { x: 150, y: 25, i: 2 }, { x: 25, y: 87, i: 3 }, { x: 87, y: 87, i: 4 }, { x: 150, y: 87, i: 5 }, { x: 25, y: 150, i: 6 }, { x: 87, y: 150, i: 7 }, { x: 150, y: 150, i: 8 }, ]; return positions.reduce((pattern, p, i, arr) => { var idx = circles.findIndex((c) => { var dx = p.x > c.x ? p.x - c.x : c.x - p.x; if (dx > CIRCLE_RADIUS) { return false; } var dy = p.y > c.y ? p.y - c.y : c.y - p.y; if (dy > CIRCLE_RADIUS) { return false; } if (dx + dy <= CIRCLE_RADIUS) { return true; } return dx * dx + dy * dy <= CIRCLE_RADIUS_2; }); if (idx >= 0) { pattern += circles[idx].i; circles.splice(idx, 1); } if (circles.length === 0) { arr.splice(1); } return pattern; }, ""); }; var dragHandler = (position) => { positions.push(position); if (position.b === 0 || positions.length >= 200) { pattern = getPattern(positions).split(""); g.clear(); drawCirclesWithPattern(pattern); positions = []; } }; Bangle.on("drag", dragHandler); }); }; var getAppList = () => { var appList = storage .list(/\.info$/) .map((appInfoFileName) => { var appInfo = storage.readJSON(appInfoFileName, 1); return ( appInfo && { name: appInfo.name, // type: appInfo.type, // icon: appInfo.icon, sortorder: appInfo.sortorder, src: appInfo.src, } ); }) .filter((app) => app && !!app.src); appList.sort((a, b) => { var n = (0 | a.sortorder) - (0 | b.sortorder); if (n) return n; // do sortorder first if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; }); return appList; }; var getSelectedApp = () => { E.showMessage("Loading apps..."); return new Promise((resolve) => { var selectAppMenu = { "": { title: "Select App", }, "< Cancel": () => { log("cancel"); showMainMenu(); }, }; var appList = getAppList(); appList.forEach((app) => { selectAppMenu[app.name] = () => { log("app selected"); log(app); resolve(app); }; }); E.showMenu(selectAppMenu); }); }; ////// // manage pattern related variables and functions // - draws all saved patterns and their linked app names // - uses the scroller to allow the user to browse through them ////// var scrollerFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; var drawBackButton = (r) => { g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); g.setFont(scrollerFont) .setFontAlign(-1, 0) .drawString("< Back", 64, r.y + 32); }; var drawAppWithPattern = (i, r, storedPatterns) => { log("draw app with pattern"); log({ i: i, r: r, storedPatterns: storedPatterns }); var storedPattern = storedPatterns[i]; var pattern = storedPattern.pattern; var app = storedPattern.app; g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); g.drawLine(r.x, r.y, 176, r.y); drawCirclesWithPattern(pattern, { enableCaching: true, scale: 0.33, offset: { x: 1, y: 3 + r.y }, }); if (!storedPattern.wrappedAppName) { storedPattern.wrappedAppName = g .wrapString(app.name, g.getWidth() - 64) .join("\n"); } log(g.getWidth()); log(storedPattern.wrappedAppName); g.setFont(scrollerFont) .setFontAlign(-1, 0) .drawString(storedPattern.wrappedAppName, 64, r.y + 32); }; var showScrollerContainingAppsWithPatterns = () => { var storedPatternsArray = getStoredPatternsArray(); log("drawing scroller for stored patterns"); log(storedPatternsArray); log(storedPatternsArray.length); g.clear(); var c = Math.max(storedPatternsArray.length + 1, 3); return new Promise((resolve) => { E.showScroller({ h: 64, c: c, draw: (i, r) => { log("draw"); log({ i: i, r: r }); if (i <= 0) { drawBackButton(r); } else if (i <= storedPatternsArray.length) { drawAppWithPattern(i - 1, r, storedPatternsArray); } }, select: (i) => { log("selected: " + i); var pattern = "back"; var appName = ""; if (i > 0) { var storedPattern = storedPatternsArray[i - 1]; pattern = storedPattern.pattern.join(""); appName = storedPattern.app.name; } clearCircleDrawingCache(); resolve({ pattern: pattern, appName: appName }); }, }); }); }; ////// // storage related functions: // - stored patterns // - stored settings ////// var getStoredPatternsMap = () => { log("loading stored patterns map"); var storedPatternsMap = storage.readJSON("ptlaunch.patterns.json", 1) || {}; delete storedPatternsMap.settings; log(storedPatternsMap); return storedPatternsMap; }; var getStoredPatternsArray = () => { var storedPatternsMap = getStoredPatternsMap(); log("converting stored patterns map to array"); var patterns = Object.keys(storedPatternsMap); var storedPatternsArray = []; for (var i = 0; i < patterns.length; i++) { var pattern = "" + patterns[i]; storedPatternsArray.push({ pattern: pattern .split("") .map((circleIndex) => parseInt(circleIndex, 10)), app: storedPatternsMap[pattern].app, }); } log(storedPatternsArray); return storedPatternsArray; }; ////// // circle related variables and functions: // - the circle array itself // - the radius and the squared radius of the circles // - circle draw function ////// var CIRCLE_RADIUS = 25; var CIRCLE_RADIUS_2 = CIRCLE_RADIUS * CIRCLE_RADIUS; var CIRCLES = [ { x: 25, y: 25, i: 0 }, { x: 87, y: 25, i: 1 }, { x: 150, y: 25, i: 2 }, { x: 25, y: 87, i: 3 }, { x: 87, y: 87, i: 4 }, { x: 150, y: 87, i: 5 }, { x: 25, y: 150, i: 6 }, { x: 87, y: 150, i: 7 }, { x: 150, y: 150, i: 8 }, ]; var drawCircle = (circle, drawBuffer, scale) => { if (!drawBuffer) { drawBuffer = g; } if (!scale) { scale = 1; } var x = circle.x * scale; var y = circle.y * scale; var r = CIRCLE_RADIUS * scale; log("drawing circle"); log({ x: x, y: y, r: r }); drawBuffer.setColor(0); drawBuffer.fillCircle(x, y, r); }; var cachedCirclesDrawings = {}; var clearCircleDrawingCache = () => { cachedCirclesDrawings = {}; }; var drawCirclesWithPattern = (pattern, options) => { if (!pattern || pattern.length === 0) { pattern = []; } if (!options) { options = {}; } var enableCaching = options.enableCaching; var scale = options.scale; var offset = options.offset; if (!enableCaching) { enableCaching = false; } if (!scale) { scale = 1; } if (!offset) { offset = { x: 0, y: 0 }; } log("drawing circles with pattern, scale and offset"); log(pattern); log(scale); log(offset); // cache drawn patterns. especially useful for the manage pattern menu var image = cachedCirclesDrawings[pattern.join("")]; if (!image) { log("circle image not cached"); var drawBuffer = Graphics.createArrayBuffer( g.getWidth() * scale, g.getHeight() * scale, 1, { msb: true } ); drawBuffer.setColor(1); drawBuffer.fillRect(0, 0, drawBuffer.getWidth(), drawBuffer.getHeight()); CIRCLES.forEach((circle) => drawCircle(circle, drawBuffer, scale)); drawBuffer.setColor(1); drawBuffer.setFontAlign(0, 0); drawBuffer.setFont("Vector", 40 * scale); pattern.forEach((circleIndex, patternIndex) => { var circle = CIRCLES[circleIndex]; drawBuffer.drawString( patternIndex + 1, (circle.x + (scale === 1 ? 1 : 5)) * scale, circle.y * scale ); }); image = { width: drawBuffer.getWidth(), height: drawBuffer.getHeight(), bpp: 1, buffer: drawBuffer.buffer, palette: new Uint16Array([g.theme.fg, g.theme.bg], 0, 1), }; if (enableCaching) { cachedCirclesDrawings[pattern.join("")] = image; } } else { log("using cached circle image"); } g.drawImage(image, offset.x, offset.y); }; ////// // misc lib functions ////// var log = (message) => { if (DEBUG) { console.log(JSON.stringify(message)); } }; ////// // run main function ////// showMainMenu();