BangleApps/apps/folderlaunch/app.ts

281 lines
8.7 KiB
TypeScript
Raw Normal View History

{
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<string> = [];
/**
* 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<string>): 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<Array<GridEntry>> = [];
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) {
2024-05-04 21:21:43 +00:00
case 'app': {
let app = storage.readJSON(entry.id + '.info', false) as AppInfo;
icon = storage.read(app.icon!)!;
text = app.name;
empty = false;
fontSize = config.display.font;
break;
2024-05-04 21:21:43 +00:00
}
case 'folder': {
icon = FOLDER_ICON;
text = entry.id;
empty = false;
fontSize = config.display.font ? config.display.font : 12;
break;
2024-05-04 21:21:43 +00:00
}
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) {
2024-05-04 21:21:43 +00:00
case "app": {
2023-06-14 19:44:27 +00:00
buzz();
let infoFile = storage.readJSON(entry.id + '.info', false) as AppInfo;
load(infoFile.src);
break;
2024-05-04 21:21:43 +00:00
}
case "folder": {
2023-06-14 19:44:27 +00:00
buzz();
resetTimeout();
page = 0;
folderPath.push(entry.id);
folder = getFolder(folderPath);
render();
break;
2024-05-04 21:21:43 +00:00
}
default: {
resetTimeout();
break;
2024-05-04 21:21:43 +00:00
}
}
}
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) {
2023-06-14 19:44:27 +00:00
buzz(200);
return;
} else page--;
} else if (ud == -1) {
resetTimeout();
if (page == nPages - 1) {
2023-06-14 19:44:27 +00:00
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 = () => {
2023-06-14 19:44:27 +00:00
buzz();
if (folderPath.length == 0)
Bangle.showClock();
else {
folderPath.pop();
folder = getFolder(folderPath);
resetTimeout();
page = 0;
render();
}
}
2023-06-14 19:44:27 +00:00
/**
* Vibrate the watch if vibration is enabled
*/
2023-06-14 23:17:57 +00:00
let buzz = config.disableVibration ? () => {} : Bangle.buzz;
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setUI({
mode: 'custom',
back: onBackButton,
btn: onBackButton,
swipe: onSwipe,
touch: onTouch,
remove: () => { if (timeout) clearTimeout(timeout); }
});
resetTimeout();
render();
}