New options - meridian, short/long, prefix/suffix
parent
7fd240ec70
commit
a0877336f8
|
|
@ -0,0 +1,2 @@
|
||||||
|
0.01: New App!
|
||||||
|
0.02: New options for configs such as meridian, short/long formats, custom prefix/suffix
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
# Box Clock
|
||||||
|
|
||||||
|
Box Clock is a customizable clock app for Bangle.js 2 that features an interactive drag and drop interface and easy JSON configuration.
|
||||||
|
|
||||||
|
## Unique Features
|
||||||
|
|
||||||
|
__Drag & Drop:__
|
||||||
|
|
||||||
|
This intuitive feature allows you to reposition any element (box) on the clock face with ease. Tap on the box(s) you want to move and the border will show, drag into position and tap outside of the boxes to finish placing.
|
||||||
|
|
||||||
|
__Double Tap to Save:__
|
||||||
|
|
||||||
|
After you've found the perfect position for your boxes, you can save their positions with a quick double tap on the background. This makes it easy to ensure your custom layout is stored for future use. A save icon will appear momentarily to confirm that your configuration has been saved.
|
||||||
|
|
||||||
|
__JSON Configuration:__
|
||||||
|
|
||||||
|
Each box can be customized extensively via a simple JSON configuration. You can also add a custom text string to your configuration with the "string" attribute. Here's what an example configuration might look like:
|
||||||
|
|
||||||
|
## Config File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"customBox": {
|
||||||
|
"string": "Your text here",
|
||||||
|
"font": "CustomFont", // Custom fonts must be removed in setUI
|
||||||
|
"fontSize": 1,
|
||||||
|
"outline": 2,
|
||||||
|
"color": "#FF9900", // Use 6 or 3 digit hex color codes
|
||||||
|
"outlineColor": "bgH", // Or match system theme colors like this
|
||||||
|
"border": 65535, // Or use 16-bit RGB565 like this
|
||||||
|
"xPadding": 1,
|
||||||
|
"yPadding": -4,
|
||||||
|
"xOffset": 0,
|
||||||
|
"yOffset": 3,
|
||||||
|
"boxPos": { "x": 0.5, "y": 0.5 },
|
||||||
|
"prefix": "", // Adds a string to the beginning of the main string
|
||||||
|
"suffix": "", // Adds a string to the end of the main string
|
||||||
|
"disableSuffix": true, // Only used to remove the DayOfMonth suffix
|
||||||
|
"short": false // Gets long format value of time, meridian, date, or DoW
|
||||||
|
|
||||||
|
},
|
||||||
|
"bg": { // Can also be removed for no backround
|
||||||
|
"img": "YourImageName.img"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* **Box Name:** The name of your text box. The app includes functional support for "time", "date", "meridian", "dow" (Day of Week), "batt" (Battery), and "step" (Step count). You can add additional custom boxes with unique titles.
|
||||||
|
|
||||||
|
* **string:** The text string to be displayed inside the box.
|
||||||
|
|
||||||
|
* **font:** The font name given to g.setFont()
|
||||||
|
|
||||||
|
* **fontSize:** The size of the font.
|
||||||
|
|
||||||
|
* **outline:** The thickness of the outline around the text.
|
||||||
|
|
||||||
|
* **color:** The color of the text.
|
||||||
|
|
||||||
|
* **outlineColor:** The color of the text outline.
|
||||||
|
|
||||||
|
* **border:** The color of the box border when moving.
|
||||||
|
|
||||||
|
* **xPadding, yPadding:** Additional padding around the text inside the box.
|
||||||
|
|
||||||
|
* **xOffset, yOffset:** Offsets the text position within the box.
|
||||||
|
|
||||||
|
* **boxPos:** Initial position of the box on the screen. Values are fractions of the screen width (x) and height (y), so { "x": 0.5, "y": 0.5 } would be in the middle of the screen.
|
||||||
|
|
||||||
|
* **prefix:** Adds a string to the beginning of the main string. For example, you can set "prefix": "Steps: " to display "Steps: 100" for the step count.
|
||||||
|
|
||||||
|
* **suffix:** Adds a string to the end of the main string. For example, you can set "suffix": "%" to display "80%" for the battery percentage.
|
||||||
|
|
||||||
|
* **disableSuffix:** Applies only to the "date" box. Set to true to disable the DayOfMonth suffix. This is used to remove the "st","nd","rd", or "th" from the DayOfMonth number
|
||||||
|
|
||||||
|
* **short:** Set to false to get the long format value of time, meridian, date, or DayOfWeek. Short formats are used by default,
|
||||||
|
|
||||||
|
* **bg:** This specifies a custom background image, with the img property defining the name of the image file on the Bangle.js storage.
|
||||||
|
|
||||||
|
## Multiple Configurations
|
||||||
|
|
||||||
|
The app includes a settings menu that allows you to switch between different configurations. The selected configuration is stored in the default JSON file alongside the other configuration data using the selectedConfig property.
|
||||||
|
|
||||||
|
If the selectedConfig property is not present or is set to 0, the app will use the default configuration. To create additional configurations, create separate JSON files with the naming convention boxclk-N.json, where N is the configuration number. The settings menu will list all available configurations.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
This app was built and tested with Bangle.js 2.
|
||||||
|
|
||||||
|
## Feedback
|
||||||
|
|
||||||
|
If you have any issues or suggestions, please open an issue on this GitHub repository. Contributions to improve the application are also welcomed.
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
[stweedo](https://github.com/stweedo)
|
||||||
|
|
@ -0,0 +1,398 @@
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 1. Module dependencies and initial configurations
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
let storage = require("Storage");
|
||||||
|
let locale = require("locale");
|
||||||
|
let widgets = require("widget_utils");
|
||||||
|
let date = new Date();
|
||||||
|
let bgImage;
|
||||||
|
let configNumber = (storage.readJSON("boxclk.json", 1) || {}).selectedConfig || 0;
|
||||||
|
let fileName = 'boxclk' + (configNumber > 0 ? `-${configNumber}` : '') + '.json';
|
||||||
|
// Add a condition to check if the file exists, if it does not, default to 'boxclk.json'
|
||||||
|
if (!storage.read(fileName)) {
|
||||||
|
fileName = 'boxclk.json';
|
||||||
|
}
|
||||||
|
let boxesConfig = storage.readJSON(fileName, 1) || {};
|
||||||
|
let boxes = {};
|
||||||
|
let boxPos = {};
|
||||||
|
let isDragging = {};
|
||||||
|
let wasDragging = {};
|
||||||
|
let doubleTapTimer = null;
|
||||||
|
let g_setColor;
|
||||||
|
|
||||||
|
let saveIcon = require("heatshrink").decompress(atob("mEwwkEogA/AHdP/4AK+gWVDBQWNAAIuVGBAIB+UQdhMfGBAHBCxUAgIXHIwPyCxQwEJAgXB+MAl/zBwQGBn8ggQjBGAQXG+EA/4XI/8gBIQXTGAMPC6n/C6HzkREBC6YACC6QAFC57aHCYIXOOgLsEn4XPABIX/C6vykQAEl6/WgCQBC5imFAAT2BC5gCBI4oUCC5x0IC/4X/C4K8Bl4XJ+TCCC4wKBABkvC4tEEoMQCxcBB4IWEC4XyDBUBFwIXGJAIAOIwowDABoWGGB4uHDBwWJAH4AzA"));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 2. Graphical and visual configurations
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
let w = g.getWidth();
|
||||||
|
let h = g.getHeight();
|
||||||
|
let totalWidth, totalHeight;
|
||||||
|
let drawTimeout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 3. Touchscreen Handlers
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
let touchHandler;
|
||||||
|
let dragHandler;
|
||||||
|
let movementDistance = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 4. Font loading function
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
let loadCustomFont = function() {
|
||||||
|
Graphics.prototype.setFontBrunoAce = function() {
|
||||||
|
// Actual height 23 (24 - 2)
|
||||||
|
return this.setFontCustom(
|
||||||
|
E.toString(require('heatshrink').decompress(atob('ABMHwADBh4DKg4bKgIPDAYUfAYV/AYX/AQMD/gmC+ADBn/AByE/GIU8AYUwLxcfAYX/8AnB//4JIP/FgMP4F+CQQBBjwJBFYRbBAd43DHoJpBh/g/xPEK4ZfDgEEORKDDAY8////wADLfZrTCgITBnhEBAYJMBAYMPw4DCM4QDjhwDCjwDBn0+AYMf/gDBh/4AYMH+ADBLpc4ToK/NGYZfnAYcfL4U/x5fBW4LvB/7vC+LvBgHAsBfIn76Cn4WBcYQDFEgJ+CQQYDyH4L/BAZbHLNYjjCAZc8ngDunycBZ4KkBa4KwBnEHY4UB+BfMgf/ZgMH/4XBc4cf4F/gE+ZgRjwAYcfj5jBM4U4M4RQBM4UA8BjIngDFEYJ8BAYUDAYQvCM4ZxBC4V+AYQvBnkBQ4M8gabBJQPAI4WAAYM/GYQaBAYJKCnqyCn5OCn4aBAYIaBAYJPCU4IABnBhIuDXCFAMD+Z/BY4IDBQwOPwEfv6TDAYUPAcwrDAYQ7BAYY/BI4cD8bLCK4RfEAA0BRYTeDcwIrFn0Pw43Bg4DugYDBjxBBU4SvDMYMH/5QBgP/LAQAP8EHN4UPwADHB4YAHA'))),
|
||||||
|
46,
|
||||||
|
atob("CBEdChgYGhgaGBsaCQ=="),
|
||||||
|
32|65536
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 5. Initial settings of boxes and their positions
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
for (let key in boxesConfig) {
|
||||||
|
if (key === 'bg' && boxesConfig[key].img) {
|
||||||
|
bgImage = storage.read(boxesConfig[key].img);
|
||||||
|
} else if (key !== 'selectedConfig') {
|
||||||
|
boxes[key] = Object.assign({}, boxesConfig[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let boxKeys = Object.keys(boxes);
|
||||||
|
|
||||||
|
boxKeys.forEach((key) => {
|
||||||
|
let boxConfig = boxes[key];
|
||||||
|
boxPos[key] = {
|
||||||
|
x: w * boxConfig.boxPos.x,
|
||||||
|
y: h * boxConfig.boxPos.y
|
||||||
|
};
|
||||||
|
isDragging[key] = false;
|
||||||
|
wasDragging[key] = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 6. Text and drawing functions
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Overwrite the setColor function to allow the
|
||||||
|
// use of (x) in g.theme.x as a string
|
||||||
|
// in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH")
|
||||||
|
let modSetColor = function() {
|
||||||
|
// Save the original setColor function
|
||||||
|
g_setColor = g.setColor;
|
||||||
|
// Overwrite setColor with the new function
|
||||||
|
g.setColor = function(color) {
|
||||||
|
if (typeof color === "string" && color in g.theme) {
|
||||||
|
g_setColor.call(g, g.theme[color]);
|
||||||
|
} else {
|
||||||
|
g_setColor.call(g, color);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let restoreSetColor = function() {
|
||||||
|
// Restore the original setColor function
|
||||||
|
if (g_setColor) {
|
||||||
|
g.setColor = g_setColor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Overwrite the drawString function
|
||||||
|
let g_drawString = g.drawString;
|
||||||
|
g.drawString = function(box, str, x, y) {
|
||||||
|
outlineText(box, str, x, y);
|
||||||
|
g.setColor(box.color);
|
||||||
|
g_drawString.call(g, str, x, y);
|
||||||
|
};
|
||||||
|
|
||||||
|
let outlineText = function(box, str, x, y) {
|
||||||
|
let px = box.outline;
|
||||||
|
let dx = [-px, 0, px, -px, px, -px, 0, px];
|
||||||
|
let dy = [-px, -px, -px, 0, 0, px, px, px];
|
||||||
|
g.setColor(box.outlineColor);
|
||||||
|
for (let i = 0; i < dx.length; i++) {
|
||||||
|
g_drawString.call(g, str, x + dx[i], y + dy[i]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let calcBoxSize = function(boxItem) {
|
||||||
|
g.reset();
|
||||||
|
g.setFontAlign(0,0);
|
||||||
|
g.setFont(boxItem.font, boxItem.fontSize);
|
||||||
|
let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline;
|
||||||
|
let fontHeight = g.getFontHeight() + 2 * boxItem.outline;
|
||||||
|
totalWidth = strWidth + 2 * boxItem.xPadding;
|
||||||
|
totalHeight = fontHeight + 2 * boxItem.yPadding;
|
||||||
|
};
|
||||||
|
|
||||||
|
let calcBoxPos = function(boxKey) {
|
||||||
|
return {
|
||||||
|
x1: boxPos[boxKey].x - totalWidth / 2,
|
||||||
|
y1: boxPos[boxKey].y - totalHeight / 2,
|
||||||
|
x2: boxPos[boxKey].x + totalWidth / 2,
|
||||||
|
y2: boxPos[boxKey].y + totalHeight / 2
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let displaySaveIcon = function() {
|
||||||
|
draw(boxes);
|
||||||
|
g.drawImage(saveIcon, w / 2 - 24, h / 2 - 24);
|
||||||
|
// Display save icon for 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
g.clearRect(w / 2 - 24, h / 2 - 24, w / 2 + 24, h / 2 + 24);
|
||||||
|
draw(boxes);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 7. String forming helper functions
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
let isBool = function(val, defaultVal) {
|
||||||
|
return typeof val !== 'undefined' ? Boolean(val) : defaultVal;
|
||||||
|
};
|
||||||
|
|
||||||
|
let getDate = function(short, disableSuffix) {
|
||||||
|
const date = new Date();
|
||||||
|
const dayOfMonth = date.getDate();
|
||||||
|
const month = short ? locale.month(date, 0) : locale.month(date, 1);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
let suffix;
|
||||||
|
if ([1, 21, 31].includes(dayOfMonth)) {
|
||||||
|
suffix = "st";
|
||||||
|
} else if ([2, 22].includes(dayOfMonth)) {
|
||||||
|
suffix = "nd";
|
||||||
|
} else if ([3, 23].includes(dayOfMonth)) {
|
||||||
|
suffix = "rd";
|
||||||
|
} else {
|
||||||
|
suffix = "th";
|
||||||
|
}
|
||||||
|
let dayOfMonthStr = short ? dayOfMonth : (disableSuffix ? dayOfMonth : dayOfMonth + suffix);
|
||||||
|
return month + " " + dayOfMonthStr + (short ? '' : (", " + year)); // not including year for short version
|
||||||
|
};
|
||||||
|
|
||||||
|
let getDayOfWeek = function(date, short) {
|
||||||
|
return locale.dow(date, short ? 1 : 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
locale.meridian = function(date, short) {
|
||||||
|
let hours = date.getHours();
|
||||||
|
let meridian = hours >= 12 ? 'PM' : 'AM';
|
||||||
|
return short ? meridian[0] : meridian;
|
||||||
|
};
|
||||||
|
|
||||||
|
let modString = function(boxItem, data) {
|
||||||
|
let prefix = boxItem.prefix || '';
|
||||||
|
let suffix = boxItem.suffix || '';
|
||||||
|
return prefix + data + suffix;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 8. Main draw function
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
let draw = (function() {
|
||||||
|
let updatePerMinute = true; // variable to track the state of time display
|
||||||
|
|
||||||
|
return function(boxes) {
|
||||||
|
date = new Date();
|
||||||
|
g.clear();
|
||||||
|
if (bgImage) {
|
||||||
|
g.drawImage(bgImage, 0, 0);
|
||||||
|
}
|
||||||
|
if (boxes.time) {
|
||||||
|
boxes.time.string = modString(boxes.time, locale.time(date, isBool(boxes.time.short, true) ? 1 : 0));
|
||||||
|
updatePerMinute = isBool(boxes.time.short, true);
|
||||||
|
}
|
||||||
|
if (boxes.meridian) {
|
||||||
|
boxes.meridian.string = modString(boxes.meridian, locale.meridian(date, isBool(boxes.meridian.short, true)));
|
||||||
|
}
|
||||||
|
if (boxes.date) {
|
||||||
|
boxes.date.string = modString(boxes.date, getDate(isBool(boxes.date.short, true), isBool(boxes.date.disableSuffix, false)));
|
||||||
|
}
|
||||||
|
if (boxes.dow) {
|
||||||
|
boxes.dow.string = modString(boxes.dow, getDayOfWeek(date, isBool(boxes.dow.short, true)));
|
||||||
|
}
|
||||||
|
if (boxes.batt) {
|
||||||
|
boxes.batt.string = modString(boxes.batt, E.getBattery());
|
||||||
|
}
|
||||||
|
if (boxes.step) {
|
||||||
|
boxes.step.string = modString(boxes.step, Bangle.getStepCount());
|
||||||
|
}
|
||||||
|
boxKeys.forEach((boxKey) => {
|
||||||
|
let boxItem = boxes[boxKey];
|
||||||
|
calcBoxSize(boxItem);
|
||||||
|
const pos = calcBoxPos(boxKey);
|
||||||
|
if (isDragging[boxKey]) {
|
||||||
|
g.setColor(boxItem.border);
|
||||||
|
g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2);
|
||||||
|
}
|
||||||
|
g.drawString(
|
||||||
|
boxItem,
|
||||||
|
boxItem.string,
|
||||||
|
boxPos[boxKey].x + boxItem.xOffset,
|
||||||
|
boxPos[boxKey].y + boxItem.yOffset
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (!Object.values(isDragging).some(Boolean)) {
|
||||||
|
if (drawTimeout) clearTimeout(drawTimeout);
|
||||||
|
let interval = updatePerMinute ? 60000 - (Date.now() % 60000) : 1000;
|
||||||
|
drawTimeout = setTimeout(() => draw(boxes), interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 9. Helper function for touch event
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
let touchInText = function(e, boxItem, boxKey) {
|
||||||
|
calcBoxSize(boxItem);
|
||||||
|
const pos = calcBoxPos(boxKey);
|
||||||
|
return e.x >= pos.x1 &&
|
||||||
|
e.x <= pos.x2 &&
|
||||||
|
e.y >= pos.y1 &&
|
||||||
|
e.y <= pos.y2;
|
||||||
|
};
|
||||||
|
|
||||||
|
let deselectAllBoxes = function() {
|
||||||
|
Object.keys(isDragging).forEach((boxKey) => {
|
||||||
|
isDragging[boxKey] = false;
|
||||||
|
});
|
||||||
|
restoreSetColor();
|
||||||
|
widgets.show();
|
||||||
|
widgets.swipeOn();
|
||||||
|
modSetColor();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 10. Setup function to configure event handlers
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
let setup = function() {
|
||||||
|
// ------------------------------------
|
||||||
|
// Define the touchHandler function
|
||||||
|
// ------------------------------------
|
||||||
|
touchHandler = function(zone, e) {
|
||||||
|
wasDragging = Object.assign({}, isDragging);
|
||||||
|
let boxTouched = false;
|
||||||
|
boxKeys.forEach((boxKey) => {
|
||||||
|
if (touchInText(e, boxes[boxKey], boxKey)) {
|
||||||
|
isDragging[boxKey] = true;
|
||||||
|
wasDragging[boxKey] = true;
|
||||||
|
boxTouched = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!boxTouched) {
|
||||||
|
if (!Object.values(isDragging).some(Boolean)) { // check if no boxes are being dragged
|
||||||
|
deselectAllBoxes();
|
||||||
|
if (doubleTapTimer) {
|
||||||
|
clearTimeout(doubleTapTimer);
|
||||||
|
doubleTapTimer = null;
|
||||||
|
// Save boxesConfig on double tap outside of any box and when no boxes are being dragged
|
||||||
|
Object.keys(boxPos).forEach((boxKey) => {
|
||||||
|
boxesConfig[boxKey].boxPos.x = (boxPos[boxKey].x / w).toFixed(3);
|
||||||
|
boxesConfig[boxKey].boxPos.y = (boxPos[boxKey].y / h).toFixed(3);
|
||||||
|
});
|
||||||
|
storage.write(fileName, JSON.stringify(boxesConfig));
|
||||||
|
displaySaveIcon();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if any box is being dragged, just deselect all without saving
|
||||||
|
deselectAllBoxes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.values(wasDragging).some(Boolean) || !boxTouched) {
|
||||||
|
draw(boxes);
|
||||||
|
}
|
||||||
|
doubleTapTimer = setTimeout(() => {
|
||||||
|
doubleTapTimer = null;
|
||||||
|
}, 500); // Increase or decrease this value based on the desired double tap timing
|
||||||
|
movementDistance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// Define the dragHandler function
|
||||||
|
// ------------------------------------
|
||||||
|
dragHandler = function(e) {
|
||||||
|
// Calculate the movement distance
|
||||||
|
movementDistance += Math.abs(e.dx) + Math.abs(e.dy);
|
||||||
|
// Check if the movement distance exceeds a threshold
|
||||||
|
if (movementDistance > 1) {
|
||||||
|
boxKeys.forEach((boxKey) => {
|
||||||
|
if (isDragging[boxKey]) {
|
||||||
|
widgets.hide();
|
||||||
|
let boxItem = boxes[boxKey];
|
||||||
|
calcBoxSize(boxItem);
|
||||||
|
let newX = boxPos[boxKey].x + e.dx;
|
||||||
|
let newY = boxPos[boxKey].y + e.dy;
|
||||||
|
if (newX - totalWidth / 2 >= 0 &&
|
||||||
|
newX + totalWidth / 2 <= w &&
|
||||||
|
newY - totalHeight / 2 >= 0 &&
|
||||||
|
newY + totalHeight / 2 <= h ) {
|
||||||
|
boxPos[boxKey].x = newX;
|
||||||
|
boxPos[boxKey].y = newY;
|
||||||
|
}
|
||||||
|
const pos = calcBoxPos(boxKey);
|
||||||
|
g.clearRect(pos.x1, pos.y1, pos.x2, pos.y2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
draw(boxes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.on('touch', touchHandler);
|
||||||
|
Bangle.on('drag', dragHandler);
|
||||||
|
|
||||||
|
Bangle.setUI({
|
||||||
|
mode : "clock",
|
||||||
|
remove : function() {
|
||||||
|
// Remove event handlers, stop draw timer, remove custom font if used
|
||||||
|
Bangle.removeListener('touch', touchHandler);
|
||||||
|
Bangle.removeListener('drag', dragHandler);
|
||||||
|
if (drawTimeout) clearTimeout(drawTimeout);
|
||||||
|
drawTimeout = undefined;
|
||||||
|
delete Graphics.prototype.setFontBrunoAce;
|
||||||
|
// Restore original drawString function (no outlines)
|
||||||
|
g.drawString = g_drawString;
|
||||||
|
restoreSetColor();
|
||||||
|
widgets.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loadCustomFont();
|
||||||
|
draw(boxes);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* 11. Main execution part
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
widgets.swipeOn();
|
||||||
|
modSetColor();
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"time": {
|
||||||
|
"font": "BrunoAce",
|
||||||
|
"fontSize": 1,
|
||||||
|
"outline": 2,
|
||||||
|
"color": "#000",
|
||||||
|
"outlineColor": "#fff",
|
||||||
|
"border": "#000",
|
||||||
|
"xPadding": 0,
|
||||||
|
"yPadding": -4,
|
||||||
|
"xOffset": 0,
|
||||||
|
"yOffset": 3,
|
||||||
|
"boxPos": { "x": 0.633, "y": 0.16 }
|
||||||
|
},
|
||||||
|
"dow": {
|
||||||
|
"font": "6x8",
|
||||||
|
"fontSize": 2,
|
||||||
|
"outline": 1,
|
||||||
|
"color": "#000",
|
||||||
|
"outlineColor": "#fff",
|
||||||
|
"border": "#000",
|
||||||
|
"xPadding": -0.5,
|
||||||
|
"yPadding": 0.5,
|
||||||
|
"xOffset": 1,
|
||||||
|
"yOffset": 1,
|
||||||
|
"boxPos": { "x": 0.633, "y": 0.3 },
|
||||||
|
"short": false
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"font": "6x8",
|
||||||
|
"fontSize": 1,
|
||||||
|
"outline": 1,
|
||||||
|
"color": "#000",
|
||||||
|
"outlineColor": "#fff",
|
||||||
|
"border": "#000",
|
||||||
|
"xPadding": 0,
|
||||||
|
"yPadding": 0.5,
|
||||||
|
"xOffset": 1,
|
||||||
|
"yOffset": 1,
|
||||||
|
"boxPos": { "x": 0.633, "y": 0.39 },
|
||||||
|
"short": false
|
||||||
|
},
|
||||||
|
"batt": {
|
||||||
|
"font": "4x6",
|
||||||
|
"fontSize": 2,
|
||||||
|
"outline": 1,
|
||||||
|
"color": "#0ff",
|
||||||
|
"outlineColor": "#000",
|
||||||
|
"border": "#fff",
|
||||||
|
"xPadding": -0.5,
|
||||||
|
"yPadding": -0.5,
|
||||||
|
"xOffset": 2,
|
||||||
|
"yOffset": 1,
|
||||||
|
"boxPos": { "x": 0.92, "y": 0.95 },
|
||||||
|
"suffix": "%"
|
||||||
|
},
|
||||||
|
"bg": {
|
||||||
|
"img": "boxclk.beachhouse.img"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwkEogA/AEB5TCwVAC6aOCoED/4AQmAXDh4XR+AX5+URgERl4XR+KGEj4XP+cAgMz/8ziEAn4XOkEBCIfziECC5ouBl/zkMAiU/+QwGC4/wE4MgLwQFCQgoXHiEfO4Mj/8yO4PxgIXMHwMggb+DgRQBC5fygIPBn4xBiYFCiDDEC43xgfyLQJfCGoMvmDCEC5ABCVIJlBCoIXM+EPAIRgBAQIIDC542DC53xT4MfC4cCBAanLBQaWDBIgXo+YXXdgoXQbQcBd5YXHiAYHC5wAC+UQe4IXTDAMABAQXEgYPEiDQEAATdBAYMwC4ZUCK4aLHSoIACC5IuHSogXKCxAXHogXTCwQAFC5YUIC7b/EC6TFFC6IwJC5itBcwQXUbI7vBC5bFGAAgXMDBQXNJIQXUGBEEBog="))
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"id": "boxclk",
|
||||||
|
"name": "Box Clock",
|
||||||
|
"version": "0.02",
|
||||||
|
"description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background",
|
||||||
|
"icon": "app.png",
|
||||||
|
"screenshots": [
|
||||||
|
{"url":"screenshot.png"},
|
||||||
|
{"url":"screenshot-1.png"}
|
||||||
|
],
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"allow_emulator": true,
|
||||||
|
"storage": [
|
||||||
|
{"name":"boxclk.app.js","url":"app.js"},
|
||||||
|
{"name":"boxclk.settings.js","url":"settings.js"},
|
||||||
|
{"name":"boxclk.img","url":"icon.js","evaluate":true},
|
||||||
|
{"name":"boxclk.beachhouse.img","url":"beachhouse.js","evaluate":true}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
{"name":"boxclk.json","url":"boxclk.json"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
(function () {
|
||||||
|
let storage = require("Storage");
|
||||||
|
let fileRegex = /^boxclk-(\d+)\.json$/;
|
||||||
|
let selectedConfig;
|
||||||
|
let configs = {};
|
||||||
|
let hasDefaultConfig = false;
|
||||||
|
|
||||||
|
function getNextConfigNumber() {
|
||||||
|
let maxNumber = 0;
|
||||||
|
storage.list().forEach(file => {
|
||||||
|
let match = file.match(fileRegex);
|
||||||
|
if (match) {
|
||||||
|
let number = parseInt(match[1]);
|
||||||
|
if (number > maxNumber) {
|
||||||
|
maxNumber = number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return maxNumber + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelection(config) {
|
||||||
|
return function () {
|
||||||
|
selectedConfig = config === "Default" ? 0 : config;
|
||||||
|
menu["Cfg:"].value = selectedConfig === 0 ? "Default" : selectedConfig;
|
||||||
|
E.showMenu(menu);
|
||||||
|
|
||||||
|
// Retrieve existing data and update selectedConfig
|
||||||
|
let defaultConfig = storage.readJSON("boxclk.json", 1) || {};
|
||||||
|
defaultConfig.selectedConfig = selectedConfig;
|
||||||
|
storage.writeJSON("boxclk.json", defaultConfig);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let configFiles = [];
|
||||||
|
storage.list().forEach(file => {
|
||||||
|
let match = file.match(fileRegex);
|
||||||
|
if (match) {
|
||||||
|
configFiles.push({ file: file, number: parseInt(match[1]) });
|
||||||
|
} else if (file === "boxclk.json") {
|
||||||
|
hasDefaultConfig = true;
|
||||||
|
let defaultConfig = storage.readJSON(file, 1);
|
||||||
|
if (defaultConfig && defaultConfig.selectedConfig != null) {
|
||||||
|
// Check if corresponding config file exists
|
||||||
|
let configFileName = 'boxclk-' + defaultConfig.selectedConfig + '.json';
|
||||||
|
if (storage.read(configFileName)) {
|
||||||
|
// If it exists, assign selectedConfig
|
||||||
|
selectedConfig = defaultConfig.selectedConfig;
|
||||||
|
} else {
|
||||||
|
// If it does not exist, set selectedConfig to 0 and update boxclk.json
|
||||||
|
defaultConfig.selectedConfig = 0;
|
||||||
|
storage.writeJSON("boxclk.json", defaultConfig);
|
||||||
|
selectedConfig = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort the config files by number
|
||||||
|
configFiles.sort((a, b) => a.number - b.number);
|
||||||
|
|
||||||
|
configFiles.forEach(configFile => {
|
||||||
|
configs[configFile.number] = handleSelection(configFile.number);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selectedConfig) {
|
||||||
|
if (hasDefaultConfig) {
|
||||||
|
selectedConfig = "Default";
|
||||||
|
} else {
|
||||||
|
let nextConfigNumber = getNextConfigNumber();
|
||||||
|
selectedConfig = nextConfigNumber.toString();
|
||||||
|
configs[selectedConfig] = handleSelection(selectedConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let menu = {
|
||||||
|
'': { 'title': '-- Box Clock --' },
|
||||||
|
'< Back': () => Bangle.showClock(),
|
||||||
|
'Cfg:': {
|
||||||
|
value: selectedConfig === 0 ? "Default" : selectedConfig,
|
||||||
|
format: () => selectedConfig === 0 ? "Default" : selectedConfig
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hasDefaultConfig) {
|
||||||
|
menu['Default'] = handleSelection('Default');
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(configs).forEach(config => {
|
||||||
|
menu[config] = handleSelection(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
E.showMenu(menu);
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue