Prototype for web interface timer editor
parent
1626615bd5
commit
7132dc8ce2
|
|
@ -0,0 +1,448 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
<style>
|
||||
body {
|
||||
overflow-x: auto;
|
||||
margin: 0.5em;
|
||||
}
|
||||
.timer-block:nth-child(odd) {
|
||||
background-color: #eee;
|
||||
}
|
||||
.timer-block:nth-child(even) {
|
||||
background-color: #fff;
|
||||
}
|
||||
.timer-block div {
|
||||
padding: 0.2em;
|
||||
}
|
||||
.timer-block label {
|
||||
display: inline-block;
|
||||
}
|
||||
.timer-block .vibrate {
|
||||
width: 5em;
|
||||
}
|
||||
.timer-block input[type="number"] {
|
||||
width: 3em;
|
||||
}
|
||||
.timer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.timer-header button {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.btn-move-up, .btn-move-down {
|
||||
width: 2em;
|
||||
}
|
||||
.timer-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5em;
|
||||
}
|
||||
#main-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<div id="content">Loading...</div>
|
||||
<script>
|
||||
const DATA_VERSION = 0;
|
||||
|
||||
// Test data
|
||||
var testTimers = [
|
||||
{
|
||||
cls: "PrimitiveTimer",
|
||||
version: 0,
|
||||
origin: 10,
|
||||
rate: -0.001,
|
||||
name: "",
|
||||
id: 9,
|
||||
chain_id: null,
|
||||
start_time: 1747250975164,
|
||||
pause_time: 1747250975164,
|
||||
vibrate_pattern: ";;;",
|
||||
buzz_count: 4
|
||||
},
|
||||
{
|
||||
cls: "PrimitiveTimer",
|
||||
version: 0,
|
||||
origin: 10,
|
||||
rate: -0.001,
|
||||
name: "",
|
||||
id: 11,
|
||||
chain_id: 10,
|
||||
start_time: 1746824262928,
|
||||
pause_time: 1746824266564,
|
||||
vibrate_pattern: ";;;",
|
||||
buzz_count: 4
|
||||
},
|
||||
{
|
||||
cls: "PrimitiveTimer",
|
||||
version: 0,
|
||||
origin: 5,
|
||||
rate: -0.001,
|
||||
name: "",
|
||||
id: 10,
|
||||
chain_id: 11,
|
||||
start_time: 1746824264739,
|
||||
pause_time: 1746824264739,
|
||||
vibrate_pattern: ";;;",
|
||||
buzz_count: 4
|
||||
},
|
||||
{
|
||||
cls: "PrimitiveTimer",
|
||||
version: 0,
|
||||
origin: 600,
|
||||
rate: -0.001,
|
||||
name: "10 min",
|
||||
id: 8,
|
||||
chain_id: null,
|
||||
start_time: 1746738217165,
|
||||
pause_time: 1746738560210,
|
||||
vibrate_pattern: ";;;",
|
||||
buzz_count: 4
|
||||
},
|
||||
{
|
||||
cls: "PrimitiveTimer",
|
||||
version: 0,
|
||||
origin: 300,
|
||||
rate: -0.001,
|
||||
name: "",
|
||||
id: 6,
|
||||
chain_id: null,
|
||||
start_time: 1746664492975,
|
||||
pause_time: 1746664492975,
|
||||
vibrate_pattern: ";;;",
|
||||
buzz_count: 4
|
||||
}
|
||||
];
|
||||
|
||||
var userTimers = [];
|
||||
|
||||
function onInit() {
|
||||
document.getElementById("content").innerHTML = `
|
||||
<h1>Timers</h1>
|
||||
<div id="main-buttons">
|
||||
<button id="btn-reload-timers" class="btn btn-primary">Reload Timers…</button>
|
||||
<button id="btn-add-timer" class="btn btn-primary">Add Timer</button>
|
||||
<button id="btn-save-timers" class="btn btn-primary">Save Timers</button>
|
||||
</div>
|
||||
<div id="timertable"></div>
|
||||
`;
|
||||
document.getElementById('btn-reload-timers').addEventListener('click', reloadTimers);
|
||||
document.getElementById('btn-add-timer').addEventListener('click', addTimer);
|
||||
document.getElementById('btn-save-timers').addEventListener('click', saveTimers);
|
||||
userTimers = loadTestTimers();
|
||||
// userTimers = loadTimers();
|
||||
updateTimerBlocks();
|
||||
}
|
||||
|
||||
function loadTimers() {
|
||||
var loadedTimers
|
||||
Util.readStorageJSON('tevtimer.timers.json', timers => {
|
||||
loadedTimers = timers;
|
||||
});
|
||||
return loadedTimers;
|
||||
}
|
||||
|
||||
function loadTestTimers() {
|
||||
// Return a copy of testTimers
|
||||
let t = [];
|
||||
for (let i = 0; i < testTimers.length; i++) {
|
||||
let timer = JSON.parse(JSON.stringify(testTimers[i]));
|
||||
t.push(timer);
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
function getTimerById(timers, id) {
|
||||
for (timer of timers) {
|
||||
if (timer.id == id) {
|
||||
return timer;
|
||||
}
|
||||
}
|
||||
console.warn(`Timer with ID ${id} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
function find_nextId() {
|
||||
let maxId = 0;
|
||||
for (let timer of userTimers) {
|
||||
if (timer.id > maxId) {
|
||||
maxId = timer.id;
|
||||
}
|
||||
}
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
function updateTimerBlocks() {
|
||||
// Track the currently focused element
|
||||
const activeElement = document.activeElement;
|
||||
const activeElementId = activeElement ? activeElement.id : null;
|
||||
|
||||
// Re-render the table
|
||||
document.getElementById('timertable').innerHTML = timerBlocks(userTimers);
|
||||
updateAtEndDropdowns();
|
||||
|
||||
// Reattach button handlers
|
||||
attachButtonHandlers();
|
||||
|
||||
// Handle input changes
|
||||
attachInputHandlers();
|
||||
|
||||
// Restore focus to the previously focused element
|
||||
if (activeElementId) {
|
||||
let elementToFocus = document.getElementById(activeElementId);
|
||||
|
||||
// If the original element no longer exists, focus on a fallback
|
||||
if (!elementToFocus) {
|
||||
// Extract the row index
|
||||
const index = parseInt(activeElementId.split('-')[1], 10);
|
||||
if (activeElementId.startsWith('delete-') && index < userTimers.length) {
|
||||
elementToFocus = document.getElementById(`delete-${index}`);
|
||||
} else if (index > 0) {
|
||||
elementToFocus = document.getElementById(`delete-${index - 1}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore focus if a valid element is found
|
||||
if (elementToFocus) {
|
||||
elementToFocus.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function timerBlocks(timers) {
|
||||
let blocks = '';
|
||||
|
||||
for (let i = 0; i < timers.length; i++) {
|
||||
let timer = timers[i];
|
||||
|
||||
// Assumes timer.rate is 0.001 (seconds), as this
|
||||
// is the only rate used in the app
|
||||
if (timer.rate != -0.001) {
|
||||
console.error('Unsupported timer rate');
|
||||
continue;
|
||||
}
|
||||
let h = Math.floor(timer.origin / 3600);
|
||||
let m = Math.floor((timer.origin % 3600) / 60);
|
||||
let s = Math.floor(timer.origin % 60);
|
||||
let atEndTimer = timer.chain_id ? getTimerById(timers, timer.chain_id) : null;
|
||||
let atEndSelected = atEndTimer ? atEndTimer.id : 'null';
|
||||
|
||||
blocks += `
|
||||
<div class="timer-block" id="timer-${i}">
|
||||
<div class="timer-header">
|
||||
<span>Timer ${i + 1}</span>
|
||||
<div class="timer-controls">
|
||||
${i > 0
|
||||
? `<button id="move-up-${i}" class="btn btn-primary btn-move-up" title="Move up">↑</button>`
|
||||
: ''}
|
||||
${i < timers.length - 1
|
||||
? `<button id="move-down-${i}" class="btn btn-primary btn-move-down" title="Move down">↓</button>`
|
||||
: ''}
|
||||
<button id="delete-${i}" class="btn btn-danger btn-delete" title="Delete">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timer-name">
|
||||
<label>Name: <input type="text" id="name-${i}" value="${timer.name}" maxlength="25" /></label>
|
||||
</div>
|
||||
<div class="timer-start">
|
||||
<label>Hrs: <input type="number" id="hours-${i}" value="${h}" min="0" max="99" /></label>
|
||||
<label>Mins: <input type="number" id="minutes-${i}" value="${m}" min="0" max="59" /></label>
|
||||
<label>Secs: <input type="number" id="seconds-${i}" value="${s}" min="0" max="59" /></label>
|
||||
</div>
|
||||
<div class="timer-at-end">
|
||||
<label>At End:
|
||||
<select id="atend-${i}"></select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="timer-settings">
|
||||
<label>Vibrate Pattern: <input type="text" class="vibrate" id="vibrate-${i}" value="${timer.vibrate_pattern}" maxlength="8" /></label>
|
||||
<label>Buzz Count: <input type="number" id="buzz-${i}" value="${timer.buzz_count}" min="0" max="15" /></label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function attachButtonHandlers() {
|
||||
// Handle "Move up" buttons
|
||||
document.querySelectorAll('.btn-move-up').forEach((button, index) => {
|
||||
button.addEventListener('click', () => moveTimerUp(index + 1));
|
||||
});
|
||||
|
||||
// Handle "Move down" buttons
|
||||
document.querySelectorAll('.btn-move-down').forEach((button, index) => {
|
||||
button.addEventListener('click', () => moveTimerDown(index));
|
||||
});
|
||||
|
||||
// Handle "Delete" buttons
|
||||
document.querySelectorAll('.btn-delete').forEach((button, index) => {
|
||||
button.addEventListener('click', () => deleteTimer(index));
|
||||
});
|
||||
}
|
||||
|
||||
function attachInputHandlers() {
|
||||
document.querySelectorAll('input[type="text"], input[type="number"], select').forEach((input) => {
|
||||
input.addEventListener('change', (event) => {
|
||||
const [type, index] = event.target.id.split('-');
|
||||
const value = event.target.value;
|
||||
|
||||
if (type === 'name') {
|
||||
userTimers[index].name = value;
|
||||
|
||||
// Update only the At End dropdowns
|
||||
updateAtEndDropdowns();
|
||||
} else if (type === 'hours' || type === 'minutes' || type === 'seconds') {
|
||||
let h = parseInt(document.getElementById(`hours-${index}`).value) || 0;
|
||||
let m = parseInt(document.getElementById(`minutes-${index}`).value) || 0;
|
||||
let s = parseInt(document.getElementById(`seconds-${index}`).value) || 0;
|
||||
userTimers[index].origin = h * 3600 + m * 60 + s;
|
||||
} else if (type === 'atend') {
|
||||
userTimers[index].chain_id = value == 'null' ? null : parseInt(value);
|
||||
} else if (type === 'vibrate') {
|
||||
userTimers[index].vibrate_pattern = value;
|
||||
} else if (type === 'buzz') {
|
||||
userTimers[index].buzz_count = parseInt(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function moveTimerUp(index) {
|
||||
if (index > 0) {
|
||||
// Swap the timers
|
||||
[userTimers[index - 1], userTimers[index]] = [userTimers[index], userTimers[index - 1]];
|
||||
|
||||
// Re-render the table
|
||||
updateTimerBlocks();
|
||||
|
||||
// Move focus to the new position of the "Move up" button
|
||||
const newFocusButton = document.getElementById(`move-up-${index - 1}`);
|
||||
if (newFocusButton) {
|
||||
newFocusButton.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveTimerDown(index) {
|
||||
if (index < userTimers.length - 1) {
|
||||
// Swap the timers
|
||||
[userTimers[index], userTimers[index + 1]] = [userTimers[index + 1], userTimers[index]];
|
||||
|
||||
// Re-render the table
|
||||
updateTimerBlocks();
|
||||
|
||||
// Move focus to the new position of the "Move down" button
|
||||
const newFocusButton = document.getElementById(`move-down-${index + 1}`);
|
||||
if (newFocusButton) {
|
||||
newFocusButton.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deleteTimer(index) {
|
||||
// Warn user if a timer chain references the timer
|
||||
for (timer of userTimers) {
|
||||
if (timer.id != userTimers[index].id &&
|
||||
timer.chain_id == userTimers[index].id) {
|
||||
if (!confirm('This timer is part of a chain. Delete it anyway?')) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (userTimers.length > 1) {
|
||||
userTimers.splice(index, 1);
|
||||
updateTimerBlocks();
|
||||
}
|
||||
if (userTimers.length == 1) {
|
||||
// Disable the last delete button
|
||||
let deleteButton = document.querySelectorAll('.btn-delete')[0];
|
||||
deleteButton.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function addTimer() {
|
||||
let newTimer = {
|
||||
cls: "PrimitiveTimer",
|
||||
version: DATA_VERSION,
|
||||
origin: 0,
|
||||
rate: -0.001,
|
||||
name: "",
|
||||
id: find_nextId(),
|
||||
chain_id: null,
|
||||
start_time: Date.now(),
|
||||
pause_time: Date.now(),
|
||||
vibrate_pattern: ";;;",
|
||||
buzz_count: 4
|
||||
};
|
||||
userTimers.push(newTimer);
|
||||
updateTimerBlocks();
|
||||
|
||||
// Enable delete buttons
|
||||
let deleteButtons = document.querySelectorAll('.btn-delete');
|
||||
deleteButtons.forEach(button => {
|
||||
button.disabled = false;
|
||||
});
|
||||
|
||||
// Move focus to the new timer's Name field
|
||||
document.getElementById(`name-${userTimers.length - 1}`).focus();
|
||||
}
|
||||
|
||||
function saveTimers() {
|
||||
// Save the timers to storage
|
||||
console.log(userTimers);
|
||||
// Util.writeStorageJSON('tevtimer.timers.json', userTimers, () => {
|
||||
// console.log('Timers saved successfully');
|
||||
// });
|
||||
}
|
||||
|
||||
function reloadTimers() {
|
||||
if (confirm("This will reload timer data from the Bangle.js and discard any unsaved changes. Reload?")) {
|
||||
userTimers = loadTestTimers();
|
||||
setTimeout(updateTimerBlocks(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAtEndDropdowns() {
|
||||
let timerNames = new Map();
|
||||
timerNames.set(null, '<Stop>');
|
||||
userTimers.forEach((timer, i) => {
|
||||
let name = timer.name ? timer.name : `<Timer ${i + 1}>`;
|
||||
timerNames.set(timer.id, name);
|
||||
});
|
||||
|
||||
userTimers.forEach((timer, i) => {
|
||||
let atEndDropdown = document.getElementById(`atend-${i}`);
|
||||
if (atEndDropdown) {
|
||||
let atEndSelected = timer.chain_id ? timer.chain_id : 'null';
|
||||
atEndDropdown.innerHTML = Array.from(timerNames.entries())
|
||||
.map(([key, value]) =>
|
||||
`<option value="${key}" ${key == atEndSelected ? 'selected' : ''}>
|
||||
${value}
|
||||
</option>`
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// XXX: For testing only; remove later
|
||||
onInit();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
"screenshots": [ {"url": "screenshot.png" } ],
|
||||
"readme": "README.md",
|
||||
"tags": "timer",
|
||||
"interface": "interface.html",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"dependencies": {"scheduler": "type"},
|
||||
"storage": [
|
||||
|
|
|
|||
Loading…
Reference in New Issue