mirror of https://github.com/espruino/BangleApps
377 lines
14 KiB
JavaScript
377 lines
14 KiB
JavaScript
|
// TODO:
|
||
|
// - Add more /*LANG*/ tags for translations.
|
||
|
// - Check if there are chronlog storage files that should be added to tasks.
|
||
|
|
||
|
{
|
||
|
const storage = require("Storage");
|
||
|
let appData = storage.readJSON("chronlog.json", true) || {
|
||
|
currentTask : "default",
|
||
|
tasks : {
|
||
|
default: {
|
||
|
file : "chronlog_default.csv", // Existing default task log file
|
||
|
state : "stopped",
|
||
|
lineNumber : 0,
|
||
|
lastLine : "",
|
||
|
lastSyncedLine : "",
|
||
|
},
|
||
|
// Add more tasks as needed
|
||
|
},
|
||
|
};
|
||
|
let currentTask = appData.currentTask;
|
||
|
let tasks = appData.tasks;
|
||
|
delete appData;
|
||
|
|
||
|
let themeColors = g.theme;
|
||
|
|
||
|
let logEntry; // Avoid previous lint warning
|
||
|
|
||
|
// Function to draw the Start/Stop button with play and pause icons
|
||
|
let drawButton = ()=>{
|
||
|
var btnWidth = g.getWidth() - 40;
|
||
|
var btnHeight = 50;
|
||
|
var btnX = 20;
|
||
|
var btnY = (g.getHeight() - btnHeight) / 2;
|
||
|
var cornerRadius = 25;
|
||
|
|
||
|
var isStopped = tasks[currentTask].state === "stopped";
|
||
|
g.setColor(isStopped ? "#0F0" : "#F00"); // Set color to green when stopped and red when started
|
||
|
|
||
|
// Draw rounded corners of the button
|
||
|
g.fillCircle(btnX + cornerRadius, btnY + cornerRadius, cornerRadius);
|
||
|
g.fillCircle(btnX + btnWidth - cornerRadius, btnY + cornerRadius, cornerRadius);
|
||
|
g.fillCircle(btnX + cornerRadius, btnY + btnHeight - cornerRadius, cornerRadius);
|
||
|
g.fillCircle(btnX + btnWidth - cornerRadius, btnY + btnHeight - cornerRadius, cornerRadius);
|
||
|
|
||
|
// Draw rectangles to fill in the button
|
||
|
g.fillRect(btnX + cornerRadius, btnY, btnX + btnWidth - cornerRadius, btnY + btnHeight);
|
||
|
g.fillRect(btnX, btnY + cornerRadius, btnX + btnWidth, btnY + btnHeight - cornerRadius);
|
||
|
|
||
|
g.setColor(themeColors.bg); // Set icon color to contrast against the button's color
|
||
|
|
||
|
// Center the icon within the button
|
||
|
var iconX = btnX + btnWidth / 2;
|
||
|
var iconY = btnY + btnHeight / 2;
|
||
|
|
||
|
if (isStopped) {
|
||
|
// Draw play icon
|
||
|
var playSize = 10; // Side length of the play triangle
|
||
|
var offset = playSize / Math.sqrt(3) - 3;
|
||
|
g.fillPoly([
|
||
|
iconX - playSize, iconY - playSize + offset,
|
||
|
iconX - playSize, iconY + playSize + offset,
|
||
|
iconX + playSize * 2 / Math.sqrt(3), iconY + offset
|
||
|
]);
|
||
|
} else {
|
||
|
// Draw pause icon
|
||
|
var barWidth = 5; // Width of pause bars
|
||
|
var barHeight = btnHeight / 2; // Height of pause bars
|
||
|
var barSpacing = 5; // Spacing between pause bars
|
||
|
g.fillRect(iconX - barSpacing / 2 - barWidth, iconY - barHeight / 2, iconX - barSpacing / 2, iconY + barHeight / 2);
|
||
|
g.fillRect(iconX + barSpacing / 2, iconY - barHeight / 2, iconX + barSpacing / 2 + barWidth, iconY + barHeight / 2);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
let drawHamburgerMenu = ()=>{
|
||
|
var x = g.getWidth() / 2; // Center the hamburger menu horizontally
|
||
|
var y = (7/8)*g.getHeight(); // Position it near the bottom
|
||
|
var lineLength = 18; // Length of the hamburger lines
|
||
|
var spacing = 6; // Space between the lines
|
||
|
|
||
|
g.setColor(themeColors.fg); // Set color to foreground color for the icon
|
||
|
// Draw three horizontal lines
|
||
|
for (var i = -1; i <= 1; i++) {
|
||
|
g.fillRect(x - lineLength/2, y + i * spacing - 1, x + lineLength/2, y + i * spacing + 1);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Function to draw the task name centered between the widget field and the start/stop button
|
||
|
let drawTaskName = ()=>{
|
||
|
g.setFont("Vector", 20); // Set a smaller font for the task name display
|
||
|
|
||
|
// Calculate position to center the task name horizontally
|
||
|
var x = (g.getWidth()) / 2;
|
||
|
|
||
|
// Calculate position to center the task name vertically between the widget field and the start/stop button
|
||
|
var y = g.getHeight()/4; // Center vertically
|
||
|
|
||
|
g.setColor(themeColors.fg).setFontAlign(0,0); // Set text color to foreground color
|
||
|
g.drawString(currentTask, x, y); // Draw the task name centered on the screen
|
||
|
};
|
||
|
|
||
|
// Function to draw the last log entry of the current task
|
||
|
let drawLastLogEntry = ()=>{
|
||
|
g.setFont("Vector", 10); // Set a smaller font for the task name display
|
||
|
|
||
|
// Calculate position to center the log entry horizontally
|
||
|
var x = (g.getWidth()) / 2;
|
||
|
|
||
|
// Calculate position to place the log entry properly between the start/stop button and hamburger menu
|
||
|
var btnBottomY = (g.getHeight() + 50) / 2; // Y-coordinate of the bottom of the start/stop button
|
||
|
var menuBtnYTop = g.getHeight() * (5 / 6); // Y-coordinate of the top of the hamburger menu button
|
||
|
var y = btnBottomY + (menuBtnYTop - btnBottomY) / 2 + 2; // Center vertically between button and menu
|
||
|
|
||
|
g.setColor(themeColors.fg).setFontAlign(0,0); // Set text color to foreground color
|
||
|
g.drawString(g.wrapString(tasks[currentTask].lastLine, 150).join("\n"), x, y);
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
// Helper function to read the last log entry from the current task's log file
|
||
|
let updateLastLogEntry = ()=>{
|
||
|
var filename = tasks[currentTask].file;
|
||
|
var file = require("Storage").open(filename, "r");
|
||
|
var lastLine = "";
|
||
|
var line;
|
||
|
while ((line = file.readLine()) !== undefined) {
|
||
|
lastLine = line; // Keep reading until the last line
|
||
|
}
|
||
|
tasks[currentTask].lastLine = lastLine;
|
||
|
};
|
||
|
*/
|
||
|
|
||
|
// Main UI drawing function
|
||
|
let drawMainMenu = ()=>{
|
||
|
g.clear();
|
||
|
Bangle.drawWidgets(); // Draw any active widgets
|
||
|
g.setColor(themeColors.bg); // Set color to theme's background color
|
||
|
g.fillRect(Bangle.appRect); // Fill the app area with the background color
|
||
|
|
||
|
drawTaskName(); // Draw the centered task name
|
||
|
drawLastLogEntry(); // Draw the last log entry of the current task
|
||
|
drawButton(); // Draw the Start/Stop toggle button
|
||
|
drawHamburgerMenu(); // Draw the hamburger menu button icon
|
||
|
|
||
|
//g.flip(); // Send graphics to the display
|
||
|
};
|
||
|
|
||
|
// Function to toggle the active state
|
||
|
let toggleChronlog = ()=>{
|
||
|
var dateObj = new Date();
|
||
|
var dateObjStrSplit = dateObj.toString().split(" ");
|
||
|
var currentTime = dateObj.getFullYear().toString() + "-" + (dateObj.getMonth()<10?"0":"") + dateObj.getMonth().toString() + "-" + (dateObj.getDate()<10?"0":"") + dateObj.getDate().toString() + "T" + (dateObj.getHours()<10?"0":"") + dateObj.getHours().toString() + ":" + (dateObj.getMinutes()<10?"0":"") + dateObj.getMinutes().toString() + ":" + (dateObj.getSeconds()<10?"0":"") + dateObj.getSeconds().toString() + " " + dateObjStrSplit[dateObjStrSplit.length-1];
|
||
|
|
||
|
tasks[currentTask].lineNumber = Number(tasks[currentTask].lineNumber) + 1;
|
||
|
logEntry = tasks[currentTask].lineNumber + (tasks[currentTask].state === "stopped" ? ",Start," : ",Stop,") + currentTime + "\n";
|
||
|
var filename = tasks[currentTask].file;
|
||
|
|
||
|
// Open the appropriate file and append the log entry
|
||
|
var file = require("Storage").open(filename, "a");
|
||
|
file.write(logEntry);
|
||
|
tasks[currentTask].lastLine = logEntry;
|
||
|
|
||
|
// Toggle the state and update the button text
|
||
|
tasks[currentTask].state = tasks[currentTask].state === "stopped" ? "started" : "stopped";
|
||
|
drawMainMenu(); // Redraw the main UI
|
||
|
};
|
||
|
|
||
|
// Define the touch handler function for the main menu
|
||
|
let handleMainMenuTouch = (button, xy)=>{
|
||
|
var btnTopY = (g.getHeight() - 50) / 2;
|
||
|
var btnBottomY = btnTopY + 50;
|
||
|
var menuBtnYTop = (7/8)*g.getHeight() - 15;
|
||
|
var menuBtnYBottom = (7/8)*g.getHeight() + 15;
|
||
|
var menuBtnXLeft = (g.getWidth() / 2) - 15;
|
||
|
var menuBtnXRight = (g.getWidth() / 2) + 15;
|
||
|
|
||
|
// Detect if the touch is within the toggle button area
|
||
|
if (xy.x >= 20 && xy.x <= (g.getWidth() - 20) && xy.y > btnTopY && xy.y < btnBottomY) {
|
||
|
toggleChronlog();
|
||
|
}
|
||
|
// Detect if the touch is within the hamburger menu button area
|
||
|
else if (xy.x >= menuBtnXLeft && xy.x <= menuBtnXRight && xy.y >= menuBtnYTop && xy.y <= menuBtnYBottom) {
|
||
|
showMenu();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Function to attach the touch event listener
|
||
|
let setMainUI = ()=>{
|
||
|
Bangle.setUI({
|
||
|
mode: "custom",
|
||
|
back: load,
|
||
|
touch: handleMainMenuTouch
|
||
|
});
|
||
|
};
|
||
|
|
||
|
let saveAppState = ()=>{
|
||
|
let appData = {
|
||
|
currentTask : currentTask,
|
||
|
tasks : tasks,
|
||
|
};
|
||
|
require("Storage").writeJSON("chronlog.json", appData);
|
||
|
};
|
||
|
// Set up a listener for the 'kill' event
|
||
|
E.on('kill', saveAppState);
|
||
|
|
||
|
// Function to switch to a selected task
|
||
|
let switchTask = (taskName)=>{
|
||
|
currentTask = taskName; // Update the current task
|
||
|
|
||
|
// Reinitialize the UI elements
|
||
|
setMainUI();
|
||
|
drawMainMenu(); // Redraw UI to reflect the task change and the button state
|
||
|
};
|
||
|
|
||
|
// Function to create a new task
|
||
|
let createNewTask = ()=>{
|
||
|
// Prompt the user to input the task's name
|
||
|
require("textinput").input({
|
||
|
text: "" // Default empty text for new task
|
||
|
}).then(result => {
|
||
|
var taskName = result; // Store the result from text input
|
||
|
if (taskName) {
|
||
|
if (tasks.hasOwnProperty(taskName)) {
|
||
|
// Task already exists, handle this case as needed
|
||
|
E.showAlert(/*LANG*/"Task already exists", "Error").then(drawMainMenu);
|
||
|
} else {
|
||
|
// Create a new task log file for the new task
|
||
|
var filename = "chronlog_" + taskName.replace(/\W+/g, "_") + ".csv";
|
||
|
tasks[taskName] = {
|
||
|
file : filename,
|
||
|
state : "stopped",
|
||
|
lineNumber : 0,
|
||
|
lastLine : "",
|
||
|
lastSyncedLine : "",
|
||
|
};
|
||
|
|
||
|
currentTask = taskName;
|
||
|
|
||
|
setMainUI();
|
||
|
drawMainMenu(); // Redraw UI with the new task
|
||
|
}
|
||
|
} else {
|
||
|
setMainUI();
|
||
|
drawMainMenu(); // User cancelled, redraw main menu
|
||
|
}
|
||
|
}).catch(e => {
|
||
|
console.log("Text input error", e);
|
||
|
setMainUI();
|
||
|
drawMainMenu(); // In case of error also redraw main menu
|
||
|
});
|
||
|
};
|
||
|
|
||
|
// Function to display the list of tasks for selection
|
||
|
let chooseTask = ()=>{
|
||
|
// Construct the tasks menu from the tasks object
|
||
|
var taskMenu = {
|
||
|
"": { "title": /*LANG*/"Choose Task",
|
||
|
"back" : function() {
|
||
|
setMainUI(); // Reattach when the menu is closed
|
||
|
drawMainMenu(); // Cancel task selection
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
for (var taskName in tasks) {
|
||
|
if (!tasks.hasOwnProperty(taskName)) continue;
|
||
|
taskMenu[taskName] = (function(name) {
|
||
|
return function() {
|
||
|
switchTask(name);
|
||
|
};
|
||
|
})(taskName);
|
||
|
}
|
||
|
|
||
|
// Add a menu option for creating a new task
|
||
|
taskMenu[/*LANG*/"Create New Task"] = createNewTask;
|
||
|
|
||
|
E.showMenu(taskMenu); // Display the task selection
|
||
|
};
|
||
|
|
||
|
// Function to annotate the current or last work session
|
||
|
let annotateTask = ()=>{
|
||
|
|
||
|
// Prompt the user to input the annotation text
|
||
|
require("textinput").input({
|
||
|
text: "" // Default empty text for annotation
|
||
|
}).then(result => {
|
||
|
var annotationText = result.trim();
|
||
|
if (annotationText) {
|
||
|
// Append annotation to the last or current log entry
|
||
|
tasks[currentTask].lineNumber ++;
|
||
|
var annotatedEntry = tasks[currentTask].lineNumber + /*LANG*/",Note," + annotationText + "\n";
|
||
|
var filename = tasks[currentTask].file;
|
||
|
var file = require("Storage").open(filename, "a");
|
||
|
file.write(annotatedEntry);
|
||
|
tasks[currentTask].lastLine = annotatedEntry;
|
||
|
setMainUI();
|
||
|
drawMainMenu(); // Redraw UI after adding the annotation
|
||
|
} else {
|
||
|
// User cancelled, so we do nothing and just redraw the main menu
|
||
|
setMainUI();
|
||
|
drawMainMenu();
|
||
|
}
|
||
|
}).catch(e => {
|
||
|
console.log("Annotation input error", e);
|
||
|
setMainUI();
|
||
|
drawMainMenu(); // In case of error also redraw main menu
|
||
|
});
|
||
|
};
|
||
|
|
||
|
let syncToAndroid = (taskName, isFullSync)=>{
|
||
|
let mode = "a";
|
||
|
if (isFullSync) mode = "w";
|
||
|
let lastSyncedLine = tasks[taskName].lastSyncedLine || 0;
|
||
|
let taskNameValidFileName = taskName.replace(" ","_"); // FIXME: Should use something similar to replaceAll using a regular expression to catch all illegal characters.
|
||
|
|
||
|
let storageFile = require("Storage").open("chronlog_"+taskNameValidFileName+".csv", "r");
|
||
|
let contents = storageFile.readLine();
|
||
|
let lineNumber = contents ? contents.slice(0, contents.indexOf(",")) : 0;
|
||
|
let shouldSyncLine = ()=>{return (contents && (isFullSync || (Number(lineNumber)>Number(lastSyncedLine))));};
|
||
|
let doSyncLine = (mde)=>{Bluetooth.println(JSON.stringify({t:"file", n:"chronlog_"+taskNameValidFileName+".csv", c:contents, m:mde}));};
|
||
|
|
||
|
if (shouldSyncLine()) doSyncLine(mode);
|
||
|
contents = storageFile.readLine();
|
||
|
while (contents) {
|
||
|
lineNumber = contents.slice(0, contents.indexOf(",")); // Could theoretically do with `lineNumber++`, but this is more robust in case numbering in file ended up irregular.
|
||
|
if (shouldSyncLine()) doSyncLine("a");
|
||
|
contents = storageFile.readLine();
|
||
|
}
|
||
|
tasks[taskName].lastSyncedLine = lineNumber;
|
||
|
};
|
||
|
|
||
|
// Function to display the list of tasks for selection
|
||
|
let syncTasks = ()=>{
|
||
|
let isToDoFullSync = false;
|
||
|
// Construct the tasks menu from the tasks object
|
||
|
var syncMenu = {
|
||
|
"": { "title": /*LANG*/"Sync Tasks",
|
||
|
"back" : function() {
|
||
|
setMainUI(); // Reattach when the menu is closed
|
||
|
drawMainMenu(); // Cancel task selection
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
syncMenu[/*LANG*/"Full Resyncs"] = {
|
||
|
value: !!isToDoFullSync, // !! converts undefined to false
|
||
|
onchange: ()=>{
|
||
|
isToDoFullSync = !isToDoFullSync
|
||
|
},
|
||
|
}
|
||
|
for (var taskName in tasks) {
|
||
|
if (!tasks.hasOwnProperty(taskName)) continue;
|
||
|
syncMenu[taskName] = (function(name) {
|
||
|
return function() {syncToAndroid(name,isToDoFullSync);};
|
||
|
})(taskName);
|
||
|
}
|
||
|
|
||
|
E.showMenu(syncMenu); // Display the task selection
|
||
|
};
|
||
|
|
||
|
let showMenu = ()=>{
|
||
|
var menu = {
|
||
|
"": { "title": /*LANG*/"Menu",
|
||
|
"back": function() {
|
||
|
setMainUI(); // Reattach when the menu is closed
|
||
|
drawMainMenu(); // Redraw the main UI when closing the menu
|
||
|
},
|
||
|
},
|
||
|
/*LANG*/"Annotate": annotateTask, // Now calls the real annotation function
|
||
|
/*LANG*/"Change Task": chooseTask, // Opens the task selection screen
|
||
|
/*LANG*/"Sync to Android": syncTasks,
|
||
|
};
|
||
|
E.showMenu(menu);
|
||
|
};
|
||
|
|
||
|
Bangle.loadWidgets();
|
||
|
drawMainMenu(); // Draw the main UI when the app starts
|
||
|
// When the application starts, attach the touch event listener
|
||
|
setMainUI();
|
||
|
}
|