Files
librePECS/main.js

544 lines
19 KiB
JavaScript
Raw Permalink Normal View History

import { pecBoard, pec } from './pecslib.js';
// gui object classes?
// Why am I doing this?
class auxillery_dialog {
constructor (title) {
var me = this;
this.root = document.createElement("div");
this.root.classList.add("dialog");
this.title = "string" == typeof title ? title : "";
var title_bar = document.createElement("div");
2025-08-12 16:08:02 -06:00
title_bar.classList.add("title_bar");
var titleText = document.createElement("span");
titleText.innerText = this.title;
titleText.classList.add("title_text");
title_bar.appendChild(titleText);
this.root.appendChild(title_bar);
this.body = document.createElement("div");
this.root.appendChild(me.body);
var close = document.createElement("span");
close.innerText = "<";
close.classList.add("close");
title_bar.appendChild(close);
close.addEventListener("click", ()=>{
me.root.classList.remove("focused");
});
}
}
2025-08-12 16:09:08 -06:00
// Functions
function systemMsg (message,level=0) { // abstraction?
// Yes, but it allows us to easilly redirect error
// output to where we want, for now, just the console...
switch (level) {
case 0:
console.log("INFO: "+message);
break;
case 1:
console.error("WARNING: "+message);
break;
case 2:
console.error("ERROR: "+message);
break;
case 3:
console.log("DEBUG: "+message);
break;
default:
console.log(message);
}
// but later we will also have it push an error toast in the ui.
}
function initDatabase (e) {
var db = e.target.result;
var objectStore = db.createObjectStore("pecs", {keyPath: "word"});
objectStore.createIndex("word", "word", {unique: true});
objectStore.createIndex("locate", "locate");
}
function saveApp () {
var idbHandle = window.indexedDB.open("masterLibrary");
idbHandle.onerror = (e) => {
systemMsg("Error loading database",2);
};
idbHandle.onupgradeneeded = initDatabase;
idbHandle.onsuccess = (e) => {
var masterLibrary = idbHandle.result;
var saving = masterLibrary.transaction("pecs","readwrite",{durability:"strict"});
var pecStore = saving.objectStore("pecs");
library.forEach(pecItem=>{
var action = pecStore.get(pecItem.word);
action.onsuccess = (e)=>{
var record = action.result;
if (record == undefined) {
record = {
word: pecItem.word,
image: pecItem.image,
locate: "library"
};
} else {
record.locate = "library";
}
pecStore.put(record);
};
});
for (var boardID = 0; boardID < app.boards.length; boardID++) {
var curBoard = app.boards[boardID];
for (var index = 0; index < curBoard.pecs.length; index++) {
var curPec = curBoard.pecs[index];
if (null == curPec) { continue; } //don't store nulls
var action = pecStore.get(curPec.word);
action.curPec = curPec;
action.boardID = boardID;
action.pecIndex = index;
action.onsuccess = (e)=>{
var record = e.target.result;
if (record == undefined) {
record = {
word: e.target.curPec.word,
image: e.target.curPec.image,
locate: e.target.boardID + "-" + e.target.pecIndex
};
} else {
record.locate = e.target.boardID + "-" + e.target.pecIndex;
}
pecStore.put(record);
};
}
}
saving.oncomplete = (e)=>{
masterLibrary.close();
saveOptions();
loadApp();
};
};
}
function saveOptions () {
localStorage.setItem("librepecSettings", JSON.stringify(app.options));
}
function loadOptions () {
var options = JSON.parse(localStorage.getItem("librepecSettings"));
if (null != options) { app.options = options; }
var darkmode = document.getElementById("darkmode-checkbox");
var kidsmode = document.getElementById("kidsmode-checkbox");
var feedback = document.getElementById("audio-feedback-toggle");
var floodLimit = document.getElementById("flood-limit");
var floodTimeout = document.getElementById("flood-timeout");
darkmode.checked = (undefined == app.options?.darkmode) ? false : (app.options.darkmode ? true : false);
toggleDarkmode(darkmode.checked);
kidsmode.checked = (undefined == app.options?.kidsmode) ? false : (app.options.kidsmode ? true : false);
feedback.checked = (undefined == app.options?.audioFeedback) ? false : (app.options.audioFeedback ? true : false);
floodLimit.value = (undefined == app.options?.feedbackFloodLimit) ? 3 : app.options.feedbackFloodLimit;
floodTimeout.value = (undefined == app.options?.feedbackTimeout) ? 60 : app.options.feedbackTimeout;
}
import { library as library } from './library.js';
function initApp () {
// Create our initial board
app.boards = [];
app.boards.push(new pecBoard());
app.currentBoard = app.boards[0];
repopBoards(0);
repopGrid(app.currentBoard);
// Populate Library
app.library = library;
repopLibrary();
// Populate Options
app.options = {};
app.options.darkmode = false; // should always be an option
app.options.kidsMode = false; // disables most interactions
app.options.audioFeedback = false; // read word aloude
app.options.feedbackFloodLimit = 3;
app.options.feedbackTimeout = 60; // 60 seconds
// overload defaults with options stored in localstorage
}
function hardResetApp () {
alert("App ahs been hard reset");
initApp();
saveApp();
}
function loadAppPec (item) {
var locate = item.locate;
if ("library" == locate) {
app.library.push(new pec(item.word, item.image));
} else {
locate = locate.split("-").map(n=>parseInt(n));
if (!(app.boards[locate[0]] instanceof pecBoard)) {
app.boards[locate[0]] = new pecBoard();
}
app.boards[locate[0]].pecs[locate[1]] = new pec(item.word, item.image);
}
}
function loadApp () {
var idbHandle = window.indexedDB.open("masterLibrary");
idbHandle.onerror = (e) => {
systemMsg("Error loading database",2);
};
idbHandle.onupgradeneeded = initDatabase;
window.currentBoardID = app.boards.indexOf(app.currentBoard);
currentBoardID = isNaN(currentBoardID) ? 0 : currentBoardID;
idbHandle.onsuccess = (e) => {
var masterLibrary = idbHandle.result;
var loading = masterLibrary.transaction("pecs","readonly",{durability:"strict"});
var pecStore = loading.objectStore("pecs");
var lengthCheck = pecStore.count();
lengthCheck.onsuccess = (e)=>{
if (0 < e.target.result) {
app.library = []; // Clear out library cache
app.boards = []; // Clear out boards cache
app.boards.push(new pecBoard());
app.currentBoard = app.boards[0];
var cursor = pecStore.openCursor();
cursor.onsuccess = (e) => {
var c = e.target.result;
if (c) {
loadAppPec(c.value);
c.continue();
}
};
} else {
// DB is empty, reinit app because
// we have nothing to load
hardResetApp();
}
};
loading.oncomplete = (e) => {
masterLibrary.close();
loadOptions();
app.currentBoard = app.boards[currentBoardID];
repopLibrary();
repopBoards();
repopGrid(app.currentBoard);
};
};
}
function toggleDarkmode () {
var html = document.documentElement;
if (app.options.darkmode) {
if (!(html.classList.contains("darkmode"))) {
html.classList.add("darkmode");
}
} else {
if (html.classList.contains("darkmode")) {
html.classList.remove("darkmode");
}
}
}
2025-08-12 16:09:08 -06:00
function inRange(target,min,max) {
if (isNaN(target) || isNaN(min) || isNaN(max)) { return false; }
return (min <= target && target <= max);
}
function invokeLibrary(e) {
if (app.options.kidsmode) { return; }
2025-08-12 16:09:08 -06:00
// Breaking this out of event so its not an
// annon function, so we can swap it in and out
var targetID = ((e.currentTarget.getAttribute("id")).split("pec"))[1];
if (isNaN(targetID)) { return systemMsg("invokeLibrary targetID not a number",2); }
var targetPec = app.gui["pec"+targetID];
if (undefined == targetPec) { return systemMsg("invokeLibrary targetPec undefined: "+targetID,2); }
if (!inRange(targetID,0,15)) { return systemMsg("invokeLibrary targetID out of range: "+targetPecIndex,2); }
app.targetPec = targetPec;
app.targetPecIndex = targetID;
app.gui.library.root.classList.add("focused");
}
function clearPec (index=null) {
if (!inRange(index,0,15)) { return systemMsg("clearPec was given an out of range index",2); }
var targetPec = app.gui["pec"+index];
if (undefined == targetPec) { return systemMsg("clearPec targetPec undefined",2); }
targetPec.classList.add("empty");
targetPec.innerHTML = "<p>+</p>";
targetPec.addEventListener("click", invokeLibrary);
}
function setPec(pecTemplate=null, targetIndex=null) {
if (null == pecTemplate) { return systemMsg("setPec was not provided data",2); }
if (!(pecTemplate instanceof pec)) { return systemMsg("setPec was not provided a valid template",2); }
if (isNaN(targetIndex)) { return systemMsg("setPec was not provided a valid targetIndex",2); }
var targetPec = app.gui["pec"+targetIndex];
targetPec.removeEventListener("click", invokeLibrary);
targetPec.innerHTML = "";
targetPec.classList.remove("empty");
2025-09-16 13:32:54 -06:00
var freshPec = new pec(pecTemplate.word, pecTemplate.image);
targetPec.appendChild(freshPec.DOM);
2025-08-12 16:09:08 -06:00
}
2025-08-18 15:18:56 -06:00
function repopBoards (current=0) {
//app.boards;
//app.currentBoards;
//app.menu;
// Normally we only want <, but we want an extra
// iteration here for the "add new board" element
for (var i = 0; i <= app.boards.length; i++) {
var boardTab = document.getElementById("board"+i);
if (null == boardTab) {
boardTab = document.createElement("span");
boardTab.setAttribute("id", "board"+i);
boardTab.classList.add("board_tab");
app.gui.menu.appendChild(boardTab);
}
// Clear event listeners
var oldTab = boardTab;
boardTab = oldTab.cloneNode(true);
oldTab.parentNode.replaceChild(boardTab, oldTab);
// not sure how memory safe the above lines are
var name = "Board "+i;
if (i == app.boards.length) {
name = "+";
boardTab.addEventListener("click",(e)=>{
if (app.options.kidsmode) { return; }
2025-08-18 15:18:56 -06:00
var current = app.boards.findIndex(i=>i==app.currentBoard);
app.boards.push(new pecBoard());
repopBoards(current);
repopGrid(app.currentBoard);
});
} else {
if (current != i) {
boardTab.classList.remove("active_board");
boardTab.addEventListener("click", (e)=>{
var sourceID = ((e.currentTarget.getAttribute("id")).split("board"))[1];
app.currentBoard = app.boards[sourceID];
repopBoards(sourceID);
repopGrid(app.currentBoard);
});
} else {
boardTab.classList.add("active_board");
}
if (undefined == app.boards[i]) { app.boards[i] = new pecBoard(); }
2025-08-18 15:18:56 -06:00
if (undefined != app.boards[i].name) {
name = app.boards[i].name;
}
}
boardTab.innerText = name;
}
}
2025-08-12 16:09:08 -06:00
function repopGrid (board=null) {
if (null == board) { return systemMsg("repopGrid was not provided data",2); }
if (!(board instanceof pecBoard)) { return systemMsg("repopGrid was not provided data of type pecBoard",2); }
board.pecs.forEach((p,i)=>{
if (null == p) { return clearPec(i); }
if (!(p instanceof pec)) {
systemMsg("Invalid Pec found, scrubbing and returning to null",1);
board.pecs[i] = null;
return clearPec(i);
}
setPec(p,i);
});
}
function useThisPec (e) {
var sourceID = ((e.currentTarget.getAttribute("id")).split("lPec"))[1];
app.gui.library.root.classList.remove("focused");
2025-09-16 13:32:54 -06:00
app.currentBoard.pecs[app.targetPecIndex] = app.library.splice(sourceID,1)[0];
saveApp();
2025-09-16 13:32:54 -06:00
repopLibrary();
2025-08-12 16:09:08 -06:00
repopGrid(app.currentBoard);
}
2025-09-16 13:32:54 -06:00
function createNewPec () {
var name = app.gui.newpec_name.value;
var image = app.gui.newpec_image.files[0];
app.library.push(new pec(name, URL.createObjectURL(image)));
saveApp();
2025-09-16 13:32:54 -06:00
repopLibrary();
app.gui.newpec.root.classList.remove("focused");
}
function updatePreview () {
var name = app.gui.newpec_name.value;
var image = app.gui.newpec_image.files[0];
app.gui.newpec_preview.innerHTML = "";
app.gui.newpec_preview.removeEventListener("click", createNewPec);
app.gui.newpec_status.innerText = "Create your new pec by naming it and providing an image.";
var correct = 0;
if (["image/png","image/jpeg","image/svg"].includes(image?.type)) {
var imageBox = document.createElement("img");
imageBox.src = URL.createObjectURL(image);
imageBox.classList.add("pecImage");
app.gui.newpec_preview.appendChild(imageBox);
correct++;
}
if ("" != name) {
var nameBox = document.createElement("div");
nameBox.innerText = name;
nameBox.classList.add("pecName");
app.gui.newpec_preview.appendChild(nameBox);
correct++;
}
if (1 < correct) {
app.gui.newpec_status.innerText = "Click/Tap the preview to save.";
app.gui.newpec_preview.addEventListener("click", createNewPec);
}
}
function addNewPec (e) {
// clear out any previous values in form.
app.gui.newpec_form.reset();
updatePreview();
// make form visible
app.gui.newpec.root.classList.add("focused");
}
function repopLibrary () {
// clear library
app.gui.library.body.innerHTML = "";
// recreate items from app.library
app.library.forEach((p,i)=>{
var newPec = document.createElement("span");
newPec.classList.add("libSlot");
newPec.innerText = p.word;
newPec.id = "lPec"+i;
newPec.addEventListener("click", useThisPec);
app.gui.library.body.appendChild(newPec);
});
// add new pec button at end of list
var newPec = document.createElement("span");
newPec.classList.add("libSlot");
newPec.innerText = " + ";
newPec.id = "lPec"+(app.library.length + 1);
newPec.addEventListener("click", addNewPec);
app.gui.library.body.appendChild(newPec);
}
// Initialize gui
window.app = {};
app.gui = {};
app.gui.main = document.createElement("div");
app.gui.main.id = "root";
document.body.appendChild(app.gui.main);
app.gui.menu = document.createElement("nav");
app.gui.menu.id = "navigation";
app.gui.main.appendChild(app.gui.menu);
2025-08-18 15:21:33 -06:00
app.gui.optionsButton = document.createElement("div");
app.gui.optionsButton.classList.add("options_button");
app.gui.optionsButton.innerHTML = "&#x2630;";
app.gui.menu.appendChild(app.gui.optionsButton);
app.gui.view = document.createElement("div");
app.gui.view.id = "view";
app.gui.main.appendChild(app.gui.view);
// Library popup
app.gui.library = new auxillery_dialog("Library");
app.gui.library.body.classList.add("library_body");
2025-08-12 16:08:02 -06:00
app.gui.main.append(app.gui.library.root);
// Options popup
app.gui.options = new auxillery_dialog("Options");
app.gui.options.body.classList.add("options_body");
2025-08-18 15:21:33 -06:00
app.gui.main.append(app.gui.options.root);
app.gui.optionsButton.addEventListener("click", ()=>{
if (app.options.kidsmode) { return; }
loadOptions();
app.gui.options.root.classList.add("focused");
});
app.gui.optionsButton.addEventListener("mousedown", ()=>{
if (!app.options.kidsmode) { return; }
app.gui.optionsButton.classList.add("holding");
app.optionHold = setTimeout(()=>{
app.gui.optionsButton.classList.remove("holding");
loadOptions();
app.gui.options.root.classList.add("focused");
},3000);
});
app.gui.optionsButton.addEventListener("mouseup", ()=>{
if (!app.options.kidsmode) { return; }
app.gui.optionsButton.classList.remove("holding");
clearTimeout(app.optionHold);
});
2025-09-16 13:32:54 -06:00
// New Pec popup
app.gui.newpec = new auxillery_dialog("Create New Pec");
app.gui.newpec.body.classList.add("newpec_body");
app.gui.main.append(app.gui.newpec.root);
// pre-populate grid with emptys
for (var x = 0; x < 16; x++) {
var name = "pec"+x;
2025-08-12 16:08:02 -06:00
var thisPec = document.createElement("span");
app.gui[name] = thisPec;
thisPec.classList.add("pecSlot");
thisPec.id = name;
clearPec(x);
app.gui.view.appendChild(thisPec);
}
2025-08-18 15:21:33 -06:00
// build Options UI
app.gui.options_darkmode = document.createElement("div");
app.gui.options.body.appendChild(app.gui.options_darkmode);
app.gui.options_darkmode.innerHTML = '<label for="darkmode-checkbox">Darkmode</label> <input id="darkmode-checkbox" type="checkbox" class="dm-indicator" />';
app.gui.options_darkmode.addEventListener("change", (e)=>{
app.options.darkmode = e.target.checked;
toggleDarkmode();
saveOptions();
});
app.gui.options_kidsmode = document.createElement("div");
app.gui.options.body.appendChild(app.gui.options_kidsmode);
app.gui.options_kidsmode.innerHTML = '<label for="kidsmode-checkbox">Kids Mode (Disables adding pecs, boards, and accessing options (hold for 3 seconds to access)</label> <input id="kidsmode-checkbox" type="checkbox" class="toggle-switch" />';
app.gui.options_kidsmode.addEventListener("change", (e) => {
app.options.kidsmode = e.target.checked;
saveOptions();
});
2025-08-18 15:21:33 -06:00
app.gui.options_audioFeedback = document.createElement("div");
app.gui.options.body.appendChild(app.gui.options_audioFeedback);
app.gui.options_audioFeedback.innerHTML = '<label for="audio-feedback-toggle">Read Allowed</label> <input id="audio-feedback-toggle" type="checkbox" class="toggle-switch" />';
app.gui.options_audioFeedback.addEventListener("change", (e) => {
app.options.audioFeedback = e.target.checked;
saveOptions();
});
2025-08-18 15:21:33 -06:00
app.gui.options_feedbackFloodLimit = document.createElement("div");
app.gui.options.body.appendChild(app.gui.options_feedbackFloodLimit);
app.gui.options_feedbackFloodLimit.innerHTML = '<label for="flood-limit">Audio Flood Limit</label> <input id="flood-limit" type="number" min=1 step=2 max=10 />';
app.gui.options_feedbackFloodLimit.addEventListener("change", (e) => {
app.options.feedbackFloodLimit = e.target.value;
saveOptions();
});
2025-08-18 15:21:33 -06:00
app.gui.options_feedbackTimeout = document.createElement("div");
app.gui.options.body.appendChild(app.gui.options_feedbackTimeout);
app.gui.options_feedbackTimeout.innerHTML = '<label for="flood-timeout">Audio Flood Timeout</label> <input id="flood-timeout" type="number" min=0 step=10 />';
app.gui.options_feedbackTimeout.addEventListener("change", (e) => {
app.options.feedbackTimeout = e.target.value;
saveOptions();
});
app.gui.options_reset = document.createElement("div");
app.gui.options.body.appendChild(app.gui.options_reset);
app.gui.options_reset.innerHTML = '<span id="reset-hard-reset" class="button">Completely Reset App (hold down for 3 seconds)</span><span id="reset-soft-reset" class="button">Clear All Boards</span>';
app.gui.options_reset_hard = document.getElementById("reset-hard-reset");
app.gui.options_reset_soft = document.getElementById("reset-soft-reset");
// Add functionality for resets here
app.gui.options_about = document.createElement("div");
app.gui.options.body.appendChild(app.gui.options_about);
2025-09-16 13:32:54 -06:00
app.gui.options_about.innerHTML = 'App licenced <a href="LICENSE.txt" target="_blank"><img src="images/ui/gplv3-or-later.svg" height="30px" /></a><label>-</label> Cards from <a href="https://assistivecards.com" target="_blank"><img src="images/ui/ac.svg" height="30px" alt="Assistive Cards" /></a><label>-</label> Project Source <a href="https://labs.murkfall.net/bluesaxman/librePECS">labs.murkfall.net</a>';
// Build New Pec UI
2025-09-16 13:32:54 -06:00
app.gui.newpec.body.innerHTML = '<form id="new-pec-form"><label for="new-pec-name">Name</label><input id="new-pec-name" type="text" value="" /><br /><label for="new-pec-image">Picture</label><input id="new-pec-image" type="file" accept="image/png, image/jpeg, image/svg" value="" /><br /><div id="new-pec-preview"></div><br /><div id="new-pec-status">Create your new pec by naming it and providing an image.</div></form>';
app.gui.newpec_form = document.getElementById("new-pec-form");
app.gui.newpec_name = document.getElementById("new-pec-name");
app.gui.newpec_image = document.getElementById("new-pec-image");
app.gui.newpec_preview = document.getElementById("new-pec-preview");
app.gui.newpec_status = document.getElementById("new-pec-status");
app.gui.newpec_preview.classList.add("pecSlot");
app.gui.newpec_name.addEventListener("change", updatePreview);
app.gui.newpec_image.addEventListener("change", updatePreview);
// initialize and load app
initApp();
loadApp();