From 051b4179715a0cf62f27aa86690c3507c3fd8533 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 10 May 2023 21:56:59 +0100 Subject: [PATCH 01/14] widhid: don't check clkinfo focus on drag ... because we've already disabled those handlers --- apps/widhid/wid.js | 2 -- apps/widhid/wid.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/apps/widhid/wid.js b/apps/widhid/wid.js index ed1e78e76..92a1e2483 100644 --- a/apps/widhid/wid.js +++ b/apps/widhid/wid.js @@ -19,8 +19,6 @@ } }); var onDrag = (function (e) { - if (Bangle.CLKINFO_FOCUS) - return; if (e.b === 0) { var wasDragging = dragging; dragging = false; diff --git a/apps/widhid/wid.ts b/apps/widhid/wid.ts index 6b5e38855..a02e4f1aa 100644 --- a/apps/widhid/wid.ts +++ b/apps/widhid/wid.ts @@ -23,8 +23,6 @@ }) satisfies SwipeCallback; const onDrag = (e => { - if((Bangle as BangleExt).CLKINFO_FOCUS) return; - if(e.b === 0){ // released const wasDragging = dragging; From a9affac57ecdb99f73e260bd73f505e8d55ad812 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 10 May 2023 21:57:33 +0100 Subject: [PATCH 02/14] widhid: disable (temporarily) if a menu's shown --- apps/widhid/wid.js | 14 ++++++++++++++ apps/widhid/wid.ts | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/apps/widhid/wid.js b/apps/widhid/wid.js index 92a1e2483..38af50995 100644 --- a/apps/widhid/wid.js +++ b/apps/widhid/wid.js @@ -10,9 +10,23 @@ var dragging = false; var activeTimeout; var waitForRelease = true; + var menuShown = 0; + var origShowMenu = E.showMenu; + E.showMenu = (function (menu) { + menuShown++; + var origSetUI = Bangle.setUI; + Bangle.setUI = (function (mode, cb) { + menuShown--; + Bangle.setUI = origSetUI; + return origSetUI(mode, cb); + }); + return origShowMenu(menu); + }); var onSwipe = (function (_lr, ud) { if (Bangle.CLKINFO_FOCUS) return; + if (menuShown) + return; if (!activeTimeout && ud > 0) { listen(); Bangle.buzz(20); diff --git a/apps/widhid/wid.ts b/apps/widhid/wid.ts index a02e4f1aa..bd031c1cc 100644 --- a/apps/widhid/wid.ts +++ b/apps/widhid/wid.ts @@ -12,9 +12,33 @@ let dragging = false; let activeTimeout: number | undefined; let waitForRelease = true; + let menuShown = 0; + + // If the user shows a menu, we want to temporarily disable ourselves + // We can detect showing of a menu by overriding E.showMenu + // to detect hiding of a menu, we hook setUI, since all menus + // either show other menus, load() or (eventually) call it + // (I hope) + // + // Alternatively we could watch for when Bangle.dragHandler and + // Bangle.swipeHandler get removed from Bangle["#on"] + const origShowMenu = E.showMenu; + E.showMenu = ((menu: Menu): MenuInstance => { + menuShown++; + + const origSetUI = Bangle.setUI; + Bangle.setUI = ((mode: unknown, cb: () => void) => { + menuShown--; + Bangle.setUI = origSetUI; + return origSetUI(mode as any, cb); + }) as any; + + return origShowMenu(menu); + }) as any; const onSwipe = ((_lr, ud) => { if((Bangle as BangleExt).CLKINFO_FOCUS) return; + if(menuShown) return; if(!activeTimeout && ud! > 0){ listen(); From 9a3ac3bc7a7d08a75905d92a788ed918dd8a9054 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 10 May 2023 23:13:19 +0100 Subject: [PATCH 03/14] widhid: better clash detection --- apps/widhid/wid.js | 47 ++++++++++++++++++++--------------- apps/widhid/wid.ts | 62 +++++++++++++++++++++++++--------------------- 2 files changed, 61 insertions(+), 48 deletions(-) diff --git a/apps/widhid/wid.js b/apps/widhid/wid.js index 38af50995..b361b6453 100644 --- a/apps/widhid/wid.js +++ b/apps/widhid/wid.js @@ -10,24 +10,29 @@ var dragging = false; var activeTimeout; var waitForRelease = true; - var menuShown = 0; - var origShowMenu = E.showMenu; - E.showMenu = (function (menu) { - menuShown++; - var origSetUI = Bangle.setUI; - Bangle.setUI = (function (mode, cb) { - menuShown--; - Bangle.setUI = origSetUI; - return origSetUI(mode, cb); - }); - return origShowMenu(menu); - }); - var onSwipe = (function (_lr, ud) { + var mayInterceptSwipe = function () { if (Bangle.CLKINFO_FOCUS) - return; - if (menuShown) - return; - if (!activeTimeout && ud > 0) { + return 0; + if (Bangle.CLOCK) + return 1; + var swipes = Bangle["#onswipe"]; + if (typeof swipes === "function") { + if (swipes !== onSwipe) + return swipes.length > 1; + } + else if (swipes) { + for (var _i = 0, swipes_1 = swipes; _i < swipes_1.length; _i++) { + var handler = swipes_1[_i]; + if (handler !== onSwipe && (handler === null || handler === void 0 ? void 0 : handler.length) > 1) + return 0; + } + } + if (Bangle["#ondrag"]) + return 0; + return 1; + }; + var onSwipe = (function (_lr, ud) { + if (ud > 0 && !activeTimeout && mayInterceptSwipe()) { listen(); Bangle.buzz(20); } @@ -153,13 +158,14 @@ stroke: null, }; var suspendOthers = function () { - for (var event in touchEvents) { + for (var event_ in touchEvents) { + var event = event_; var handlers = Bangle["#on".concat(event)]; if (!handlers) continue; var newEvents = void 0; if (handlers instanceof Array) - newEvents = handlers.slice(); + newEvents = handlers.filter(function (f) { return f; }); else newEvents = [handlers]; for (var _i = 0, newEvents_1 = newEvents; _i < newEvents_1.length; _i++) { @@ -170,7 +176,8 @@ } }; var resumeOthers = function () { - for (var event in touchEvents) { + for (var event_ in touchEvents) { + var event = event_; var handlers = touchEvents[event]; touchEvents[event] = null; if (handlers) diff --git a/apps/widhid/wid.ts b/apps/widhid/wid.ts index bd031c1cc..a85725569 100644 --- a/apps/widhid/wid.ts +++ b/apps/widhid/wid.ts @@ -1,4 +1,9 @@ (() => { + type BangleEventKeys = "tap" | "gesture" | "aiGesture" | "swipe" | "touch" | "drag" | "stroke"; + type BangleEvents = { + [key in BangleEventKeys as `#on${key}`]?: Handler | (Handler | undefined)[] + }; + const settings: Settings = require("Storage").readJSON("setting.json", true) || { HID: false } as Settings; if (settings.HID !== "kbmedia") { console.log("widhid: can't enable, HID setting isn't \"kbmedia\""); @@ -12,35 +17,35 @@ let dragging = false; let activeTimeout: number | undefined; let waitForRelease = true; - let menuShown = 0; // If the user shows a menu, we want to temporarily disable ourselves - // We can detect showing of a menu by overriding E.showMenu - // to detect hiding of a menu, we hook setUI, since all menus - // either show other menus, load() or (eventually) call it - // (I hope) // - // Alternatively we could watch for when Bangle.dragHandler and - // Bangle.swipeHandler get removed from Bangle["#on"] - const origShowMenu = E.showMenu; - E.showMenu = ((menu: Menu): MenuInstance => { - menuShown++; + // We could detect showing of a menu by overriding E.showMenu + // and to detect hiding of a menu, we hook setUI + // + // Perhaps easier to check Bangle.swipeHandler - set by setUI, + // called by E.showMenu + const mayInterceptSwipe = () => { + if((Bangle as BangleExt).CLKINFO_FOCUS) return 0; + if(Bangle.CLOCK) return 1; - const origSetUI = Bangle.setUI; - Bangle.setUI = ((mode: unknown, cb: () => void) => { - menuShown--; - Bangle.setUI = origSetUI; - return origSetUI(mode as any, cb); - }) as any; + const swipes = (Bangle as BangleEvents)["#onswipe"]; + if(typeof swipes === "function"){ + if(swipes !== onSwipe) + return swipes.length > 1; // second argument is up/down + }else if(swipes){ + for(const handler of swipes) + if(handler !== onSwipe && handler?.length > 1) + return 0; + } - return origShowMenu(menu); - }) as any; + if((Bangle as BangleEvents)["#ondrag"]) return 0; + return 1; + }; const onSwipe = ((_lr, ud) => { - if((Bangle as BangleExt).CLKINFO_FOCUS) return; - if(menuShown) return; - - if(!activeTimeout && ud! > 0){ + // do these checks in order of cheapness + if(ud! > 0 && !activeTimeout && mayInterceptSwipe()){ listen(); Bangle.buzz(20); } @@ -174,7 +179,7 @@ // disable event handlers type Handler = () => void; const touchEvents: { - [key: string]: null | Handler[] + [key in BangleEventKeys]: null | Handler[] } = { tap: null, gesture: null, @@ -186,15 +191,15 @@ }; const suspendOthers = () => { - for(const event in touchEvents){ - const handlers: Handler[] | Handler | undefined - = (Bangle as any)[`#on${event}`]; + for(const event_ in touchEvents){ + const event = event_ as BangleEventKeys; + const handlers = (Bangle as BangleEvents)[`#on${event}`]; if(!handlers) continue; let newEvents; if(handlers instanceof Array) - newEvents = handlers.slice(); + newEvents = handlers.filter(f=>f) as Handler[]; else newEvents = [handlers /* single fn */]; @@ -205,7 +210,8 @@ } }; const resumeOthers = () => { - for(const event in touchEvents){ + for(const event_ in touchEvents){ + const event = event_ as BangleEventKeys; const handlers = touchEvents[event]; touchEvents[event] = null; From b12fa582158d478e3949506cfb93d1c72b8cc94f Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 10 May 2023 23:38:43 +0100 Subject: [PATCH 04/14] bump version --- apps/widhid/ChangeLog | 3 +++ apps/widhid/metadata.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 apps/widhid/ChangeLog diff --git a/apps/widhid/ChangeLog b/apps/widhid/ChangeLog new file mode 100644 index 000000000..a6ed84766 --- /dev/null +++ b/apps/widhid/ChangeLog @@ -0,0 +1,3 @@ +0.01: New widget - music control via a swipe +0.02: Improve interactivity - avoid responding to swipes when a menu or + launcher is active. diff --git a/apps/widhid/metadata.json b/apps/widhid/metadata.json index 10e75fadc..84334636b 100644 --- a/apps/widhid/metadata.json +++ b/apps/widhid/metadata.json @@ -2,7 +2,7 @@ "id": "widhid", "name": "Bluetooth Music Swipe Control Widget", "shortName": "BLE Swipe Widget", - "version": "0.01", + "version": "0.02", "description": "Based on Swipe Bluetooth Music Controls (based on Bluetooth Music Controls). Swipe down to enable, then swipe up/down for volume, left/right for previous and next and tap for play/pause. Enable HID in settings, pair with your phone/computer, then use this widget to control music from your watch!", "icon": "icon.png", "readme": "README.md", From be872e62dda52d0d10066787b547c1be8e357624 Mon Sep 17 00:00:00 2001 From: Bruce Blore Date: Fri, 12 May 2023 15:52:45 -0700 Subject: [PATCH 05/14] New app: folderlaunch: Folder-based launcher --- apps/folderlaunch/ChangeLog | 1 + apps/folderlaunch/README.md | 38 ++++ apps/folderlaunch/app.js | 235 +++++++++++++++++++++++ apps/folderlaunch/app.ts | 299 ++++++++++++++++++++++++++++++ apps/folderlaunch/configLoad.js | 107 +++++++++++ apps/folderlaunch/configLoad.ts | 158 ++++++++++++++++ apps/folderlaunch/icon.js | 1 + apps/folderlaunch/icon.png | Bin 0 -> 234 bytes apps/folderlaunch/metadata.json | 48 +++++ apps/folderlaunch/screenshot1.png | Bin 0 -> 4832 bytes apps/folderlaunch/screenshot2.png | Bin 0 -> 3849 bytes apps/folderlaunch/settings.js | 257 +++++++++++++++++++++++++ apps/folderlaunch/settings.ts | 272 +++++++++++++++++++++++++++ apps/folderlaunch/types.d.ts | 48 +++++ 14 files changed, 1464 insertions(+) create mode 100644 apps/folderlaunch/ChangeLog create mode 100644 apps/folderlaunch/README.md create mode 100644 apps/folderlaunch/app.js create mode 100644 apps/folderlaunch/app.ts create mode 100644 apps/folderlaunch/configLoad.js create mode 100644 apps/folderlaunch/configLoad.ts create mode 100644 apps/folderlaunch/icon.js create mode 100644 apps/folderlaunch/icon.png create mode 100644 apps/folderlaunch/metadata.json create mode 100644 apps/folderlaunch/screenshot1.png create mode 100644 apps/folderlaunch/screenshot2.png create mode 100644 apps/folderlaunch/settings.js create mode 100644 apps/folderlaunch/settings.ts create mode 100644 apps/folderlaunch/types.d.ts diff --git a/apps/folderlaunch/ChangeLog b/apps/folderlaunch/ChangeLog new file mode 100644 index 000000000..759f68777 --- /dev/null +++ b/apps/folderlaunch/ChangeLog @@ -0,0 +1 @@ +0.01: New app! \ No newline at end of file diff --git a/apps/folderlaunch/README.md b/apps/folderlaunch/README.md new file mode 100644 index 000000000..bef4ebc78 --- /dev/null +++ b/apps/folderlaunch/README.md @@ -0,0 +1,38 @@ +# Folder launcher + +Launcher that allows you to put your apps into folders + +![](screenshot1.png) +![](screenshot2.png) + +## Launcher UI + +The apps and folders will be presented in a grid layout, configurable in size. Tapping on a folder will open the folder. Folders can contain both apps and more folders. Tapping on an app will launch the app. If there is more than one page, there will be a scroll bar on the right side to indicate how far through the list you have scrolled. Folders will be displayed before apps, in the order that they were added. Apps will honor their sort order, if it exists. + +Swiping up and down will scroll. Swiping from the left, using the back button, or pressing BTN1 will take you up a level to the folder containing the current one, or exit the launcher if you are at the top level. + +The first time you launch an app, you will be asked if you want to fast load it, if the feature is enabled. Fast loading saves time by skipping much of the initialization between apps. Instead, the launcher just undoes the changes it made to the watch's state. However, fast loading can only be done between two apps that support widgets, so the decision to fast load has to be made for each app. The options are "Yes", "Not now", and "Never", so choose "Yes" if the app uses widgets, "Not now" if you don't remember, and "Never" if you know for sure it doesn't use widgets. + +## Settings menu + +* Show clocks / Show launcher: Whether clock and launcher apps are displayed in the UI to be launched. The default is no. + +* Hidden apps: Displays the list of installed apps, enabling them to be manually hidden. (Or unhidden, if hidden from here.) This may be convenient for apps that you have some other shortcut to access, or apps that are only shortcuts to an infrequently used settings menu. By default, no apps are hidden. + +* Display: + * Rows: The side length of the square grid. Lowest value is 1, no upper limit. The default is 2, but 3 is also convenient. + * Show icons?: Whether app and folder icons are displayed. The default is yes. + * Font size: How much height of each grid cell to allocate for the app or folder name. If size zero is selected, there will be no title for apps and folders will use a size of 12. (This is important because it is not possible to distinguish folders solely by icon.) The default is 12. + + To prevent the launcher from becoming unusable, if neither icons nor text are enabled in the settings menu, text will still be drawn. + +* Prompt for fast launch: If yes, when launching an app that does not yet have a setting saved, ask whether it should be fast loaded. If no, already saved settings are still applied, but apps that have not been assigned a setting will be slow loaded. The default is yes. + +* Timeout: If the launcher is left idle for too long, return to the clock. This is convenient if you often accidentally open the launcher without noticing. At zero seconds, the timeout is disabled. The default is 30 seconds. + +* Folder management: Open the folder management menu for the root folder. (The folder first displayed when opening the launcher.) The folder management menu contains the following: + * New subfolder: Open the keyboard to enter the name for a new subfolder to be created. If left blank or given the name of an existing subfolder, no folder is created. If a subfolder is created, open the folder management menu for the new folder. + * Move app here: Display a list of apps. Selecting one moves it into the folder. + * One menu entry for each subfolder, which opens the folder management menu for that subfolder. + * View apps: Only present if this folder contains apps, Display a menu of all apps in the folder. This is for information only, tapping the apps does nothing. + * Delete folder: Only present if not viewing the root folder. Delete the current folder and move all apps into the parent folder. \ No newline at end of file diff --git a/apps/folderlaunch/app.js b/apps/folderlaunch/app.js new file mode 100644 index 000000000..5d1622825 --- /dev/null +++ b/apps/folderlaunch/app.js @@ -0,0 +1,235 @@ +{ + var loader_1 = require('folderlaunch-configLoad.js'); + var storage_1 = require('Storage'); + var FOLDER_ICON_1 = require("heatshrink").decompress(atob("mEwwMA///wAJCAoPAAongAonwAon4Aon8Aon+Aon/AooA/AH4A/AFgA=")); + var config_1 = loader_1.getConfig(); + var timeout_1; + var resetTimeout_1 = function () { + if (timeout_1) { + clearTimeout(timeout_1); + } + if (config_1.timeout != 0) { + timeout_1 = setTimeout(function () { + Bangle.showClock(); + }, config_1.timeout); + } + }; + var folderPath_1 = []; + var getFolder_1 = function (folderPath) { + var result = config_1.rootFolder; + for (var _i = 0, folderPath_2 = folderPath; _i < folderPath_2.length; _i++) { + var folderName = folderPath_2[_i]; + result = result.folders[folderName]; + } + nPages_1 = Math.ceil((result.apps.length + Object.keys(result.folders).length) / (config_1.display.rows * config_1.display.rows)); + return result; + }; + var folder_1 = getFolder_1(folderPath_1); + var getFontSize_1 = function (length, maxWidth, minSize, maxSize) { + var size = Math.floor(maxWidth / length); + size *= (20 / 12); + if (size < minSize) + return minSize; + else if (size > maxSize) + return maxSize; + else + return Math.floor(size); + }; + var grid_1 = []; + for (var x = 0; x < config_1.display.rows; x++) { + grid_1.push([]); + for (var y = 0; y < config_1.display.rows; y++) { + grid_1[x].push({ + type: 'empty', + id: '' + }); + } + } + var render_1 = function () { + var gridSize = config_1.display.rows * config_1.display.rows; + var startIndex = page_1 * gridSize; + for (var i = 0; i < gridSize; i++) { + var y = Math.floor(i / config_1.display.rows); + var x = i % config_1.display.rows; + var folderIndex = startIndex + i; + var appIndex = folderIndex - Object.keys(folder_1.folders).length; + if (folderIndex < Object.keys(folder_1.folders).length) { + grid_1[x][y].type = 'folder'; + grid_1[x][y].id = Object.keys(folder_1.folders)[folderIndex]; + } + else if (appIndex < folder_1.apps.length) { + grid_1[x][y].type = 'app'; + grid_1[x][y].id = folder_1.apps[appIndex]; + } + else + grid_1[x][y].type = 'empty'; + } + var squareSize = (g.getHeight() - 24) / config_1.display.rows; + if (!config_1.display.icon && !config_1.display.font) + config_1.display.font = 12; + g.clearRect(0, 24, g.getWidth(), g.getHeight()) + .reset() + .setFontAlign(0, -1); + var empty = true; + for (var x = 0; x < config_1.display.rows; x++) { + for (var y = 0; y < config_1.display.rows; y++) { + var entry = grid_1[x][y]; + var icon = void 0; + var text = void 0; + var fontSize = void 0; + switch (entry.type) { + case 'app': + var app_1 = storage_1.readJSON(entry.id + '.info', false); + icon = storage_1.read(app_1.icon); + text = app_1.name; + empty = false; + fontSize = config_1.display.font; + break; + case 'folder': + icon = FOLDER_ICON_1; + text = entry.id; + empty = false; + fontSize = config_1.display.font ? config_1.display.font : 12; + break; + default: + continue; + } + var iconSize = config_1.display.icon ? Math.max(0, squareSize - fontSize) : 0; + var iconScale = iconSize / 48; + var posX = 12 + (x * squareSize); + var posY = 24 + (y * squareSize); + if (config_1.display.icon && iconSize != 0) + try { + g.drawImage(icon, posX + (squareSize - iconSize) / 2, posY, { scale: iconScale }); + } + catch (error) { + console.log("Failed to draw icon for ".concat(text, ": ").concat(error)); + console.log(icon); + } + if (fontSize) + g.setFont('Vector', getFontSize_1(text.length, squareSize, 6, squareSize - iconSize)) + .drawString(text, posX + (squareSize / 2), posY + iconSize); + } + } + if (empty) + E.showMessage('Folder is empty. Swipe left, back button, or BTN1 to go back.'); + if (nPages_1 > 1) { + var barSize = (g.getHeight() - 24) / nPages_1; + var barTop = 24 + (page_1 * barSize); + g.fillRect(g.getWidth() - 8, barTop, g.getWidth() - 4, barTop + barSize); + } + }; + var onTouch = function (_button, xy) { + var x = Math.floor((xy.x - 12) / ((g.getWidth() - 24) / config_1.display.rows)); + if (x < 0) + x = 0; + else if (x >= config_1.display.rows) + x = config_1.display.rows - 1; + var y = Math.floor((xy.y - 24) / ((g.getHeight() - 24) / config_1.display.rows)); + if (y < 0) + y = 0; + else if (y >= config_1.display.rows) + y = config_1.display.rows - 1; + var entry = grid_1[x][y]; + switch (entry.type) { + case "app": + Bangle.buzz(); + var app_2 = config_1.apps[entry.id]; + var infoFile_1 = storage_1.readJSON(entry.id + '.info', false); + if (app_2.fast) + Bangle.load(infoFile_1.src); + else if (config_1.fastNag && !app_2.nagged) + E.showPrompt('Would you like to fast load?', { + title: infoFile_1.name, + buttons: { + "Yes": 0, + "Not now": 1, + "Never": 2 + } + }).then(function (value) { + switch (value) { + case 0: + app_2.nagged = true; + app_2.fast = true; + loader_1.cleanAndSave(config_1); + Bangle.load(infoFile_1.src); + break; + case 1: + load(infoFile_1.src); + break; + default: + app_2.nagged = true; + loader_1.cleanAndSave(config_1); + load(infoFile_1.src); + break; + } + }); + else + load(infoFile_1.src); + break; + case "folder": + Bangle.buzz(); + resetTimeout_1(); + page_1 = 0; + folderPath_1.push(entry.id); + folder_1 = getFolder_1(folderPath_1); + render_1(); + break; + default: + resetTimeout_1(); + break; + } + }; + var page_1 = 0; + var nPages_1; + var onSwipe = function (lr, ud) { + if (lr == 1 && ud == 0) { + onBackButton_1(); + return; + } + else if (ud == 1) { + resetTimeout_1(); + if (page_1 == 0) { + Bangle.buzz(200); + return; + } + else + page_1--; + } + else if (ud == -1) { + resetTimeout_1(); + if (page_1 == nPages_1 - 1) { + Bangle.buzz(200); + return; + } + else + page_1++; + } + render_1(); + }; + var onBackButton_1 = function () { + Bangle.buzz(); + if (folderPath_1.length == 0) + Bangle.showClock(); + else { + folderPath_1.pop(); + folder_1 = getFolder_1(folderPath_1); + resetTimeout_1(); + page_1 = 0; + render_1(); + } + }; + Bangle.loadWidgets(); + Bangle.drawWidgets(); + Bangle.setUI({ + mode: 'custom', + back: onBackButton_1, + btn: onBackButton_1, + swipe: onSwipe, + touch: onTouch, + remove: function () { if (timeout_1) + clearTimeout(timeout_1); } + }); + resetTimeout_1(); + render_1(); +} diff --git a/apps/folderlaunch/app.ts b/apps/folderlaunch/app.ts new file mode 100644 index 000000000..ef62687d7 --- /dev/null +++ b/apps/folderlaunch/app.ts @@ -0,0 +1,299 @@ +{ + const loader = require('folderlaunch-configLoad.js') + const storage = require('Storage') + + const FOLDER_ICON = require("heatshrink").decompress(atob("mEwwMA///wAJCAoPAAongAonwAon4Aon8Aon+Aon/AooA/AH4A/AFgA=")) + + let config: Config = loader.getConfig(); + + let timeout: any; + /** + * If a timeout to return to the clock is set, reset it. + */ + let resetTimeout = function () { + if (timeout) { + clearTimeout(timeout); + } + + if (config.timeout != 0) { + timeout = setTimeout(() => { + Bangle.showClock(); + }, config.timeout); + } + } + + let folderPath: Array = []; + /** + * Get the folder at the provided path + * + * @param folderPath a path for the desired folder + * @return the folder that was found + */ + let getFolder = function (folderPath: Array): Folder { + let result: Folder = config.rootFolder; + for (let folderName of folderPath) + result = result.folders[folderName]!; + nPages = Math.ceil((result.apps.length + Object.keys(result.folders).length) / (config.display.rows * config.display.rows)); + return result; + } + let folder: Folder = getFolder(folderPath); + + /** + * Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize + * + * @param length the number of characters of the string + * @param maxWidth the maximum allowable width + * @param minSize the minimum acceptable font size + * @param maxSize the maximum acceptable font size + * @return the calculated font size + */ + let getFontSize = function (length: number, maxWidth: number, minSize: number, maxSize: number): number { + let size = Math.floor(maxWidth / length); //Number of pixels of width available to character + size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width + + // Clamp to within range + if (size < minSize) return minSize; + else if (size > maxSize) return maxSize; + else return Math.floor(size); + } + + // grid[x][y] = id of app at column x row y, or undefined if no app displayed there + let grid: Array> = []; + for (let x = 0; x < config.display.rows; x++) { + grid.push([]); + for (let y = 0; y < config.display.rows; y++) { + grid[x]!.push({ + type: 'empty', + id: '' + }); + } + } + let render = function () { + let gridSize: number = config.display.rows * config.display.rows; + let startIndex: number = page * gridSize; // Start at this position in the folders + + // Populate the grid + for (let i = 0; i < gridSize; i++) { + // Calculate coordinates + let y = Math.floor(i / config.display.rows); + let x = i % config.display.rows; + + // Try to place a folder + let folderIndex = startIndex + i; + let appIndex = folderIndex - Object.keys(folder.folders).length; + if (folderIndex < Object.keys(folder.folders).length) { + grid[x]![y]!.type = 'folder'; + grid[x]![y]!.id = Object.keys(folder.folders)[folderIndex]; + } + + // If that fails, try to place an app + else if (appIndex < folder.apps.length) { + grid[x]![y]!.type = 'app'; + grid[x]![y]!.id = folder.apps[appIndex]!; + } + + // If that also fails, make the space empty + else grid[x]![y]!.type = 'empty'; + } + + // Prepare to draw the grid + let squareSize: number = (g.getHeight() - 24) / config.display.rows; + if (!config.display.icon && !config.display.font) config.display.font = 12; // Fallback in case user disabled both icon and text + g.clearRect(0, 24, g.getWidth(), g.getHeight()) + .reset() + .setFontAlign(0, -1); + + // Actually draw the grid + let empty = true; // Set to empty upon drawing something, so we can know whether to draw a nice message rather than leaving the screen completely blank + for (let x = 0; x < config.display.rows; x++) { + for (let y = 0; y < config.display.rows; y++) { + let entry: GridEntry = grid[x]![y]!; + let icon: string | ArrayBuffer; + let text: string; + let fontSize: number; + + // Get the icon and text, skip if the space is empty. Always draw text for folders even if disabled + switch (entry.type) { + case 'app': + let app: AppInfoFile = storage.readJSON(entry.id + '.info', false); + icon = storage.read(app.icon)!; + text = app.name; + empty = false; + fontSize = config.display.font; + break; + case 'folder': + icon = FOLDER_ICON; + text = entry.id; + empty = false; + fontSize = config.display.font ? config.display.font : 12; + break; + default: + continue; + } + + // Calculate position and icon size + let iconSize = config.display.icon ? Math.max(0, squareSize - fontSize) : 0; // If icon is disabled, stay at zero. Otherwise, subtract font size from square + let iconScale: number = iconSize / 48; + let posX = 12 + (x * squareSize); + let posY = 24 + (y * squareSize); + + // Draw the icon + if (config.display.icon && iconSize != 0) + try { + g.drawImage(icon, posX + (squareSize - iconSize) / 2, posY, { scale: iconScale }); + } catch (error) { + console.log(`Failed to draw icon for ${text}: ${error}`); + console.log(icon); + } + + // Draw the text + if (fontSize) + g.setFont('Vector', getFontSize(text.length, squareSize, 6, squareSize - iconSize)) + .drawString(text, posX + (squareSize / 2), posY + iconSize); + } + } + + // Draw a nice message if there is nothing to see, so the user doesn't think the app is broken + if (empty) E.showMessage(/*LANG*/'Folder is empty. Swipe from left, back button, or BTN1 to go back.'); + + // Draw a scroll bar if necessary + if (nPages > 1) { // Avoid divide-by-zero and pointless scroll bars + let barSize = (g.getHeight() - 24) / nPages; + let barTop = 24 + (page * barSize); + g.fillRect( + g.getWidth() - 8, barTop, + g.getWidth() - 4, barTop + barSize); + } + } + + /** + * Handle a touch + * + * @param _button 1 for left half, 2 for right half + * @param xy postion on screen + */ + let onTouch = function (_button: number, xy: { x: number, y: number } | undefined) { + // Determine which grid cell was tapped + let x: number = Math.floor((xy!.x - 12) / ((g.getWidth() - 24) / config.display.rows)); + if (x < 0) x = 0; + else if (x >= config.display.rows) x = config.display.rows - 1; + let y: number = Math.floor((xy!.y - 24) / ((g.getHeight() - 24) / config.display.rows)); + if (y < 0) y = 0; + else if (y >= config.display.rows) y = config.display.rows - 1; + + // Handle the grid cell + let entry: GridEntry = grid[x]![y]!; + switch (entry.type) { + case "app": + Bangle.buzz(); + let app = config.apps[entry.id]!; + let infoFile = storage.readJSON(entry.id + '.info', false); + if (app.fast) Bangle.load(infoFile.src); + else if (config.fastNag && !app.nagged) + E.showPrompt(/*LANG*/ 'Would you like to fast load?', { + title: infoFile.name, + buttons: { + "Yes": 0, + "Not now": 1, + "Never": 2 + } + }).then((value: number) => { + switch (value) { + case 0: + app.nagged = true; + app.fast = true; + loader.cleanAndSave(config); + Bangle.load(infoFile.src); + break; + case 1: + load(infoFile.src); + break; + default: + app.nagged = true; + loader.cleanAndSave(config); + load(infoFile.src); + break; + } + }); + else load(infoFile.src); + break; + case "folder": + Bangle.buzz(); + resetTimeout(); + page = 0; + folderPath.push(entry.id); + folder = getFolder(folderPath); + render(); + break; + default: + resetTimeout(); + break; + } + } + + let page: number = 0; + let nPages: number; // Set when setting folder + + /** + * Handle a swipe + * + * A swipe from left is treated as the back button. Up and down swipes change pages + * + * @param lr -1 if left, 0 if pure up/down, 1 if right + * @param ud -1 if up, 0 if pure left/right, 1 if down + */ + let onSwipe = function (lr: -1 | 0 | 1 | undefined, ud: -1 | 0 | 1 | undefined) { + if (lr == 1 && ud == 0) { + onBackButton(); + return; + } else if (ud == 1) { + resetTimeout(); + if (page == 0) { + Bangle.buzz(200); + return; + } else page--; + } else if (ud == -1) { + resetTimeout(); + if (page == nPages - 1) { + Bangle.buzz(200); + return; + } else page++; + } + + // If we reached this point, the page number has been changed and is valid. + render(); + } + + /** + * Go back up a level. If already at the root folder, exit the launcher + */ + let onBackButton = () => { + Bangle.buzz(); + if (folderPath.length == 0) + Bangle.showClock(); + else { + folderPath.pop(); + folder = getFolder(folderPath); + resetTimeout(); + page = 0; + render(); + } + } + + + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + Bangle.setUI({ + mode: 'custom', + back: onBackButton, + btn: onBackButton, + swipe: onSwipe, + touch: onTouch, + remove: () => { if (timeout) clearTimeout(timeout); } + }); + + resetTimeout(); + render(); + +} diff --git a/apps/folderlaunch/configLoad.js b/apps/folderlaunch/configLoad.js new file mode 100644 index 000000000..9ad348f67 --- /dev/null +++ b/apps/folderlaunch/configLoad.js @@ -0,0 +1,107 @@ +var storage = require("Storage"); +var SETTINGS_FILE = "folderlaunch.json"; +var DEFAULT_CONFIG = { + showClocks: false, + showLaunchers: false, + hidden: [], + display: { + rows: 2, + icon: true, + font: 12 + }, + fastNag: true, + timeout: 30000, + rootFolder: { + folders: {}, + apps: [] + }, + apps: {}, + hash: 0 +}; +function clearFolder(folder) { + for (var childName in folder.folders) + folder.folders[childName] = clearFolder(folder.folders[childName]); + folder.apps = []; + return folder; +} +function cleanAndSave(config) { + var infoFiles = storage.list(/\.info$/); + var installedAppIds = []; + for (var _i = 0, infoFiles_1 = infoFiles; _i < infoFiles_1.length; _i++) { + var infoFile = infoFiles_1[_i]; + installedAppIds.push(storage.readJSON(infoFile, true).id); + } + var toRemove = []; + for (var appId in config.apps) + if (!installedAppIds.includes(appId)) + toRemove.push(appId); + for (var _a = 0, toRemove_1 = toRemove; _a < toRemove_1.length; _a++) { + var appId = toRemove_1[_a]; + delete config.apps[appId]; + } + storage.writeJSON(SETTINGS_FILE, config); + return config; +} +var infoFileSorter = function (a, b) { + var aJson = storage.readJSON(a, false); + var bJson = storage.readJSON(b, false); + var n = (0 | aJson.sortorder) - (0 | bJson.sortorder); + if (n) + return n; + if (aJson.name < bJson.name) + return -1; + if (aJson.name > bJson.name) + return 1; + return 0; +}; +module.exports = { + cleanAndSave: cleanAndSave, + infoFileSorter: infoFileSorter, + getConfig: function () { + var config = storage.readJSON(SETTINGS_FILE, true) || DEFAULT_CONFIG; + if (config.hash == storage.hash(/\.info$/)) { + return config; + } + E.showMessage('Rebuilding cache...'); + config.rootFolder = clearFolder(config.rootFolder); + var infoFiles = storage.list(/\.info$/); + infoFiles.sort(infoFileSorter); + for (var _i = 0, infoFiles_2 = infoFiles; _i < infoFiles_2.length; _i++) { + var infoFile = infoFiles_2[_i]; + var app_1 = storage.readJSON(infoFile, false); + if ((!config.showClocks && app_1.type == 'clock') || + (!config.showLaunchers && app_1.type == 'launch') || + (app_1.type == 'widget') || + (!app_1.src)) { + if (Object.keys(config.hidden).includes(app_1.id)) + delete config.apps[app_1.id]; + continue; + } + if (!config.apps.hasOwnProperty(app_1.id)) { + config.apps[app_1.id] = { + folder: [], + fast: false, + nagged: false + }; + } + if (config.hidden.includes(app_1.id)) + continue; + var curFolder = config.rootFolder; + var depth = 0; + for (var _a = 0, _b = config.apps[app_1.id].folder; _a < _b.length; _a++) { + var folderName = _b[_a]; + if (curFolder.folders.hasOwnProperty(folderName)) { + curFolder = curFolder.folders[folderName]; + depth++; + } + else { + config.apps[app_1.id].folder = config.apps[app_1.id].folder.slice(0, depth); + break; + } + } + curFolder.apps.push(app_1.id); + } + config.hash = storage.hash(/\.info$/); + return cleanAndSave(config); + } +}; diff --git a/apps/folderlaunch/configLoad.ts b/apps/folderlaunch/configLoad.ts new file mode 100644 index 000000000..fc7c76306 --- /dev/null +++ b/apps/folderlaunch/configLoad.ts @@ -0,0 +1,158 @@ +const storage = require("Storage"); + +const SETTINGS_FILE: string = "folderlaunch.json"; + +const DEFAULT_CONFIG: Config = { + showClocks: false, + showLaunchers: false, + hidden: [], + display: { + rows: 2, + icon: true, + font: 12 + }, + fastNag: true, + timeout: 30000, + rootFolder: { + folders: {}, + apps: [] + }, + apps: {}, + hash: 0 +} + +/** + * Recursively remove all apps from a folder + * + * @param folder the folder to clean + * @return the folder with all apps removed + */ +function clearFolder(folder: Folder): Folder { + for (let childName in folder.folders) + folder.folders[childName] = clearFolder(folder.folders[childName]!); + folder.apps = []; + return folder; +} + +/** + * Clean and save the configuration. + * + * Assume that: + * - All installed apps have an appInfo entry + * - References to nonexistent folders have been removed from appInfo + * And therefore we do not need to do this ourselves. + * Note: It is not a real problem if the assumptions are not true. If this was called by getConfig, the assumptions are already taken care of. If this was called somewhere else, they will be taken care of the next time getConfig is called. + * + * Perform the following cleanup: + * - Remove appInfo entries for nonexistent apps, to prevent irrelevant data invisible to the user from accumulating + * + * @param config the configuration to be cleaned + * @return the cleaned configuration + */ +function cleanAndSave(config: Config): Config { + // Get the list of installed apps + let infoFiles: Array = storage.list(/\.info$/); + let installedAppIds: Array = []; + for (let infoFile of infoFiles) + installedAppIds.push(storage.readJSON(infoFile, true).id); + + // Remove nonexistent apps from appInfo + let toRemove: Array = []; + for (let appId in config.apps) + if (!installedAppIds.includes(appId)) + toRemove.push(appId); + for (let appId of toRemove) + delete config.apps[appId]; + + // Save and return + storage.writeJSON(SETTINGS_FILE, config); + return config; +} + +/** + * Comparator function to sort a list of app info files. + * Copied and slightly modified (mainly to port to Typescript) from dtlaunch. + * + * @param a the first + * @param b the second + * @return negative if a should go first, positive if b should go first, zero if equivalent. + */ +let infoFileSorter = (a: string, b: string): number => { + let aJson: AppInfoFile = storage.readJSON(a, false); + let bJson: AppInfoFile = storage.readJSON(b, false); + var n = (0 | aJson.sortorder!) - (0 | bJson.sortorder!); + if (n) return n; // do sortorder first + if (aJson.name < bJson.name) return -1; + if (aJson.name > bJson.name) return 1; + return 0; +} + +export = { + cleanAndSave: cleanAndSave, + infoFileSorter: infoFileSorter, + + /** + * Get the configuration for the launcher. Perform a cleanup if any new apps were installed or any apps refer to nonexistent folders. + * + * @param keepHidden if true, don't exclude apps that would otherwise be hidden + * @return the loaded configuration + */ + getConfig: (): Config => { + let config = storage.readJSON(SETTINGS_FILE, true) || DEFAULT_CONFIG; + + // We only need to load data from the filesystem if there is a change + if (config.hash == storage.hash(/\.info$/)) { + return config; + } + + E.showMessage(/*LANG*/'Rebuilding cache...') + config.rootFolder = clearFolder(config.rootFolder); + let infoFiles: Array = storage.list(/\.info$/); + infoFiles.sort(infoFileSorter); + + for (let infoFile of infoFiles) { + let app: AppInfoFile = storage.readJSON(infoFile, false); + + // If the app is to be hidden by policy, exclude it completely + if ( + (!config.showClocks && app.type == 'clock') || + (!config.showLaunchers && app.type == 'launch') || + (app.type == 'widget') || + (!app.src) + ) { + if (Object.keys(config.hidden).includes(app.id)) delete config.apps[app.id]; + continue; + } + + // Creates the apps entry if it doesn't exist yet. + if (!config.apps.hasOwnProperty(app.id)) { + config.apps[app.id] = { + folder: [], + fast: false, + nagged: false + }; + } + + // If the app is manually hidden, don't put it in a folder but still keep information about it + if (config.hidden.includes(app.id)) continue; + + // Place apps in folders, deleting references to folders that no longer exist + // Note: Relies on curFolder secretly being a reference rather than a copy + let curFolder: Folder = config.rootFolder; + let depth = 0; + for (let folderName of config.apps[app.id].folder) { + if (curFolder.folders.hasOwnProperty(folderName)) { + curFolder = curFolder.folders[folderName]!; + depth++; + } else { + config.apps[app.id].folder = config.apps[app.id].folder.slice(0, depth); + break; + } + } + curFolder.apps.push(app.id); + } + config.hash = storage.hash(/\.info$/); + + return cleanAndSave(config); + } +} \ No newline at end of file diff --git a/apps/folderlaunch/icon.js b/apps/folderlaunch/icon.js new file mode 100644 index 000000000..426ffc1d2 --- /dev/null +++ b/apps/folderlaunch/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwMA///wAJCAoPAAongAonwAon4Aon8Aon+Aon/AooA/AH4A/AFgA=")) \ No newline at end of file diff --git a/apps/folderlaunch/icon.png b/apps/folderlaunch/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1ebab139f7a47f4581d28a20b15f88b714cf5ad8 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtjh-%!AsNnZr(EPbY#?y7>No$} zOS~>gH$|qrKg~T!Ve_@9`P0vzIdi)6`1$5SqW~Pf?xN_(rmqXyKNLG`r1v{7mrj&&-`s&K8V-Q(7H`F1R jyIq?cWjF7O|6sCMx_{#QtR3Az$1`}k`njxgN@xNAS*2I@ literal 0 HcmV?d00001 diff --git a/apps/folderlaunch/metadata.json b/apps/folderlaunch/metadata.json new file mode 100644 index 000000000..c3c8db75b --- /dev/null +++ b/apps/folderlaunch/metadata.json @@ -0,0 +1,48 @@ +{ + "id": "folderlaunch", + "name": "Folder launcher", + "version": "0.01", + "description": "Launcher that allows you to put your apps into folders", + "icon": "icon.png", + "type": "launch", + "tags": "tool,system,launcher", + "supports": [ + "BANGLEJS2" + ], + "readme": "README.md", + "storage": [ + { + "name": "folderlaunch.app.js", + "url": "app.js" + }, + { + "name": "folderlaunch.settings.js", + "url": "settings.js" + }, + { + "name": "folderlaunch-configLoad.js", + "url": "configLoad.js" + }, + { + "name": "folderlaunch.img", + "url": "icon.js", + "evaluate": true + } + ], + "data": [ + { + "name": "folderlaunch.json" + } + ], + "dependencies": { + "textinput": "type" + }, + "screenshots": [ + { + "url": "screenshot1.png" + }, + { + "url": "screenshot2.png" + } + ] +} \ No newline at end of file diff --git a/apps/folderlaunch/screenshot1.png b/apps/folderlaunch/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..88642a728b17b67eedcb3bb0e049db1e9ada5421 GIT binary patch literal 4832 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 { onchange(value, \"".concat(app_1, "\"); }")) + }; + } + E.showMenu(menu); + }; + var getAppInfo = function (id) { + return storage.readJSON(id + '.info', false); + }; + var showFolderMenu = function (path) { + var folder = config.rootFolder; + for (var _i = 0, path_1 = path; _i < path_1.length; _i++) { + var folderName = path_1[_i]; + try { + folder = folder.folders[folderName]; + } + catch (_a) { + E.showAlert('BUG: Nonexistent folder ' + path); + } + } + var back = function () { + if (path.length) { + path.pop(); + showFolderMenu(path); + } + else + showMainMenu(); + }; + var menu = { + '': { + 'title': path.length ? path[path.length - 1] : 'Root folder', + 'back': back + }, + 'New subfolder': function () { + textinput.input({ text: '' }).then(function (result) { + if (result && !Object.keys(folder.folders).includes(result)) { + folder.folders[result] = { + folders: {}, + apps: [] + }; + changed = true; + path.push(result); + showFolderMenu(path); + } + else { + E.showAlert('No folder created').then(function () { + showFolderMenu(path); + }); + } + }); + }, + 'Move app here': function () { + var menu = { + '': { + 'title': 'Select app', + 'back': function () { + showFolderMenu(path); + } + } + }; + var mover = function (appId) { + var folder = config.rootFolder; + for (var _i = 0, _a = config.apps[appId].folder; _i < _a.length; _i++) { + var folderName = _a[_i]; + folder = folder.folders[folderName]; + } + folder.apps = folder.apps.filter(function (item) { return item != appId; }); + config.apps[appId].folder = path.slice(); + folder = config.rootFolder; + for (var _b = 0, path_2 = path; _b < path_2.length; _b++) { + var folderName = path_2[_b]; + folder = folder.folders[folderName]; + } + folder.apps.push(appId); + changed = true; + showFolderMenu(path); + }; + mover; + E.showMessage('Loading apps...'); + for (var _i = 0, _a = Object.keys(config.apps) + .filter(function (item) { return !folder.apps.includes(item); }) + .map(function (item) { return item + '.info'; }) + .sort(loader.infoFileSorter) + .map(function (item) { return item.split('.info')[0]; }); _i < _a.length; _i++) { + var appId = _a[_i]; + menu[getAppInfo(appId).name] = eval("() => { mover(\"".concat(appId, "\"); }")); + } + E.showMenu(menu); + } + }; + var switchToFolder = function (subfolder) { + path.push(subfolder); + showFolderMenu(path); + }; + switchToFolder; + for (var _b = 0, _c = Object.keys(folder.folders); _b < _c.length; _b++) { + var subfolder = _c[_b]; + menu[subfolder] = eval("() => { switchToFolder(\"".concat(subfolder, "\") }")); + } + if (folder.apps.length) + menu['View apps'] = function () { + var menu = { + '': { + 'title': path[path.length - 1], + 'back': function () { showFolderMenu(path); } + } + }; + for (var _i = 0, _a = folder.apps; _i < _a.length; _i++) { + var appId = _a[_i]; + menu[storage.readJSON(appId + '.info', false).name] = function () { }; + } + E.showMenu(menu); + }; + if (path.length) + menu['Delete folder'] = function () { + var apps = folder.apps; + var subfolders = folder.folders; + var toDelete = path.pop(); + folder = config.rootFolder; + for (var _i = 0, path_3 = path; _i < path_3.length; _i++) { + var folderName = path_3[_i]; + folder = folder.folders[folderName]; + } + for (var _a = 0, apps_1 = apps; _a < apps_1.length; _a++) { + var appId = apps_1[_a]; + config.apps[appId].folder.pop(); + folder.apps.push(appId); + } + for (var _b = 0, _c = Object.keys(subfolders); _b < _c.length; _b++) { + var subfolder = _c[_b]; + folder.folders[subfolder] = subfolders[subfolder]; + } + delete folder.folders[toDelete]; + changed = true; + showFolderMenu(path); + }; + E.showMenu(menu); + }; + var save = function () { + if (changed) { + E.showMessage('Saving...'); + config.hash = 0; + loader.cleanAndSave(config); + changed = false; + } + ; + }; + E.on('kill', save); + var showMainMenu = function () { + E.showMenu({ + '': { + 'title': 'Folder launcher', + 'back': function () { + save(); + back(); + } + }, + 'Show clocks': { + value: config.showClocks, + format: function (value) { return (value ? 'Yes' : 'No'); }, + onchange: function (value) { + config.showClocks = value; + changed = true; + } + }, + 'Show launchers': { + value: config.showLaunchers, + format: function (value) { return (value ? 'Yes' : 'No'); }, + onchange: function (value) { + config.showLaunchers = value; + changed = true; + } + }, + 'Hidden apps': hiddenAppsMenu, + 'Display': function () { + E.showMenu({ + '': { + 'title': 'Display', + 'back': showMainMenu + }, + 'Rows': { + value: config.display.rows, + min: 1, + onchange: function (value) { + config.display.rows = value; + changed = true; + } + }, + 'Show icons?': { + value: config.display.icon, + format: function (value) { return (value ? 'Yes' : 'No'); }, + onchange: function (value) { + config.display.icon = value; + changed = true; + } + }, + 'Font size': { + value: config.display.font, + min: 0, + format: function (value) { return (value ? value : 'Icon only'); }, + onchange: function (value) { + config.display.font = value; + changed = true; + } + } + }); + }, + 'Prompt for fast launch': { + value: config.fastNag, + format: function (value) { return (value ? 'Yes' : 'No'); }, + onchange: function (value) { + config.fastNag = value; + changed = true; + } + }, + 'Timeout': { + value: config.timeout, + format: function (value) { return "".concat(value / 1000, " sec"); }, + min: 0, + step: 1000, + onchange: function (value) { + config.timeout = value; + changed = true; + } + }, + 'Folder management': function () { + showFolderMenu([]); + } + }); + }; + showMainMenu(); +}); diff --git a/apps/folderlaunch/settings.ts b/apps/folderlaunch/settings.ts new file mode 100644 index 000000000..1f8cff031 --- /dev/null +++ b/apps/folderlaunch/settings.ts @@ -0,0 +1,272 @@ +(function (back: Function) { + const loader = require('folderlaunch-configLoad.js'); + const storage = require('Storage'); + const textinput = require('textinput'); + + let config: Config = loader.getConfig(); + let changed: boolean = false; + + let hiddenAppsMenu = () => { + let menu: Menu = { + '': { + 'title': 'Hide?', + 'back': showMainMenu + } + } + + let onchange = (value: boolean, appId: string) => { + if (value && !config.hidden.includes(appId)) // Hiding, not already hidden + config.hidden.push(appId); + else if (!value && config.hidden.includes(appId)) // Unhiding, already hidden + config.hidden = config.hidden.filter(item => item != appId) + changed = true; + } + onchange // Do nothing, but stop typescript from yelling at me for this function being unused. It gets used by eval. I know eval is evil, but the menus are a bit limited. + + for (let app in config.apps) { + let appInfo: AppInfoFile = storage.readJSON(app + '.info', false); + menu[appInfo.name] = { + value: config.hidden.includes(app), + format: (value: boolean) => (value ? 'Yes' : 'No'), + onchange: eval(`(value) => { onchange(value, "${app}"); }`) + } + } + + E.showMenu(menu); + }; + + let getAppInfo = (id: string): AppInfoFile => { + return storage.readJSON(id + '.info', false); + } + + let showFolderMenu = (path: Array) => { + let folder: Folder = config.rootFolder; + for (let folderName of path) + try { + folder = folder.folders[folderName]!; + } catch { + E.showAlert(/*LANG*/'BUG: Nonexistent folder ' + path); + } + + let back = () => { + if (path.length) { + path.pop(); + showFolderMenu(path); + } else showMainMenu(); + }; + + let menu: Menu = { + '': { + 'title': path.length ? path[path.length - 1]! : /*LANG*/ 'Root folder', + 'back': back + }, + /*LANG*/'New subfolder': () => { + textinput.input({ text: '' }).then((result: string) => { + if (result && !Object.keys(folder.folders).includes(result)) { + folder.folders[result] = { + folders: {}, + apps: [] + }; + changed = true; + path.push(result); + showFolderMenu(path); + } else { + E.showAlert(/*LANG*/'No folder created').then(() => { + showFolderMenu(path); + }) + } + }); + }, + /*LANG*/'Move app here': () => { + let menu: Menu = { + '': { + 'title': /*LANG*/'Select app', + 'back': () => { + showFolderMenu(path); + } + } + }; + + let mover = (appId: string) => { + // Delete app from old folder + let folder: Folder = config.rootFolder; + for (let folderName of config.apps[appId]!.folder) + folder = folder.folders[folderName]!; + folder.apps = folder.apps.filter((item: string) => item != appId); + + // Change folder in app info, .slice is to force a copy rather than a reference + config.apps[appId]!.folder = path.slice(); + + // Place app in new folder (here) + folder = config.rootFolder; + for (let folderName of path) + folder = folder.folders[folderName]!; + folder.apps.push(appId); + + // Mark changed and refresh menu + changed = true; + showFolderMenu(path); + }; + mover; + + E.showMessage(/*LANG*/'Loading apps...'); + for ( + let appId of Object.keys(config.apps) // All known apps + .filter(item => !folder.apps.includes(item)) // Filter out ones already in this folder + .map(item => item + '.info') // Convert to .info files + .sort(loader.infoFileSorter) // Sort the info files using infoFileSorter + .map(item => item.split('.info')[0]) // Back to app ids + ) { + menu[getAppInfo(appId).name] = eval(`() => { mover("${appId}"); }`); + } + + E.showMenu(menu); + } + }; + + let switchToFolder = (subfolder: string) => { + path.push(subfolder); + showFolderMenu(path); + }; + switchToFolder; + + for (let subfolder of Object.keys(folder.folders)) { + menu[subfolder] = eval(`() => { switchToFolder("${subfolder}") }`); + } + + if (folder.apps.length) menu[/*LANG*/'View apps'] = () => { + let menu: Menu = { + '': { + 'title': path[path.length - 1]!, + 'back': () => { showFolderMenu(path); } + } + } + for (let appId of folder.apps) { + menu[storage.readJSON(appId + '.info', false).name] = () => { }; + } + E.showMenu(menu); + } + + if (path.length) menu[/*LANG*/'Delete folder'] = () => { + // Cache apps for changing the folder reference + let apps: Array = folder.apps; + let subfolders = folder.folders; + + // Move up to the parent folder + let toDelete: string = path.pop()!; + folder = config.rootFolder; + for (let folderName of path) + folder = folder.folders[folderName]!; + + // Move all apps and folders to the parent folder, then delete this one + for (let appId of apps) { + config.apps[appId]!.folder.pop(); + folder.apps.push(appId); + } + for (let subfolder of Object.keys(subfolders)) + folder.folders[subfolder] = subfolders[subfolder]!; + delete folder.folders[toDelete]; + + // Mark as modified and go back + changed = true; + showFolderMenu(path); + } + + E.showMenu(menu); + }; + + let save = () => { + if (changed) { + E.showMessage(/*LANG*/'Saving...'); + config.hash = 0; // Invalidate the cache so changes to hidden apps or folders actually get reflected + loader.cleanAndSave(config); + changed = false; // So we don't do it again on exit + }; + }; + + E.on('kill', save); + + let showMainMenu = () => { + E.showMenu({ + '': { + 'title': 'Folder launcher', + 'back': () => { + save(); + back(); + } + }, + 'Show clocks': { + value: config.showClocks, + format: value => (value ? 'Yes' : 'No'), + onchange: value => { + config.showClocks = value; + changed = true; + } + }, + 'Show launchers': { + value: config.showLaunchers, + format: value => (value ? 'Yes' : 'No'), + onchange: value => { + config.showLaunchers = value; + changed = true; + } + }, + 'Hidden apps': hiddenAppsMenu, + 'Display': () => { + E.showMenu({ + '': { + 'title': 'Display', + 'back': showMainMenu + }, + 'Rows': { + value: config.display.rows, + min: 1, + onchange: value => { + config.display.rows = value; + changed = true; + } + }, + 'Show icons?': { + value: config.display.icon, + format: value => (value ? 'Yes' : 'No'), + onchange: value => { + config.display.icon = value; + changed = true; + } + }, + 'Font size': { + value: config.display.font as any, + min: 0, + format: (value: any) => (value ? value : 'Icon only'), + onchange: (value: any) => { + config.display.font = value; + changed = true; + } + } + }); + }, + 'Prompt for fast launch': { + value: config.fastNag, + format: value => (value ? 'Yes' : 'No'), + onchange: value => { + config.fastNag = value; + changed = true; + } + }, + 'Timeout': { + value: config.timeout, + format: value => value ? `${value / 1000} sec` : 'None', + min: 0, + step: 1000, + onchange: value => { + config.timeout = value; + changed = true; + } + }, + 'Folder management': () => { + showFolderMenu([]); + } + }); + }; + showMainMenu(); +}); \ No newline at end of file diff --git a/apps/folderlaunch/types.d.ts b/apps/folderlaunch/types.d.ts new file mode 100644 index 000000000..878f11938 --- /dev/null +++ b/apps/folderlaunch/types.d.ts @@ -0,0 +1,48 @@ +type AppInfoFile = { // Contents of a .info file + id: string, + name: string, + type?: string, + src?: string, + icon: string, + version: string, + tags: string, + files: string, + data: string, + sortorder?: number +}; + +type Folder = { + folders: { // name: folder pairs of all nested folders + [key: string]: Folder + }, + apps: Array // List of ids of all apps in this folder +}; + +type FolderList = Array; + +type Config = { + showClocks: boolean, // Whether clocks are shown + showLaunchers: boolean, // Whether launchers are shown + hidden: Array, // IDs of apps to explicitly hide + display: { + rows: number, // Display an X by X grid of apps + icon: boolean, // Whether to show icons + font: number // Which font to use for the name, or false to not show the name + }, + fastNag: boolean, // Ask whether new apps should be fast-loaded the first time they are launched + timeout: number, // How many ms before returning to the clock, or zero to never return + rootFolder: Folder, // The top level folder, first displayed when opened + apps: { // Saved info for each app + [key: string]: { + folder: FolderList, // Folder path + fast: boolean, // Whether the app should be fast launched + nagged: boolean // Whether the app's fast launch setting was configured + } + }, + hash: number // Hash of .info files +}; + +type GridEntry = { // An entry in the grid displayed on-screen + type: 'app' | 'folder' | 'empty', // Which type of item is in this space + id: string // The id of that item +} \ No newline at end of file From 5e47dc39263c086eb8e4e268a80514f4bad1b7ae Mon Sep 17 00:00:00 2001 From: Bruce Blore Date: Fri, 12 May 2023 16:58:30 -0700 Subject: [PATCH 06/14] Removed one singular semicolon --- apps/folderlaunch/settings.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/folderlaunch/settings.js b/apps/folderlaunch/settings.js index 25265e53c..f9833b01c 100644 --- a/apps/folderlaunch/settings.js +++ b/apps/folderlaunch/settings.js @@ -168,7 +168,6 @@ loader.cleanAndSave(config); changed = false; } - ; }; E.on('kill', save); var showMainMenu = function () { From 109fd078f41a5b465042ffd132c06287caef4efd Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sat, 13 May 2023 08:44:50 +0100 Subject: [PATCH 07/14] popcon: use E.stopEventPropagation() for the drag handler --- apps/widhid/README.md | 1 + apps/widhid/wid.js | 71 ++------------------------------- apps/widhid/wid.ts | 93 +++++-------------------------------------- 3 files changed, 14 insertions(+), 151 deletions(-) diff --git a/apps/widhid/README.md b/apps/widhid/README.md index 7651d74eb..652a2ed49 100644 --- a/apps/widhid/README.md +++ b/apps/widhid/README.md @@ -13,6 +13,7 @@ Swipe down to enable - note the icon changes from blue to orange, indicating it' All other watch interaction is disabled for 3 seconds, to prevent clashing taps/drags - this period is extended as you continue to alter the volume, play/pause and jump between tracks. +Requires espruino firmware > 2v17 to avoid event handler clashes. # Setup / Technical details diff --git a/apps/widhid/wid.js b/apps/widhid/wid.js index b361b6453..dd411513e 100644 --- a/apps/widhid/wid.js +++ b/apps/widhid/wid.js @@ -10,34 +10,14 @@ var dragging = false; var activeTimeout; var waitForRelease = true; - var mayInterceptSwipe = function () { - if (Bangle.CLKINFO_FOCUS) - return 0; - if (Bangle.CLOCK) - return 1; - var swipes = Bangle["#onswipe"]; - if (typeof swipes === "function") { - if (swipes !== onSwipe) - return swipes.length > 1; - } - else if (swipes) { - for (var _i = 0, swipes_1 = swipes; _i < swipes_1.length; _i++) { - var handler = swipes_1[_i]; - if (handler !== onSwipe && (handler === null || handler === void 0 ? void 0 : handler.length) > 1) - return 0; - } - } - if (Bangle["#ondrag"]) - return 0; - return 1; - }; var onSwipe = (function (_lr, ud) { - if (ud > 0 && !activeTimeout && mayInterceptSwipe()) { + if (ud > 0 && !activeTimeout && !Bangle.CLKINFO_FOCUS) { listen(); Bangle.buzz(20); } }); var onDrag = (function (e) { + E.stopEventPropagation && E.stopEventPropagation(); if (e.b === 0) { var wasDragging = dragging; dragging = false; @@ -99,9 +79,9 @@ var listen = function () { var wasActive = !!activeTimeout; if (!wasActive) { - suspendOthers(); waitForRelease = true; Bangle.on("drag", onDrag); + Bangle["#ondrag"] = [onDrag].concat(Bangle["#ondrag"].filter(function (f) { return f !== onDrag; })); redraw(); } if (activeTimeout) @@ -109,7 +89,6 @@ activeTimeout = setTimeout(function () { activeTimeout = undefined; Bangle.removeListener("drag", onDrag); - resumeOthers(); redraw(); }, 3000); }; @@ -148,48 +127,4 @@ var toggle = function () { return sendHid(0x10); }; var up = function () { return sendHid(0x40); }; var down = function () { return sendHid(0x80); }; - var touchEvents = { - tap: null, - gesture: null, - aiGesture: null, - swipe: null, - touch: null, - drag: null, - stroke: null, - }; - var suspendOthers = function () { - for (var event_ in touchEvents) { - var event = event_; - var handlers = Bangle["#on".concat(event)]; - if (!handlers) - continue; - var newEvents = void 0; - if (handlers instanceof Array) - newEvents = handlers.filter(function (f) { return f; }); - else - newEvents = [handlers]; - for (var _i = 0, newEvents_1 = newEvents; _i < newEvents_1.length; _i++) { - var handler = newEvents_1[_i]; - Bangle.removeListener(event, handler); - } - touchEvents[event] = newEvents; - } - }; - var resumeOthers = function () { - for (var event_ in touchEvents) { - var event = event_; - var handlers = touchEvents[event]; - touchEvents[event] = null; - if (handlers) - for (var _i = 0, handlers_1 = handlers; _i < handlers_1.length; _i++) { - var handler = handlers_1[_i]; - try { - Bangle.on(event, handler); - } - catch (e) { - console.log("couldn't restore \"".concat(event, "\" handler:"), e); - } - } - } - }; })(); diff --git a/apps/widhid/wid.ts b/apps/widhid/wid.ts index a85725569..3291c188d 100644 --- a/apps/widhid/wid.ts +++ b/apps/widhid/wid.ts @@ -1,9 +1,4 @@ (() => { - type BangleEventKeys = "tap" | "gesture" | "aiGesture" | "swipe" | "touch" | "drag" | "stroke"; - type BangleEvents = { - [key in BangleEventKeys as `#on${key}`]?: Handler | (Handler | undefined)[] - }; - const settings: Settings = require("Storage").readJSON("setting.json", true) || { HID: false } as Settings; if (settings.HID !== "kbmedia") { console.log("widhid: can't enable, HID setting isn't \"kbmedia\""); @@ -18,40 +13,18 @@ let activeTimeout: number | undefined; let waitForRelease = true; - // If the user shows a menu, we want to temporarily disable ourselves - // - // We could detect showing of a menu by overriding E.showMenu - // and to detect hiding of a menu, we hook setUI - // - // Perhaps easier to check Bangle.swipeHandler - set by setUI, - // called by E.showMenu - const mayInterceptSwipe = () => { - if((Bangle as BangleExt).CLKINFO_FOCUS) return 0; - if(Bangle.CLOCK) return 1; - - const swipes = (Bangle as BangleEvents)["#onswipe"]; - if(typeof swipes === "function"){ - if(swipes !== onSwipe) - return swipes.length > 1; // second argument is up/down - }else if(swipes){ - for(const handler of swipes) - if(handler !== onSwipe && handler?.length > 1) - return 0; - } - - if((Bangle as BangleEvents)["#ondrag"]) return 0; - return 1; - }; - const onSwipe = ((_lr, ud) => { // do these checks in order of cheapness - if(ud! > 0 && !activeTimeout && mayInterceptSwipe()){ + if(ud! > 0 && !activeTimeout && !(Bangle as BangleExt).CLKINFO_FOCUS){ listen(); Bangle.buzz(20); } }) satisfies SwipeCallback; const onDrag = (e => { + // Espruino/35c8cb9be11 + (E as any).stopEventPropagation && (E as any).stopEventPropagation(); + if(e.b === 0){ // released const wasDragging = dragging; @@ -108,9 +81,14 @@ const listen = () => { const wasActive = !!activeTimeout; if(!wasActive){ - suspendOthers(); waitForRelease = true; // wait for first touch up before accepting gestures + Bangle.on("drag", onDrag); + // move our drag to the start of the event listener array + (Bangle as any)["#ondrag"] = [onDrag].concat( + (Bangle as any)["#ondrag"].filter((f: unknown) => f !== onDrag) + ); + redraw(); } @@ -119,7 +97,6 @@ activeTimeout = undefined; Bangle.removeListener("drag", onDrag); - resumeOthers(); redraw(); }, 3000); @@ -174,54 +151,4 @@ const toggle = () => /*DEBUG ? console.log("toggle") : */ sendHid(0x10); const up = () => /*DEBUG ? console.log("up") : */ sendHid(0x40); const down = () => /*DEBUG ? console.log("down") : */ sendHid(0x80); - - // similarly to the lightswitch app, we tangle with the listener arrays to - // disable event handlers - type Handler = () => void; - const touchEvents: { - [key in BangleEventKeys]: null | Handler[] - } = { - tap: null, - gesture: null, - aiGesture: null, - swipe: null, - touch: null, - drag: null, - stroke: null, - }; - - const suspendOthers = () => { - for(const event_ in touchEvents){ - const event = event_ as BangleEventKeys; - const handlers = (Bangle as BangleEvents)[`#on${event}`]; - - if(!handlers) continue; - - let newEvents; - if(handlers instanceof Array) - newEvents = handlers.filter(f=>f) as Handler[]; - else - newEvents = [handlers /* single fn */]; - - for(const handler of newEvents) - Bangle.removeListener(event, handler); - - touchEvents[event] = newEvents; - } - }; - const resumeOthers = () => { - for(const event_ in touchEvents){ - const event = event_ as BangleEventKeys; - const handlers = touchEvents[event]; - touchEvents[event] = null; - - if(handlers) - for(const handler of handlers) - try{ - Bangle.on(event as any, handler); - }catch(e){ - console.log(`couldn't restore "${event}" handler:`, e); - } - } - }; })() From aca46616205d0db3cfda98aa9c6abd6a7dba480c Mon Sep 17 00:00:00 2001 From: Bruce Blore Date: Sat, 13 May 2023 04:31:22 -0700 Subject: [PATCH 08/14] Removed unnecessary semicolon in typescript too, changed type of settings function --- apps/folderlaunch/settings.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/folderlaunch/settings.ts b/apps/folderlaunch/settings.ts index 1f8cff031..1ce4bc0cd 100644 --- a/apps/folderlaunch/settings.ts +++ b/apps/folderlaunch/settings.ts @@ -1,4 +1,4 @@ -(function (back: Function) { +(function (back) { const loader = require('folderlaunch-configLoad.js'); const storage = require('Storage'); const textinput = require('textinput'); @@ -181,7 +181,7 @@ config.hash = 0; // Invalidate the cache so changes to hidden apps or folders actually get reflected loader.cleanAndSave(config); changed = false; // So we don't do it again on exit - }; + } }; E.on('kill', save); @@ -269,4 +269,4 @@ }); }; showMainMenu(); -}); \ No newline at end of file +} satisfies SettingsFunc); \ No newline at end of file From f983a4c6b69ceed1b2b86f9d6a23edfb455e9353 Mon Sep 17 00:00:00 2001 From: Bruce Blore Date: Sat, 13 May 2023 04:39:24 -0700 Subject: [PATCH 09/14] Removed my own fast loading functionality in favor of using fast load utils --- apps/folderlaunch/README.md | 4 ---- apps/folderlaunch/app.ts | 31 +------------------------------ apps/folderlaunch/configLoad.ts | 2 -- apps/folderlaunch/settings.ts | 8 -------- apps/folderlaunch/types.d.ts | 1 - 5 files changed, 1 insertion(+), 45 deletions(-) diff --git a/apps/folderlaunch/README.md b/apps/folderlaunch/README.md index bef4ebc78..a0a03894c 100644 --- a/apps/folderlaunch/README.md +++ b/apps/folderlaunch/README.md @@ -11,8 +11,6 @@ The apps and folders will be presented in a grid layout, configurable in size. T Swiping up and down will scroll. Swiping from the left, using the back button, or pressing BTN1 will take you up a level to the folder containing the current one, or exit the launcher if you are at the top level. -The first time you launch an app, you will be asked if you want to fast load it, if the feature is enabled. Fast loading saves time by skipping much of the initialization between apps. Instead, the launcher just undoes the changes it made to the watch's state. However, fast loading can only be done between two apps that support widgets, so the decision to fast load has to be made for each app. The options are "Yes", "Not now", and "Never", so choose "Yes" if the app uses widgets, "Not now" if you don't remember, and "Never" if you know for sure it doesn't use widgets. - ## Settings menu * Show clocks / Show launcher: Whether clock and launcher apps are displayed in the UI to be launched. The default is no. @@ -26,8 +24,6 @@ The first time you launch an app, you will be asked if you want to fast load it, To prevent the launcher from becoming unusable, if neither icons nor text are enabled in the settings menu, text will still be drawn. -* Prompt for fast launch: If yes, when launching an app that does not yet have a setting saved, ask whether it should be fast loaded. If no, already saved settings are still applied, but apps that have not been assigned a setting will be slow loaded. The default is yes. - * Timeout: If the launcher is left idle for too long, return to the clock. This is convenient if you often accidentally open the launcher without noticing. At zero seconds, the timeout is disabled. The default is 30 seconds. * Folder management: Open the folder management menu for the root folder. (The folder first displayed when opening the launcher.) The folder management menu contains the following: diff --git a/apps/folderlaunch/app.ts b/apps/folderlaunch/app.ts index ef62687d7..122b17bb8 100644 --- a/apps/folderlaunch/app.ts +++ b/apps/folderlaunch/app.ts @@ -186,37 +186,8 @@ switch (entry.type) { case "app": Bangle.buzz(); - let app = config.apps[entry.id]!; let infoFile = storage.readJSON(entry.id + '.info', false); - if (app.fast) Bangle.load(infoFile.src); - else if (config.fastNag && !app.nagged) - E.showPrompt(/*LANG*/ 'Would you like to fast load?', { - title: infoFile.name, - buttons: { - "Yes": 0, - "Not now": 1, - "Never": 2 - } - }).then((value: number) => { - switch (value) { - case 0: - app.nagged = true; - app.fast = true; - loader.cleanAndSave(config); - Bangle.load(infoFile.src); - break; - case 1: - load(infoFile.src); - break; - default: - app.nagged = true; - loader.cleanAndSave(config); - load(infoFile.src); - break; - } - }); - else load(infoFile.src); - break; + load(infoFile.src); case "folder": Bangle.buzz(); resetTimeout(); diff --git a/apps/folderlaunch/configLoad.ts b/apps/folderlaunch/configLoad.ts index fc7c76306..95bdf2cb6 100644 --- a/apps/folderlaunch/configLoad.ts +++ b/apps/folderlaunch/configLoad.ts @@ -11,7 +11,6 @@ const DEFAULT_CONFIG: Config = { icon: true, font: 12 }, - fastNag: true, timeout: 30000, rootFolder: { folders: {}, @@ -128,7 +127,6 @@ export = { if (!config.apps.hasOwnProperty(app.id)) { config.apps[app.id] = { folder: [], - fast: false, nagged: false }; } diff --git a/apps/folderlaunch/settings.ts b/apps/folderlaunch/settings.ts index 1ce4bc0cd..b17b3cc36 100644 --- a/apps/folderlaunch/settings.ts +++ b/apps/folderlaunch/settings.ts @@ -245,14 +245,6 @@ } }); }, - 'Prompt for fast launch': { - value: config.fastNag, - format: value => (value ? 'Yes' : 'No'), - onchange: value => { - config.fastNag = value; - changed = true; - } - }, 'Timeout': { value: config.timeout, format: value => value ? `${value / 1000} sec` : 'None', diff --git a/apps/folderlaunch/types.d.ts b/apps/folderlaunch/types.d.ts index 878f11938..5567975a0 100644 --- a/apps/folderlaunch/types.d.ts +++ b/apps/folderlaunch/types.d.ts @@ -29,7 +29,6 @@ type Config = { icon: boolean, // Whether to show icons font: number // Which font to use for the name, or false to not show the name }, - fastNag: boolean, // Ask whether new apps should be fast-loaded the first time they are launched timeout: number, // How many ms before returning to the clock, or zero to never return rootFolder: Folder, // The top level folder, first displayed when opened apps: { // Saved info for each app From 032b7c03f5fb1eee24e833d2762a480a8e848c50 Mon Sep 17 00:00:00 2001 From: Bruce Blore Date: Sat, 13 May 2023 04:59:33 -0700 Subject: [PATCH 10/14] Built the js --- apps/folderlaunch/app.js | 41 ++++----------------------------- apps/folderlaunch/configLoad.js | 2 -- apps/folderlaunch/settings.js | 10 +------- 3 files changed, 6 insertions(+), 47 deletions(-) diff --git a/apps/folderlaunch/app.js b/apps/folderlaunch/app.js index 5d1622825..3905abbe1 100644 --- a/apps/folderlaunch/app.js +++ b/apps/folderlaunch/app.js @@ -1,8 +1,8 @@ { - var loader_1 = require('folderlaunch-configLoad.js'); + var loader = require('folderlaunch-configLoad.js'); var storage_1 = require('Storage'); var FOLDER_ICON_1 = require("heatshrink").decompress(atob("mEwwMA///wAJCAoPAAongAonwAon4Aon8Aon+Aon/AooA/AH4A/AFgA=")); - var config_1 = loader_1.getConfig(); + var config_1 = loader.getConfig(); var timeout_1; var resetTimeout_1 = function () { if (timeout_1) { @@ -112,7 +112,7 @@ } } if (empty) - E.showMessage('Folder is empty. Swipe left, back button, or BTN1 to go back.'); + E.showMessage('Folder is empty. Swipe from left, back button, or BTN1 to go back.'); if (nPages_1 > 1) { var barSize = (g.getHeight() - 24) / nPages_1; var barTop = 24 + (page_1 * barSize); @@ -134,39 +134,8 @@ switch (entry.type) { case "app": Bangle.buzz(); - var app_2 = config_1.apps[entry.id]; - var infoFile_1 = storage_1.readJSON(entry.id + '.info', false); - if (app_2.fast) - Bangle.load(infoFile_1.src); - else if (config_1.fastNag && !app_2.nagged) - E.showPrompt('Would you like to fast load?', { - title: infoFile_1.name, - buttons: { - "Yes": 0, - "Not now": 1, - "Never": 2 - } - }).then(function (value) { - switch (value) { - case 0: - app_2.nagged = true; - app_2.fast = true; - loader_1.cleanAndSave(config_1); - Bangle.load(infoFile_1.src); - break; - case 1: - load(infoFile_1.src); - break; - default: - app_2.nagged = true; - loader_1.cleanAndSave(config_1); - load(infoFile_1.src); - break; - } - }); - else - load(infoFile_1.src); - break; + var infoFile = storage_1.readJSON(entry.id + '.info', false); + load(infoFile.src); case "folder": Bangle.buzz(); resetTimeout_1(); diff --git a/apps/folderlaunch/configLoad.js b/apps/folderlaunch/configLoad.js index 9ad348f67..2c27df1b4 100644 --- a/apps/folderlaunch/configLoad.js +++ b/apps/folderlaunch/configLoad.js @@ -9,7 +9,6 @@ var DEFAULT_CONFIG = { icon: true, font: 12 }, - fastNag: true, timeout: 30000, rootFolder: { folders: {}, @@ -80,7 +79,6 @@ module.exports = { if (!config.apps.hasOwnProperty(app_1.id)) { config.apps[app_1.id] = { folder: [], - fast: false, nagged: false }; } diff --git a/apps/folderlaunch/settings.js b/apps/folderlaunch/settings.js index f9833b01c..fa69963c4 100644 --- a/apps/folderlaunch/settings.js +++ b/apps/folderlaunch/settings.js @@ -229,17 +229,9 @@ } }); }, - 'Prompt for fast launch': { - value: config.fastNag, - format: function (value) { return (value ? 'Yes' : 'No'); }, - onchange: function (value) { - config.fastNag = value; - changed = true; - } - }, 'Timeout': { value: config.timeout, - format: function (value) { return "".concat(value / 1000, " sec"); }, + format: function (value) { return value ? "".concat(value / 1000, " sec") : 'None'; }, min: 0, step: 1000, onchange: function (value) { From 4dfdc8ab63cf8f07122dc36db5700cf149ef398b Mon Sep 17 00:00:00 2001 From: Bruce Blore Date: Sat, 13 May 2023 08:07:42 -0700 Subject: [PATCH 11/14] Fixed two buzzes and use AppInfo instead of AppInfoFile --- apps/folderlaunch/app.js | 2 +- apps/folderlaunch/app.ts | 7 ++++--- apps/folderlaunch/configLoad.ts | 6 +++--- apps/folderlaunch/settings.ts | 4 ++-- apps/folderlaunch/types.d.ts | 13 ------------- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/apps/folderlaunch/app.js b/apps/folderlaunch/app.js index 3905abbe1..b341c5f3d 100644 --- a/apps/folderlaunch/app.js +++ b/apps/folderlaunch/app.js @@ -133,9 +133,9 @@ var entry = grid_1[x][y]; switch (entry.type) { case "app": - Bangle.buzz(); var infoFile = storage_1.readJSON(entry.id + '.info', false); load(infoFile.src); + break; case "folder": Bangle.buzz(); resetTimeout_1(); diff --git a/apps/folderlaunch/app.ts b/apps/folderlaunch/app.ts index 122b17bb8..921143819 100644 --- a/apps/folderlaunch/app.ts +++ b/apps/folderlaunch/app.ts @@ -115,8 +115,8 @@ // Get the icon and text, skip if the space is empty. Always draw text for folders even if disabled switch (entry.type) { case 'app': - let app: AppInfoFile = storage.readJSON(entry.id + '.info', false); - icon = storage.read(app.icon)!; + let app: AppInfo = storage.readJSON(entry.id + '.info', false); + icon = storage.read(app.icon!)!; text = app.name; empty = false; fontSize = config.display.font; @@ -185,9 +185,10 @@ let entry: GridEntry = grid[x]![y]!; switch (entry.type) { case "app": - Bangle.buzz(); + // Bangle.buzz(); let infoFile = storage.readJSON(entry.id + '.info', false); load(infoFile.src); + break; case "folder": Bangle.buzz(); resetTimeout(); diff --git a/apps/folderlaunch/configLoad.ts b/apps/folderlaunch/configLoad.ts index 95bdf2cb6..0cb9d59f6 100644 --- a/apps/folderlaunch/configLoad.ts +++ b/apps/folderlaunch/configLoad.ts @@ -77,8 +77,8 @@ function cleanAndSave(config: Config): Config { * @return negative if a should go first, positive if b should go first, zero if equivalent. */ let infoFileSorter = (a: string, b: string): number => { - let aJson: AppInfoFile = storage.readJSON(a, false); - let bJson: AppInfoFile = storage.readJSON(b, false); + let aJson: AppInfo = storage.readJSON(a, false); + let bJson: AppInfo = storage.readJSON(b, false); var n = (0 | aJson.sortorder!) - (0 | bJson.sortorder!); if (n) return n; // do sortorder first if (aJson.name < bJson.name) return -1; @@ -110,7 +110,7 @@ export = { infoFiles.sort(infoFileSorter); for (let infoFile of infoFiles) { - let app: AppInfoFile = storage.readJSON(infoFile, false); + let app: AppInfo = storage.readJSON(infoFile, false); // If the app is to be hidden by policy, exclude it completely if ( diff --git a/apps/folderlaunch/settings.ts b/apps/folderlaunch/settings.ts index b17b3cc36..4433b09a2 100644 --- a/apps/folderlaunch/settings.ts +++ b/apps/folderlaunch/settings.ts @@ -24,7 +24,7 @@ onchange // Do nothing, but stop typescript from yelling at me for this function being unused. It gets used by eval. I know eval is evil, but the menus are a bit limited. for (let app in config.apps) { - let appInfo: AppInfoFile = storage.readJSON(app + '.info', false); + let appInfo: AppInfo = storage.readJSON(app + '.info', false); menu[appInfo.name] = { value: config.hidden.includes(app), format: (value: boolean) => (value ? 'Yes' : 'No'), @@ -35,7 +35,7 @@ E.showMenu(menu); }; - let getAppInfo = (id: string): AppInfoFile => { + let getAppInfo = (id: string): AppInfo => { return storage.readJSON(id + '.info', false); } diff --git a/apps/folderlaunch/types.d.ts b/apps/folderlaunch/types.d.ts index 5567975a0..9d22b9ff5 100644 --- a/apps/folderlaunch/types.d.ts +++ b/apps/folderlaunch/types.d.ts @@ -1,16 +1,3 @@ -type AppInfoFile = { // Contents of a .info file - id: string, - name: string, - type?: string, - src?: string, - icon: string, - version: string, - tags: string, - files: string, - data: string, - sortorder?: number -}; - type Folder = { folders: { // name: folder pairs of all nested folders [key: string]: Folder From 50789ad62c8549b74a8c991abac3af27792766af Mon Sep 17 00:00:00 2001 From: Bruce Blore Date: Sat, 13 May 2023 08:08:01 -0700 Subject: [PATCH 12/14] Added some fields to AppInfo --- typescript/types/info.d.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/typescript/types/info.d.ts b/typescript/types/info.d.ts index 41dec8578..004c3371d 100644 --- a/typescript/types/info.d.ts +++ b/typescript/types/info.d.ts @@ -1,12 +1,17 @@ type AppInfo = { - src: string, - img: string, - icon: string, + id: string, + src?: string, + img?: string, + icon?: string, name: string, type: AppType, + version: string, + tags: string, + files: string, + data?: string, sortorder?: number, }; type AppType = "app" | "clock" | "widget" | "module" | "bootloader" | - "settings" | "clkinfo" | "RAM" | "launch" | "textinput" | "scheduler" | - "notify" | "locale"; + "settings" | "clkinfo" | "RAM" | "launch" | "textinput" | "scheduler" | + "notify" | "locale"; From 96389aa255bf291ef40ab9ace2b37c48c3ee3a55 Mon Sep 17 00:00:00 2001 From: Bruce Blore Date: Sat, 13 May 2023 08:13:25 -0700 Subject: [PATCH 13/14] Re-enabled first buzz --- apps/folderlaunch/app.js | 1 + apps/folderlaunch/app.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/folderlaunch/app.js b/apps/folderlaunch/app.js index b341c5f3d..bb674bc88 100644 --- a/apps/folderlaunch/app.js +++ b/apps/folderlaunch/app.js @@ -133,6 +133,7 @@ var entry = grid_1[x][y]; switch (entry.type) { case "app": + Bangle.buzz(); var infoFile = storage_1.readJSON(entry.id + '.info', false); load(infoFile.src); break; diff --git a/apps/folderlaunch/app.ts b/apps/folderlaunch/app.ts index 921143819..3e2dbacc9 100644 --- a/apps/folderlaunch/app.ts +++ b/apps/folderlaunch/app.ts @@ -185,7 +185,7 @@ let entry: GridEntry = grid[x]![y]!; switch (entry.type) { case "app": - // Bangle.buzz(); + Bangle.buzz(); let infoFile = storage.readJSON(entry.id + '.info', false); load(infoFile.src); break; From 06a7adca30dedc09369b819512f4a9079a39ffcf Mon Sep 17 00:00:00 2001 From: Bruce Blore Date: Sat, 13 May 2023 14:28:10 -0700 Subject: [PATCH 14/14] Made tags, type, and version optional --- typescript/types/info.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typescript/types/info.d.ts b/typescript/types/info.d.ts index 004c3371d..c305b0261 100644 --- a/typescript/types/info.d.ts +++ b/typescript/types/info.d.ts @@ -4,9 +4,9 @@ type AppInfo = { img?: string, icon?: string, name: string, - type: AppType, - version: string, - tags: string, + type?: AppType, + version?: string, + tags?: string, files: string, data?: string, sortorder?: number,