diff --git a/apps.json b/apps.json index c47d3cf6b..b5ddcbb86 100644 --- a/apps.json +++ b/apps.json @@ -4800,6 +4800,22 @@ "screenshots":[ { "url":"screenshot.png" } ] + }, + { + "id": "ptlaunch", + "name": "Pattern Launcher", + "shortName": "Pattern Launcher", + "version": "0.01", + "description": "Directly launch apps from the clock screen with custom patterns.", + "icon": "app.png", + "tags": "tools", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "ptlaunch.app.js", "url": "app.js" }, + { "name": "ptlaunch.boot.js", "url": "boot.js" }, + { "name": "ptlaunch.img", "url": "app-icon.js", "evaluate": true } + ], + "data": [{"name":"ptlaunch.patterns.json"}] } - ] diff --git a/apps/ptlaunch/ChangeLog b/apps/ptlaunch/ChangeLog new file mode 100644 index 000000000..4967d3207 --- /dev/null +++ b/apps/ptlaunch/ChangeLog @@ -0,0 +1 @@ +0.01: Initial creation of the pattern launch app \ No newline at end of file diff --git a/apps/ptlaunch/README.md b/apps/ptlaunch/README.md new file mode 100644 index 000000000..a69492782 --- /dev/null +++ b/apps/ptlaunch/README.md @@ -0,0 +1,46 @@ +# Pattern Launcher + +Directly launch apps from the clock screen with custom patterns. + +## Usage + +Create patterns and link them to apps in the Pattern Launcher app. + +Then launch the linked apps directly from the clock screen by simply drawing the desired pattern. + +## Screenshots and detailed steps + +![](main_menu.png) +![](add_pattern.png) +![](select_app.png) + +From the main menu you can: +- Add a new pattern and link it to an app (first entry) + - To create a new pattern first select "Add Pattern" + - Now draw any pattern you like, this will later launch the linked app from the clock screen + - If you don't like the pattern, simply re-draw it. The previous pattern will be discarded. + - If you are happy with the pattern tap on screen or press the button to continue + - Now select the app you want to launch with the pattern. + - Note, you can bind multiple patterns to the same app. +- Remove linked patterns (second entry) + - To remove a pattern first select "Remove Pattern" + - You will now see a list of apps that have patterns linked to them + - Simply select the app that you want to unlink. This will remove the saved pattern, but not the app itself! + - Note, that you can not actually preview the patterns. This makes removing patterns that are linked to the same app annoying. sorry! +- Disable the lock screen on the clock screen from the settings (third entry) + - To launch the app from the pattern on the clock screen the watch must be unlocked. + - If this annoys you, you can disable the lock on the clock screen from the setting here + +## FAQ + +1) Nothing happens when I draw on the clock screen! + +Please double-check if you actually have a pattern linked to an app. + +2) I have a pattern linked to an app and still nothing happens when I draw on the clock screen! + +Make sure the watch is unlocked before you start drawing. If this bothers you, you can permanently disable the watch-lock from within the Pattern Launcher app (via the Settings). + +3) I have done all that and still nothing happens! + +Please note that drawing on the clock screen will not visually show the pattern you drew. It will start the app as soon as the pattern was recognized - this might take 1 or 2 seconds! If still nothing happens, that might be a bug, sorry! diff --git a/apps/ptlaunch/add_pattern.png b/apps/ptlaunch/add_pattern.png new file mode 100644 index 000000000..c7cc38e82 Binary files /dev/null and b/apps/ptlaunch/add_pattern.png differ diff --git a/apps/ptlaunch/app-icon.js b/apps/ptlaunch/app-icon.js new file mode 100644 index 000000000..07f025d71 --- /dev/null +++ b/apps/ptlaunch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkE//ziEAiM//4ACmMAgMvA4fziIIBCAUgEAUCBwXxFIYYDCAvyHIgPCGwIgFCA0wAYIRCh49BLQoXB+AHEgYUBgQaCE4JGEHAZGDAAMBAQMjC4UBEw0Aj5PFDIchC4Q/BC5CtIgIXUGwIXDI6EBiCaCCYJ3RIganTa5AnEgbXIewwPGn4ICCA8hgESAoQABmUQgI2CCBQA/AAvzbIRuD/8xNwMTCBTfDPwbYEPAaPDf4LnFB4T/EEAS/Fj7vFZ4LvBgMiFIQXBCAwmEE4Q3BiUikTnHJAQFEJ4XwgERHgI/CJ4oAIC4QYBiYXDCxgXDgUzLQQXIGwpHDLoRHJgJmFO4arCO5MCK4QACh6nCJ4poDCAbGFe4QnEY4IgGG4oOCc4ofCbAj3C/8hiMSAoQYCiMRMQQQKAH4AGkMAJwsyiEBL4wQER4Z+DR5AQFX4ooCX44QGVobvOgMREAUQBwg3B+IXFc4cTmYUBgIXFgImCAAkf/59BkERIgMBBwo/BC5AkDCgwXOAAIMGI5xFBBgR3SJYinXa5A4EfAQQHewoABJAgfCCA/zFAMRn4OC/8xIAIWDCAJGBgIQBA==")) \ No newline at end of file diff --git a/apps/ptlaunch/app.js b/apps/ptlaunch/app.js new file mode 100644 index 000000000..8ba1adf81 --- /dev/null +++ b/apps/ptlaunch/app.js @@ -0,0 +1,416 @@ +var storage = require("Storage"); + +var DEBUG = false; +var log = (message) => { + if (DEBUG) { + console.log(JSON.stringify(message)); + } +}; + +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 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"); + createPattern().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(); + }); + }); + }); + }, + "Remove Pattern": () => { + log("selecting pattern through app"); + getStoredPatternViaApp(storedPatterns).then((pattern) => { + E.showMessage("Deleting..."); + delete storedPatterns[pattern]; + storage.writeJSON("ptlaunch.patterns.json", storedPatterns); + 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 drawCircle = (circle) => { + g.fillCircle(circle.x, circle.y, CIRCLE_RADIUS); +}; + +var positions = []; +var createPattern = () => { + return new Promise((resolve) => { + E.showMenu(); + g.clear(); + g.setColor(0, 0, 0); + CIRCLES.forEach((circle) => drawCircle(circle)); + + 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 dragHandler = (position) => { + positions.push(position); + + debounce().then(() => { + if (isFinished) { + return; + } + E.showMessage("Calculating..."); + var t0 = Date.now(); + + log(positions.length); + + var circlesClone = cloneCirclesArray(); + pattern = []; + + var step = Math.floor(positions.length / 100) + 1; + + var p, a, b, circle; + + for (var i = 0; i < positions.length; i += step) { + p = positions[i]; + + circle = circlesClone[0]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(0, 1); + } + } + + circle = circlesClone[1]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(1, 1); + } + } + + circle = circlesClone[2]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(2, 1); + } + } + + circle = circlesClone[3]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(3, 1); + } + } + + circle = circlesClone[4]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(4, 1); + } + } + + circle = circlesClone[5]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(5, 1); + } + } + + circle = circlesClone[6]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(6, 1); + } + } + circle = circlesClone[7]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(7, 1); + } + } + + circle = circlesClone[8]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(8, 1); + } + } + } + var tx = Date.now(); + log(tx - t0); + positions = []; + var t1 = Date.now(); + log(t1 - t0); + + log("pattern:"); + log(pattern); + + log("redrawing"); + g.clear(); + g.setColor(0, 0, 0); + CIRCLES.forEach((circle) => drawCircle(circle)); + + g.setColor(1, 1, 1); + g.setFontAlign(0, 0); + g.setFont("6x8", 4); + pattern.forEach((circleIndex, patternIndex) => { + var circle = CIRCLES[circleIndex]; + g.drawString(patternIndex + 1, circle.x, circle.y); + }); + var t2 = Date.now(); + log(t2 - t0); + }); + }; + + 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); + }); +}; + +var getStoredPatternViaApp = (storedPatterns) => { + E.showMessage("Loading patterns..."); + log("getStoredPatternViaApp"); + return new Promise((resolve) => { + var selectPatternMenu = { + "": { + title: "Select App", + }, + "< Cancel": () => { + log("cancel"); + showMainMenu(); + }, + }; + + log(storedPatterns); + var patterns = Object.keys(storedPatterns); + log(patterns); + + patterns.forEach((pattern) => { + if (pattern) { + if (storedPatterns[pattern]) { + var app = storedPatterns[pattern].app; + if (!!app && !!app.name) { + var appName = app.name; + var i = 0; + while (appName in selectPatternMenu[app.name]) { + appName = app.name + i; + i++; + } + selectPatternMenu[appName] = () => { + log("pattern via app selected"); + log(pattern); + log(app); + resolve(pattern); + }; + } + } + } + }); + + E.showMenu(selectPatternMenu); + }); +}; + +showMainMenu(); + +////// +// lib functions +////// + +var debounceTimeoutId; +var debounce = (delay) => { + if (debounceTimeoutId) { + clearTimeout(debounceTimeoutId); + } + + return new Promise((resolve) => { + debounceTimeoutId = setTimeout(() => { + debounceTimeoutId = undefined; + resolve(); + }, delay || 500); + }); +}; + +var cloneCirclesArray = () => { + var circlesClone = Array(CIRCLES.length); + + for (var i = 0; i < CIRCLES.length; i++) { + circlesClone[i] = CIRCLES[i]; + } + + return circlesClone; +}; diff --git a/apps/ptlaunch/app.png b/apps/ptlaunch/app.png new file mode 100644 index 000000000..14ed77f1d Binary files /dev/null and b/apps/ptlaunch/app.png differ diff --git a/apps/ptlaunch/boot.js b/apps/ptlaunch/boot.js new file mode 100644 index 000000000..1433f1700 --- /dev/null +++ b/apps/ptlaunch/boot.js @@ -0,0 +1,196 @@ +var DEBUG = true; +var log = (message) => { + if (DEBUG) { + console.log(JSON.stringify(message)); + } +}; + +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 storedPatterns; +var positions = []; +var dragHandler = (position) => { + positions.push(position); + + debounce().then(() => { + log(positions.length); + + var circlesClone = cloneCirclesArray(); + var pattern = []; + + var step = Math.floor(positions.length / 100) + 1; + + var p, a, b, circle; + + for (var i = 0; i < positions.length; i += step) { + p = positions[i]; + + circle = circlesClone[0]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(0, 1); + } + } + + circle = circlesClone[1]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(1, 1); + } + } + + circle = circlesClone[2]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(2, 1); + } + } + + circle = circlesClone[3]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(3, 1); + } + } + + circle = circlesClone[4]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(4, 1); + } + } + + circle = circlesClone[5]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(5, 1); + } + } + + circle = circlesClone[6]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(6, 1); + } + } + circle = circlesClone[7]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(7, 1); + } + } + + circle = circlesClone[8]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(8, 1); + } + } + } + positions = []; + + pattern = pattern.join(""); + + if (pattern) { + if (storedPatterns[pattern]) { + var app = storedPatterns[pattern].app; + if (!!app && !!app.src) { + Bangle.removeListener("drag", dragHandler); + load(app.src); + } + } + } + }); +}; + +var debounceTimeoutId; +var debounce = (delay) => { + if (debounceTimeoutId) { + clearTimeout(debounceTimeoutId); + } + + return new Promise((resolve) => { + debounceTimeoutId = setTimeout(() => { + debounceTimeoutId = undefined; + resolve(); + }, delay || 500); + }); +}; + +var cloneCirclesArray = () => { + var circlesClone = Array(CIRCLES.length); + + for (var i = 0; i < CIRCLES.length; i++) { + circlesClone[i] = CIRCLES[i]; + } + + return circlesClone; +}; + +(function () { + var sui = Bangle.setUI; + Bangle.setUI = function (mode, cb) { + sui(mode, cb); + if (!mode) { + Bangle.removeListener("drag", dragHandler); + storedPatterns = {}; + return; + } + if (!mode.startsWith("clock")) { + storedPatterns = {}; + Bangle.removeListener("drag", dragHandler); + return; + } + + var storage = require("Storage"); + storedPatterns = storage.readJSON("ptlaunch.patterns.json", 1) || {}; + if (Object.keys(storedPatterns).length > 0) { + Bangle.on("drag", dragHandler); + if (storedPatterns.settings) { + if (storedPatterns.settings.lockDisabled) { + Bangle.setOptions({ lockTimeout: 1000 * 60 * 60 * 24 * 365 }); + } + } + } + }; +})(); diff --git a/apps/ptlaunch/main_menu.png b/apps/ptlaunch/main_menu.png new file mode 100644 index 000000000..a4ecebb0f Binary files /dev/null and b/apps/ptlaunch/main_menu.png differ diff --git a/apps/ptlaunch/select_app.png b/apps/ptlaunch/select_app.png new file mode 100644 index 000000000..56f0dfc83 Binary files /dev/null and b/apps/ptlaunch/select_app.png differ