diff --git a/src/web/App.mjs b/src/web/App.mjs
index 52a96769..20c25ad2 100755
--- a/src/web/App.mjs
+++ b/src/web/App.mjs
@@ -120,9 +120,10 @@ class App {
*
* @param {boolean} [step] - Set to true if we should only execute one operation instead of the
* whole recipe.
+ * @param input - The inputs to bake
*/
- bake(step=false) {
- if (this.baking) return;
+ bake(step=false, input) {
+ // if (this.baking) return;
// Reset attemptHighlight flag
this.options.attemptHighlight = true;
@@ -131,7 +132,7 @@ class App {
this.manager.recipe.updateBreakpointIndicator(false);
this.manager.worker.bake(
- this.getAllInput(), // The user's input
+ input, // The user's input
this.getRecipeConfig(), // The configuration of the recipe
this.options, // Options set by the user
this.progress, // The current position in the recipe
@@ -151,7 +152,10 @@ class App {
if (this.autoBake_ && !this.baking) {
log.debug("Auto-baking");
- this.bake();
+ this.manager.input.inputWorker.postMessage({
+ action: "autobake",
+ data: this.manager.input.getActiveTab()
+ });
} else {
this.manager.controls.showStaleIndicator();
}
@@ -177,23 +181,13 @@ class App {
this.manager.worker.silentBake(recipeConfig);
}
-
- /**
- * Gets the user's input data.
- *
- * @returns {string}
- */
- getInput() {
- return this.manager.input.getActive();
- }
-
/**
* Gets the user's input data for all tabs.
*
* @returns {Array}
*/
getAllInput() {
- return this.manager.input.getAll();
+ this.manager.input.getAll();
}
/**
@@ -686,8 +680,8 @@ class App {
// Update the current history state (not creating a new one)
if (this.options.updateUrl) {
- this.lastStateUrl = this.manager.controls.generateStateUrl(true, true, recipeConfig);
- window.history.replaceState({}, title, this.lastStateUrl);
+ // this.lastStateUrl = this.manager.controls.generateStateUrl(true, true, recipeConfig);
+ // window.history.replaceState({}, title, this.lastStateUrl);
}
}
diff --git a/src/web/InputWaiter.mjs b/src/web/InputWaiter.mjs
index 2377a969..f66cf9be 100644
--- a/src/web/InputWaiter.mjs
+++ b/src/web/InputWaiter.mjs
@@ -6,6 +6,7 @@
*/
import LoaderWorker from "worker-loader?inline&fallback=false!./LoaderWorker";
+import InputWorker from "worker-loader?inline&fallback=false!./InputWorker";
import Utils from "../core/Utils";
import { toBase64 } from "../core/lib/Base64";
import { isImage } from "../core/lib/FileType";
@@ -44,11 +45,11 @@ class InputWaiter {
145, //Scroll
];
+ this.inputWorker = null;
this.loaderWorkers = [];
+ this.workerId = 0;
this.maxWorkers = navigator.hardwareConcurrency || 4;
- this.inputs = [];
- this.pendingFiles = [];
- this.maxTabs = 4; // Calculate this
+ this.maxTabs = 4;
}
/**
@@ -56,42 +57,83 @@ class InputWaiter {
*/
calcMaxTabs() {
const numTabs = Math.floor((document.getElementById("IO").offsetWidth - 75) / 120);
- this.maxTabs = numTabs;
+ this.maxTabs = (numTabs > 1) ? numTabs : 2;
+ if (this.inputWorker) {
+ this.inputWorker.postMessage({
+ action: "updateMaxTabs",
+ data: this.maxTabs
+ });
+ }
}
/**
- * Terminates any existing loader workers and sets up a new worker
+ * Terminates any existing workers and sets up a new InputWorker and LoaderWorker
*/
- setupLoaderWorker() {
- for (let i = 0; i < this.loaderWorkers.length; i++) {
- const worker = this.loaderWorkers.pop();
- worker.terminate();
+ setupInputWorker() {
+ if (this.inputWorker !== null) this.inputWorker.terminate();
+
+ for (let i = this.loaderWorkers.length - 1; i >= 0; i--) {
+ this.removeLoaderWorker(this.loaderWorkers[i]);
}
- this.addLoaderWorker();
+ log.debug("Adding new InputWorker");
+ this.inputWorker = new InputWorker();
+ this.inputWorker.postMessage({
+ action: "updateMaxWorkers",
+ data: this.maxWorkers
+ });
+ this.inputWorker.postMessage({
+ action: "updateMaxTabs",
+ data: this.maxTabs
+ });
+ this.inputWorker.addEventListener("message", this.handleInputWorkerMessage.bind(this));
+
+ if (this.loaderWorkers.length === 0) {
+ this.activateLoaderWorker();
+ }
+
+ }
+
+ /**
+ * Activates a loaderWorker and sends it to the InputWorker
+ */
+ activateLoaderWorker() {
+ const workerIdx = this.addLoaderWorker(true);
+ if (workerIdx === -1) return;
+
+ const workerObj = this.loaderWorkers[workerIdx];
+ this.inputWorker.postMessage({
+ action: "loaderWorkerReady",
+ data: {
+ id: workerObj.id,
+ port: workerObj.port
+ }
+ }, [workerObj.port]);
}
/**
* Adds a new loaderWorker
*
+ * @param {boolean} [active=false]
* @returns {number} The index of the created worker
*/
addLoaderWorker() {
- for (let i = 0; i < this.loaderWorkers.length; i++) {
- if (!this.loaderWorkers[i].active) {
- return i;
- }
- }
if (this.loaderWorkers.length === this.maxWorkers) {
return -1;
}
log.debug("Adding new LoaderWorker.");
const newWorker = new LoaderWorker();
- newWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
+ const messageChannel = new MessageChannel();
+ const workerId = this.workerId++;
+ // newWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
+ newWorker.postMessage({
+ port: messageChannel.port1,
+ id: workerId
+ }, [messageChannel.port1]);
const newWorkerObj = {
worker: newWorker,
- active: false,
- inputNum: 0
+ id: workerId,
+ port: messageChannel.port2
};
this.loaderWorkers.push(newWorkerObj);
return this.loaderWorkers.indexOf(newWorkerObj);
@@ -107,6 +149,7 @@ class InputWaiter {
if (idx === -1) {
return;
}
+ log.debug(`Terminating worker ${this.loaderWorkers[idx].id}`);
this.loaderWorkers[idx].worker.terminate();
this.loaderWorkers.splice(idx, 1);
if (this.loaderWorkers.length === 0) {
@@ -116,70 +159,135 @@ class InputWaiter {
}
/**
- * Finds and returns the object for the loaderWorker of a given inputNum
+ * Finds and returns the object for the loaderWorker of a given id
*
- * @param {number} inputNum
+ * @param {number} id
*/
- getLoaderWorker(inputNum) {
+ getLoaderWorker(id) {
+ const idx = this.getLoaderWorkerIndex(id);
+ if (idx === -1) return;
+ return this.loaderWorkers[idx];
+ }
+
+ /**
+ * Gets the index for the loaderWorker of a given id
+ *
+ * @param {number} id
+ */
+ getLoaderWorkerIndex(id) {
for (let i = 0; i < this.loaderWorkers.length; i++) {
- if (this.loaderWorkers[i].inputNum === inputNum) {
- return this.loaderWorkers[i];
+ if (this.loaderWorkers[i].id === id) {
+ return i;
}
}
+ return -1;
+ }
+
+ // removeInput should talk to the worker
+
+ /**
+ * Handler for messages sent back by the inputWorker
+ *
+ * @param {MessageEvent} e
+ */
+ handleInputWorkerMessage(e) {
+ const r = e.data;
+
+ if (!r.hasOwnProperty("action")) {
+ log.error("No action");
+ return;
+ }
+
+ log.debug(`Receiving ${r.action} from InputWorker.`);
+
+ switch (r.action) {
+ case "activateLoaderWorker":
+ this.activateLoaderWorker();
+ break;
+ case "loadInput":
+ this.loaderWorkers[r.data.workerIdx].worker.postMessage({
+ file: r.data.file,
+ inputNum: r.data.inputNum
+ });
+ this.loaderWorkers[r.data.workerIdx].inputNum = r.data.inputNum;
+ this.loaderWorkers[r.data.workerIdx].active = true;
+ break;
+ case "terminateLoaderWorker":
+ this.removeLoaderWorker(this.getLoaderWorker(r.data));
+ break;
+ case "allInputs":
+ this.app.bake(false, r.data);
+ break;
+ case "refreshTabs":
+ this.refreshTabs(r.data.nums, r.data.activeTab);
+ break;
+ case "changeTab":
+ this.changeTab(r.data, this.app.options.syncTabs);
+ break;
+ case "updateTabHeader":
+ this.updateTabHeader(r.data);
+ break;
+ case "updateFileProgress":
+ this.updateFileProgress(r.data.inputNum, r.data.progress);
+ break;
+ case "loadingInfo":
+ this.showLoadingInfo(r.data);
+ break;
+ case "setInput":
+ this.set(r.data, true);
+ break;
+ case "inputAdded":
+ this.inputAdded(r.data.changeTab, r.data.inputNum);
+ break;
+ case "addInputs":
+ this.addInputs(r.data);
+ break;
+ default:
+ log.error(`Unknown action ${r.action}.`);
+ }
+ // Handle the responses and use them to control the UI / other workers / stuff
+ }
+
+ // get / set input
+ /**
+ * Gets the input for the active tab
+ */
+ getActive() {
+ const textArea = document.getElementById("input-text");
+ const value = (textArea.value !== undefined) ? textArea.value : "";
+ const inputNum = this.getActiveTab();
+
+ if (this.fileBuffer) {
+ return this.fileBuffer;
+ } else {
+ this.updateInputValue(inputNum, value);
+ return value;
+ }
}
/**
- * Loads a file into the input
- *
- * @param {File} file
- * @param {number} inputNum
+ * Gets the input for all tabs
*/
- loadFile(file, inputNum) {
- if (file && inputNum) {
- this.closeFile(this.getLoaderWorker(inputNum));
- let loaded = false;
-
- const workerId = this.addLoaderWorker();
- if (workerId !== -1) {
- this.loaderWorkers[workerId].active = true;
- this.loaderWorkers[workerId].inputNum = inputNum;
- this.loaderWorkers[workerId].worker.postMessage({
- file: file,
- inputNum: inputNum
- });
- loaded = true;
- } else {
- this.pendingFiles.push({
- file: file,
- inputNum: inputNum
- });
- }
- if (this.getInput(inputNum) !== null) {
- this.removeInput(inputNum);
- }
- this.inputs.push({
- inputNum: inputNum,
- data: {
- fileBuffer: new ArrayBuffer(),
- name: file.name,
- size: file.size.toLocaleString(),
- type: file.type || "unknown"
- },
- status: (loaded) ? "loading" : "pending",
- progress: 0
- });
- }
+ getAll() {
+ this.inputWorker.postMessage({
+ action: "getAll"
+ });
}
/**
- * Closes a file and removes it from inputs
+ * Sets the input in the input area
*
- * @param {number} inputNum
+ * @param inputData
+ * @param {boolean} [silent=false]
*/
- closeFile(inputNum) {
- this.removeLoaderWorker(this.getLoaderWorker(inputNum));
+ set(inputData, silent=false) {
+ const inputText = document.getElementById("input-text");
+ const activeTab = this.getActiveTab();
+ if (inputData.inputNum !== activeTab) return;
- if (inputNum === this.getActiveTab()) {
+ if (typeof inputData.input === "string") {
+ inputText.value = inputData.input;
+ // close file
const fileOverlay = document.getElementById("input-file"),
fileName = document.getElementById("input-file-name"),
fileSize = document.getElementById("input-file-size"),
@@ -192,225 +300,131 @@ class InputWaiter {
fileType.textContent = "";
fileLoaded.textContent = "";
- const inputText = document.getElementById("input-text"),
- fileThumb = document.getElementById("input-file-thumbnail");
inputText.style.overflow = "auto";
inputText.classList.remove("blur");
- fileThumb.src = require("./static/images/file-128x128.png");
+
+ const lines = inputData.input.length < (this.app.options.ioDisplayThreshold * 1024) ?
+ inputData.input.count("\n") + 1 : null;
+ this.setInputInfo(inputData.input.length, lines);
+ } else {
+ this.setFile(inputData);
+ // show file info here
}
+
+ if (!silent) window.dispatchEvent(this.manager.statechange);
+
}
/**
- * Remove an input from the input list
- * @param {number} inputNum
+ * Shows file details
+ *
+ * @param inputData
*/
- removeInput(inputNum) {
- for (let i = 0; i < this.inputs.length; i++) {
- if (this.inputs[i].inputNum === inputNum) {
- this.inputs.splice(i, 1);
- }
- }
+ setFile(inputData) {
+ const activeTab = this.getActiveTab();
+ if (inputData.inputNum !== activeTab) return;
+
+ const fileOverlay = document.getElementById("input-file"),
+ fileName = document.getElementById("input-file-name"),
+ fileSize = document.getElementById("input-file-size"),
+ fileType = document.getElementById("input-file-type"),
+ fileLoaded = document.getElementById("input-file-loaded");
+
+ fileOverlay.style.display = "block";
+ fileName.textContent = inputData.name;
+ fileSize.textContent = inputData.size + " bytes";
+ fileType.textContent = inputData.type;
+ fileLoaded.textContent = inputData.progress + "%";
+
+ this.displayFilePreview(inputData);
}
/**
- * Updates the progress value of an input
+ * Shows a chunk of the file in the input behind the file overlay
+ *
+ * @param {Object} inputData
+ * @param {number} inputData.inputNum
+ * @param {ArrayBuffer} inputData.input
+ */
+ displayFilePreview(inputData) {
+ const activeTab = this.getActiveTab(),
+ input = inputData.input,
+ inputText = document.getElementById("input-text");
+ if (inputData.inputNum !== activeTab) return;
+ inputText.style.overflow = "hidden";
+ inputText.classList.add("blur");
+ inputText.value = Utils.printable(Utils.arrayBufferToStr(input));
+ }
+
+ /**
+ * Updates the displayed input progress for a file
*
* @param {number} inputNum
* @param {number} progress
*/
- updateInputProgress(inputNum, progress) {
- for (let i = 0; i < this.inputs.length; i++) {
- if (this.inputs[i].inputNum === inputNum) {
- // Don't let progress go over 100
- this.inputs[i].progress = (progress <= 100) ? progress : 100;
- }
+ updateFileProgress(inputNum, progress) {
+ const activeTab = this.getActiveTab();
+ if (inputNum !== activeTab) return;
+
+ const fileLoaded = document.getElementById("input-file-loaded");
+ fileLoaded.textContent = progress + "%";
+
+ if (progress < 100) {
+ // setTimeout(function() {
+ // this.inputWorker.postMessage({
+ // action: "getInputProgress",
+ // data: activeTab
+ // });
+ // }.bind(this), 100);
+ } else {
+ this.inputWorker.postMessage({
+ action: "setInput",
+ data: inputNum
+ });
}
}
+
/**
- * Updates the stored value of an input
+ * Updates the input value for the specified inputNum
*
* @param {number} inputNum
- * @param {ArrayBuffer | String} value
+ * @param {string | ArrayBuffer} value
*/
updateInputValue(inputNum, value) {
-
- for (let i = 0; i < this.inputs.length; i++) {
- if (this.inputs[i].inputNum === inputNum) {
- if (typeof value === "string") {
- this.inputs[i].data = value;
- } else {
- this.inputs[i].data.fileBuffer = value;
-
- if (inputNum === this.getActiveTab()) {
- this.displayFilePreview();
- }
- }
- this.inputs[i].progress = 100;
- this.inputs[i].status = "loaded";
- return;
- }
- }
- // If we get to here, an input for inputNum could not be found
-
- if (typeof value === "string") {
- this.inputs.push({
+ this.inputWorker.postMessage({
+ action: "updateInputValue",
+ data: {
inputNum: inputNum,
- data: value,
- status: "loaded",
- progress: 100
- });
- }
+ value: value
+ }
+ });
}
/**
- * Handler for messages sent back by LoaderWorkers
+ * Displays information about the input.
*
- * @param {MessageEvent} e
+ * @param {number} length - The length of the current input string
+ * @param {number} lines - The number of the lines in the current input string
*/
- handleLoaderMessage(e) {
- const r = e.data;
- let inputNum = 0;
+ setInputInfo(length, lines) {
+ let width = length.toString().length.toLocaleString();
+ width = width < 2 ? 2 : width;
- if (r.hasOwnProperty("inputNum")) {
- inputNum = r.inputNum;
+ const lengthStr = length.toString().padStart(width, " ").replace(/ /g, " ");
+ let msg = "length: " + lengthStr;
+
+ if (typeof lines === "number") {
+ const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
+ msg += "
lines: " + linesStr;
}
- if (r.hasOwnProperty("progress")) {
- this.updateInputProgress(inputNum, r.progress);
- this.setFile(inputNum);
- // UI here
- }
-
- if (r.hasOwnProperty("error")) {
- this.app.alert(r.error, 10000);
- }
-
- if (r.hasOwnProperty("fileBuffer")) {
- log.debug(`Input file ${inputNum} loaded.`);
- this.updateInputValue(inputNum, r.fileBuffer);
-
- this.setLoadingInfo();
-
- const currentWorker = this.getLoaderWorker(inputNum);
-
- if (this.pendingFiles.length > 0) {
- log.debug("Loading file completed. Loading next file.");
- const nextFile = this.pendingFiles[0];
- this.pendingFiles.splice(0, 1);
- currentWorker.inputNum = nextFile.inputNum;
- currentWorker.worker.postMessage({
- file: nextFile.file,
- inputNum: nextFile.inputNum
- });
-
- } else {
- // LoaderWorker no longer needed
- log.debug("Loading file completed. Closing LoaderWorker.");
- const progress = this.getLoadProgress();
- if (progress.total === progress.loaded) {
- window.dispatchEvent(this.manager.statechange);
- }
- this.removeLoaderWorker(currentWorker);
- }
-
- }
- }
-
- /**
- * Gets the input for the specified input number
- *
- * @param {number} inputNum
- */
- getInput(inputNum) {
- const index = this.getInputIndex(inputNum);
- if (index === -1) {
- return null;
- }
- if (typeof this.inputs[index].data === "string") {
- return this.inputs[index].data;
- } else {
- return this.inputs[index].data.fileBuffer;
- }
- }
-
- /**
- * Gets the index of the input in the inputs list
- *
- * @param {number} inputNum
- */
- getInputIndex(inputNum) {
- for (let i = 0; i < this.inputs.length; i++) {
- if (this.inputs[i].inputNum === inputNum) {
- return i;
- }
- }
- return -1;
- }
-
- /**
- * Gets the input for the active tab
- */
- getActive() {
- const textArea = document.getElementById("input-text");
- const value = (textArea.value !== undefined) ? textArea.value : "";
- const inputNum = this.getActiveTab();
-
- const input = this.getInput(inputNum);
- if (input === null || typeof input === "string") {
- this.updateInputValue(inputNum, value);
- }
-
- return this.getInput(inputNum);
+ document.getElementById("input-info").innerHTML = msg;
}
+ // get progress
- /**
- * Gets the input for all tabs
- */
- getAll() {
- // Need to make sure here that the active input is actually saved in this inputs
- this.getActive();
- const inputs = [];
-
- for (let i = 0; i < this.inputs.length; i++) {
- if (this.inputs[i].status === "loaded") {
- inputs.push({
- inputNum: this.inputs[i].inputNum,
- input: this.getInput(this.inputs[i].inputNum) || ""
- });
- }
- }
- if (inputs.length === 0) {
- inputs.push({
- inputNum: 1,
- input: ""
- });
- }
- return inputs;
- }
-
- /**
- * Get the progress of the loaderWorkers
- */
- getLoadProgress() {
- const totalInputs = this.inputs.length;
- const pendingInputs = this.pendingFiles.length;
- let loadingInputs = 0;
- for (let i = 0; i < this.loaderWorkers.length; i++) {
- if (this.loaderWorkers[i].active) {
- loadingInputs += 0;
- }
- }
- return {
- total: totalInputs,
- pending: pendingInputs,
- loading: loadingInputs,
- loaded: (totalInputs - pendingInputs - loadingInputs)
- };
- }
-
-
+ // inputChange
/**
* Handler for input change events
*
@@ -420,53 +434,28 @@ class InputWaiter {
*/
inputChange(e) {
// Ignore this function if the input is a file
- const input = this.getActive();
- if (typeof input !== "string") return;
+ const fileOverlay = document.getElementById("input-file");
+ if (fileOverlay.style.display === "block") return;
- // Remove highlighting from input and output panes as the offsets might be different now
- // this.manager.highlighter.removeHighlights();
+ const textArea = document.getElementById("input-text");
+ const value = (textArea.value !== undefined) ? textArea.value : "";
+ const activeTab = this.getActiveTab();
- // Reset recipe progress as any previous processing will be redundant now
this.app.progress = 0;
- // Update the input metadata info
- const lines = input.length < (this.app.options.ioDisplayThreshold * 1024) ?
- input.count("\n") + 1 : null;
-
- this.setInputInfo(input.length, lines);
- this.displayTabInfo(this.getActiveTab());
+ const lines = value.length < (this.app.options.ioDisplayThreshold * 1024) ?
+ (value.count("\n") + 1) : null;
+ this.setInputInfo(value.length, lines);
+ this.updateInputValue(activeTab, value);
+ this.updateTabHeader({inputNum: activeTab, input: value});
if (e && this.badKeys.indexOf(e.keyCode) < 0) {
// Fire the statechange event as the input has been modified
window.dispatchEvent(this.manager.statechange);
}
}
+ // inputPaste
- /**
- * Handler for input paste events.
- * Checks that the size of the input is below the display limit, otherwise treats it as a file/blob.
- *
- * @param {event} e
- */
- inputPaste(e) {
- const pastedData = e.clipboardData.getData("Text");
-
- if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024)) {
- this.inputChange(e);
- } else {
- e.preventDefault();
- e.stopPropagation();
-
- const file = new File([pastedData], "PastedData", {
- type: "text/plain",
- lastModified: Date.now()
- });
-
- this.loadFile(file, this.getActiveTab());
- this.set(file);
- return false;
- }
- }
/**
* Handler for input dragover events.
@@ -496,9 +485,10 @@ class InputWaiter {
e.target.closest("#input-text,#input-file").classList.remove("dropping-file");
}
+ // inputDrop
/**
* Handler for input drop events.
- * Loads the dragged data into the input textarea
+ * Loads the dragged data.
*
* @param {event} e
*/
@@ -515,8 +505,8 @@ class InputWaiter {
e.target.closest("#input-text,#input-file").classList.remove("dropping-file");
if (text) {
- this.closeFile(this.getActiveTab());
- this.set(text);
+ // close file
+ // set text output
return;
}
@@ -541,198 +531,123 @@ class InputWaiter {
}
/**
- * Load files from the UI into the input, creating tabs if needed
+ * Load files from the UI into the inputWorker, creating tabs if needed
*
* @param files
*/
loadUIFiles(files) {
- let inputNum;
- if (files.length > 20) {
- this.manager.controls.setAutoBake(false);
- this.app.alert("Auto-Bake is disabled by default when inputting more than 20 files.", 5000);
- }
- for (let i = 0; i < files.length; i++) {
- inputNum = this.getActiveTab();
- if (i > 0) {
- inputNum = this.addTab(false);
- }
- this.loadFile(files[i], inputNum);
+ const numFiles = files.length;
+ log.debug(`Loading ${numFiles} files.`);
+ // Show something in the UI to make it clear we're loading files
- if (inputNum === this.getActiveTab()) {
- this.setFile(inputNum);
- }
- }
- this.changeTab(inputNum, this.app.options.syncTabs);
+ this.inputWorker.postMessage({
+ action: "loadUIFiles",
+ data: files
+ });
+
+ this.hideLoadingMessage();
}
/**
- * Sets the input in the input area
- *
- * @param {string|File} input
- * @param {boolean} [silent=false] - Suppress statechange event
- *
- * @fires Manager#statechange
- *
+ * Displays a message to show the app is loading files
*/
- set(input, silent=false) {
- const inputText = document.getElementById("input-text");
- const inputNum = this.getActiveTab();
- if (input instanceof File) {
- this.setFile(inputNum);
- inputText.value = "";
- this.setInputInfo(input.size, null);
- this.displayTabInfo(inputNum);
- } else {
- inputText.value = input;
- this.updateInputValue(inputNum, input);
- this.closeFile(inputNum);
+ showLoadingMessage() {
+ $("#loading-files-modal").modal("show");
+ }
- if (!silent) window.dispatchEvent(this.manager.statechange);
+ /**
+ * Hides the loading message
+ */
+ hideLoadingMessage() {
+ $("#loading-files-modal").modal("hide");
+ }
- const lines = input.length < (this.app.options.ioDisplayThreshold * 1024) ?
- input.count("\n") + 1 : null;
- this.setInputInfo(input.length, lines);
- this.displayTabInfo(inputNum);
+ /**
+ * Checks the length of the files input. If it's 0, hide loading message
+ */
+ checkInputFiles() {
+ const fileInput = document.getElementById("open-file");
+ const folderInput = document.getElementById("open-folder");
+
+ if (fileInput.value.length === 0 && folderInput.value.length === 0) {
+ this.hideLoadingMessage();
}
}
/**
- * Shows file details
- *
- * @param {number} inputNum
+ * Handler for open input button click.
+ * Opens the open file dialog.
*/
- setFile(inputNum) {
- if (inputNum === this.getActiveTab()) {
- for (let i = 0; i < this.inputs.length; i++) {
- if (this.inputs[i].inputNum === inputNum && typeof this.inputs[i].data !== "string") {
- const fileOverlay = document.getElementById("input-file"),
- fileName = document.getElementById("input-file-name"),
- fileSize = document.getElementById("input-file-size"),
- fileType = document.getElementById("input-file-type"),
- fileLoaded = document.getElementById("input-file-loaded"),
- fileObj = this.inputs[i];
- fileOverlay.style.display = "block";
- fileName.textContent = fileObj.data.name;
- fileSize.textContent = fileObj.data.size + " bytes";
- fileType.textContent = fileObj.data.type;
- fileLoaded.textContent = fileObj.progress + "%";
-
- this.setInputInfo(fileObj.data.size, null);
- this.displayFilePreview();
- }
- }
- }
- this.displayTabInfo(inputNum);
+ inputOpenClick() {
+ this.showLoadingMessage();
+ document.getElementById("open-file").click();
+ document.body.onfocus = this.checkInputFiles.bind(this);
}
/**
- * Displays information about the input.
- *
- * @param {number} length - The length of the current input string
- * @param {number} lines - The number of the lines in the current input string
+ * Handler for open folder button click
+ * Opens the open folder dialog.
*/
- setInputInfo(length, lines) {
- // This should also update the tab?
- let width = length.toString().length;
+ folderOpenClick() {
+ this.showLoadingMessage();
+ document.getElementById("open-folder").click();
+ document.body.onfocus = this.checkInputFiles.bind(this);
+ }
+
+ // set / setFile
+ // get the data for the active tab from inputWorker
+ // display it!
+
+ // setinputInfo
+ /**
+ * Display the loaded files information in the input header
+ * @param loadedData
+ */
+ showLoadingInfo(loadedData) {
+ const pending = loadedData.pending;
+ const loading = loadedData.loading;
+ const loaded = loadedData.loaded;
+ const total = loadedData.total;
+
+ let width = total.toString().length;
width = width < 2 ? 2 : width;
- const lengthStr = length.toString().padStart(width, " ").replace(/ /g, " ");
- let msg = "length: " + lengthStr;
-
- if (typeof lines === "number") {
- const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
- msg += "
lines: " + linesStr;
- }
-
- document.getElementById("input-info").innerHTML = msg;
-
- }
-
- /**
- * Display progress information for file loading in header
- */
- setLoadingInfo() {
- const progress = this.getLoadProgress();
- let width = progress.total.toString().length;
- width = width < 2 ? 2 : width;
-
- const totalStr = progress.total.toString().padStart(width, " ").replace(/ /g, " ");
+ const totalStr = total.toString().padStart(width, " ").replace(/ /g, " ");
let msg = "Total: " + totalStr;
- const loadedStr = progress.loaded.toString().padStart(width, " ").replace(/ /g, " ");
+ const loadedStr = loaded.toString().padStart(width, " ").replace(/ /g, " ");
msg += "
Loaded: " + loadedStr;
- if (progress.pending > 0) {
- const pendingStr = progress.pending.toString().padStart(width, " ").replace(/ /g, " ");
+ if (pending > 0) {
+ const pendingStr = pending.toString().padStart(width, " ").replace(/ /g, " ");
msg += "
Pending: " + pendingStr;
-
+ } else if (loading > 0) {
+ const loadingStr = loading.toString().padStart(width, " ").replace(/ /g, " ");
+ msg += "
Loading: " + loadingStr;
}
document.getElementById("input-files-info").innerHTML = msg;
- }
- /**
- * Display input information in the tab header
- *
- * @param {number} inputNum
- */
- displayTabInfo(inputNum) {
- const tabItem = this.getTabItem(inputNum);
- const index = this.getInputIndex(inputNum);
- if (index === -1) return;
- const input = this.inputs[index];
- if (!tabItem) {
- return;
- }
-
- const tabContent = tabItem.firstElementChild;
- if (typeof input.data !== "string") {
- tabContent.innerText = `${inputNum}: ${input.data.name}`;
- } else {
- if (input.data.length > 0) {
- const inputText = input.data.slice(0, 100).split(/[\r\n]/)[0];
- tabContent.innerText = `${inputNum}: ${inputText}`;
- } else {
- tabContent.innerText = `${inputNum}: New Tab`;
- }
- }
+ this.updateFileProgress(loadedData.activeProgress.inputNum, loadedData.activeProgress.progress);
}
+ // displayTabInfo
+ // simple getInput for each tab
- /**
- * Shows a chunk of the file in the input behind the file overlay.
- */
- displayFilePreview() {
- const inputNum = this.getActiveTab(),
- input = this.getInput(inputNum),
- inputText = document.getElementById("input-text"),
- fileSlice = input.slice(0, 4096),
- fileThumb = document.getElementById("input-file-thumbnail"),
- arrBuffer = new Uint8Array(input),
- type = isImage(arrBuffer);
- if (type && type !== "image/tiff" && this.app.options.imagePreview && input.byteLength < 1024000) {
- // Don't show TIFFs as not much supports them
- fileThumb.src = `data:${type};base64,${toBase64(arrBuffer)}`;
- } else {
- fileThumb.src = require("./static/images/file-128x128.png");
- }
- inputText.style.overflow = "hidden";
- inputText.classList.add("blur");
- inputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
- if (this.getInput(inputNum).byteLength > 4096) {
- inputText.value += "[truncated]...";
- }
- }
+ // displayFilePreview
/**
* Create a tab element for the input tab bar
*
* @param {number} inputNum
+ * @param {boolean} [active=false]
* @returns {Element}
*/
- createTabElement(inputNum) {
+ createTabElement(inputNum, active) {
const newTab = document.createElement("li");
newTab.setAttribute("inputNum", inputNum.toString());
+ if (active) newTab.classList.add("active-input-tab");
+
const newTabContent = document.createElement("div");
newTabContent.classList.add("input-tab-content");
newTabContent.innerText = `${inputNum.toString()}: New Tab`;
@@ -753,345 +668,133 @@ class InputWaiter {
return newTab;
}
+ // addTab
+ // UI bit can be done here
+ // Adding an input should be sent to the inputWorker
+
+ // removeTab
+ // UI here
+ // remove input sent to the inputWorker
+
+ // refreshTabs
/**
- * Adds a new input to inputs.
- * Will create a new tab if there's less than maxtabs visible.
+ * Redraw the tab bar with an updated list of tabs
*
- * @param {boolean} [changeTab=true]
- */
- addTab(changeTab = true) {
- let inputNum;
- if (this.inputs.length === 0) {
- inputNum = 1;
- } else {
- inputNum = this.getLargestInputNum() + 1;
- }
- this.inputs.push({
- inputNum: inputNum,
- data: "",
- status: "loaded",
- progress: 100
- });
-
-
- this.manager.output.addOutput(inputNum, changeTab);
-
- const tabsWrapper = document.getElementById("input-tabs");
- const numTabs = tabsWrapper.children.length;
-
- if (numTabs < this.maxTabs) {
- // Create a tab element
- const newTab = this.createTabElement(inputNum);
-
- tabsWrapper.appendChild(newTab);
-
- if (numTabs > 0) {
- tabsWrapper.parentElement.style.display = "block";
-
- document.getElementById("input-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
- document.getElementById("input-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
- document.getElementById("input-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
- }
-
- }
-
- if (changeTab) {
- this.changeTab(inputNum);
- }
-
- return inputNum;
-
- }
-
- /**
- * Removes a tab and it's corresponding input
- *
- * @param {number} inputNum
- */
- removeTab(inputNum) {
- const inputIdx = this.getInputIndex(inputNum);
- let activeTab = this.getActiveTab();
- if (inputIdx === -1) {
- return;
- }
-
- const tabElement = this.getTabItem(inputNum);
-
- this.removeInput(inputNum);
-
- if (tabElement !== null) {
- if (inputNum === activeTab) {
- activeTab = this.getPreviousInputNum(activeTab);
- if (activeTab === this.getActiveTab()) {
- activeTab = this.getNextInputNum(activeTab);
- }
- }
- this.refreshTabs(activeTab);
- }
-
- this.manager.output.removeTab(inputNum);
- }
-
- /**
- * Redraw the entire tab bar to remove any outdated tabs
+ * @param nums
* @param {number} activeTab
*/
- refreshTabs(activeTab) {
+ refreshTabs(nums, activeTab) {
const tabsList = document.getElementById("input-tabs");
- let newInputs = this.getNearbyNums(activeTab, "right");
- if (newInputs.length < this.maxTabs) {
- newInputs = this.getNearbyNums(activeTab, "left");
- }
for (let i = tabsList.children.length - 1; i >= 0; i--) {
tabsList.children.item(i).remove();
}
- for (let i = 0; i < newInputs.length; i++) {
- tabsList.appendChild(this.createTabElement(newInputs[i]));
- this.displayTabInfo(newInputs[i]);
+ for (let i = 0; i < nums.length; i++) {
+ let active = false;
+ if (nums[i] === activeTab) active = true;
+ tabsList.appendChild(this.createTabElement(nums[i], active));
}
- if (newInputs.length > 1) {
+ if (nums.length > 1) {
tabsList.parentElement.style.display = "block";
document.getElementById("input-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
document.getElementById("input-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
document.getElementById("input-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+
+ document.getElementById("save-all-to-file").style.display = "inline-block";
} else {
tabsList.parentElement.style.display = "none";
document.getElementById("input-wrapper").style.height = "calc(100% - var(--title-height))";
document.getElementById("input-highlighter").style.height = "calc(100% - var(--title-height))";
document.getElementById("input-file").style.height = "calc(100% - var(--title-height))";
- }
- if (newInputs.length === 0) {
- activeTab = this.addTab();
- this.displayTabInfo(activeTab);
+ document.getElementById("save-all-to-file").style.display = "none";
}
this.changeTab(activeTab);
- this.manager.output.refreshTabs(activeTab);
- // MAKE THE OUTPUT REFRESH TOO
}
/**
- * Handler for remove tab button click
- * @param {event} mouseEvent
- */
- removeTabClick(mouseEvent) {
- if (!mouseEvent.target) {
- return;
- }
- const tabNum = mouseEvent.target.parentElement.parentElement.getAttribute("inputNum");
- if (tabNum) {
- this.removeTab(parseInt(tabNum, 10));
- }
- }
-
- /**
- * Generates a list of the nearby inputNums
- * @param inputNum
- * @param direction
- */
- getNearbyNums(inputNum, direction) {
- const nums = [];
- for (let i = 0; i < this.maxTabs; i++) {
- let newNum;
- if (i === 0) {
- newNum = inputNum;
- } else {
- switch (direction) {
- case "left":
- newNum = this.getNextInputNum(nums[i - 1]);
- if (newNum === nums[i - 1]) {
- direction = "right";
- newNum = this.getPreviousInputNum(nums[i - 1]);
- }
- break;
- case "right":
- newNum = this.getPreviousInputNum(nums[i - 1]);
- if (newNum === nums[i - 1]) {
- direction = "left";
- newNum = this.getNextInputNum(nums[i - 1]);
- }
- }
- }
- if (!nums.includes(newNum) && (newNum > 0)) {
- nums.push(newNum);
- }
- }
- nums.sort(function(a, b) {
- return a - b;
- });
- return nums;
- }
-
- /**
- * Changes the active tab
+ * Change the active tab
*
* @param {number} inputNum
* @param {boolean} [changeOutput=false]
*/
- changeTab(inputNum, changeOutput = false) {
- const currentNum = this.getActiveTab();
- if (this.getInputIndex(inputNum) === -1) return;
-
- const tabsWrapper = document.getElementById("input-tabs");
- const tabs = tabsWrapper.children;
-
+ changeTab(inputNum, changeOutput) {
+ const tabsList = document.getElementById("input-tabs");
let found = false;
- for (let i = 0; i < tabs.length; i++) {
- if (tabs.item(i).getAttribute("inputNum") === inputNum.toString()) {
- tabs.item(i).classList.add("active-input-tab");
+ let minNum = Number.MAX_SAFE_INTEGER;
+ for (let i = 0; i < tabsList.children.length; i++) {
+ const tabNum = parseInt(tabsList.children.item(i).getAttribute("inputNum"), 10);
+ if (tabNum === inputNum) {
+ tabsList.children.item(i).classList.add("active-input-tab");
found = true;
} else {
- tabs.item(i).classList.remove("active-input-tab");
+ tabsList.children.item(i).classList.remove("active-input-tab");
+ }
+ if (tabNum < minNum) {
+ minNum = tabNum;
}
}
if (!found) {
- // Shift the tabs here
let direction = "right";
- if (currentNum > inputNum) {
+ if (inputNum < minNum) {
direction = "left";
}
-
- const newInputs = this.getNearbyNums(inputNum, direction);
-
- for (let i = 0; i < newInputs.length; i++) {
- tabs.item(i).setAttribute("inputNum", newInputs[i].toString());
- this.displayTabInfo(newInputs[i]);
- if (newInputs[i] === inputNum) {
- tabs.item(i).classList.add("active-input-tab");
+ this.inputWorker.postMessage({
+ action: "refreshTabs",
+ data: {
+ inputNum: inputNum,
+ direction: direction
}
- }
- }
-
- const input = this.getInput(inputNum);
- if (typeof input === "string") {
- this.set(this.getInput(inputNum), true);
+ });
} else {
- this.setFile(inputNum);
+ this.inputWorker.postMessage({
+ action: "setInput",
+ data: inputNum
+ });
}
if (changeOutput) {
this.manager.output.changeTab(inputNum, false);
}
-
}
/**
- * Handler for changing tabs event
+ * Handler for clicking on a tab
*
* @param {event} mouseEvent
*/
changeTabClick(mouseEvent) {
- if (!mouseEvent.target) {
- return;
- }
+ if (!mouseEvent.target) return;
+
const tabNum = mouseEvent.target.parentElement.getAttribute("inputNum");
- if (tabNum) {
+ if (tabNum >= 0) {
this.changeTab(parseInt(tabNum, 10), this.app.options.syncTabs);
}
}
- /**
- * Handler for changing to the left tab
- */
- changeTabLeft() {
- const currentTab = this.getActiveTab();
- const currentInput = this.getInputIndex(currentTab);
- if (currentInput > 0) {
- this.changeTab(this.getPreviousInputNum(currentTab), this.app.options.syncTabs);
- } else {
- this.changeTab(this.inputs[0].inputNum, this.app.options.syncTabs);
- }
- }
/**
- * Handler for changing to the right tab
+ * Updates the tab header to display the new input content
*/
- changeTabRight() {
- const currentTab = this.getActiveTab();
- this.changeTab(this.getNextInputNum(currentTab), this.app.options.syncTabs);
- }
-
- /**
- * Handler for go to tab button clicked
- */
- goToTab() {
- const tabNum = parseInt(window.prompt("Enter tab number:", this.getActiveTab().toString()), 10);
- if (this.getInputIndex(tabNum) >= 0) {
- this.changeTab(tabNum, this.app.options.syncTabs);
- }
- }
-
- /**
- * Gets the largest inputNum
- *
- * @returns {number}
- */
- getLargestInputNum() {
- let largest = 0;
- for (let i = 0; i < this.inputs.length; i++) {
- if (this.inputs[i].inputNum > largest) {
- largest = this.inputs[i].inputNum;
+ updateTabHeader(headerData) {
+ const tabsList = document.getElementById("input-tabs");
+ const inputNum = headerData.inputNum;
+ const inputData = headerData.input.slice(0, 100);
+ for (let i = 0; i < tabsList.children.length; i++) {
+ if (tabsList.children.item(i).getAttribute("inputNum") === inputNum.toString()) {
+ tabsList.children.item(i).firstElementChild.innerText = `${inputNum}: ${inputData}`;
+ break;
}
}
- return largest;
}
+ // removeTabClick
- /**
- * Gets the smallest inputNum
- *
- * @returns {number}
- */
- getSmallestInputNum() {
- let smallest = this.getLargestInputNum();
- for (let i = 0; i < this.inputs.length; i++) {
- if (this.inputs[i].inputNum < smallest) {
- smallest = this.inputs[i].inputNum;
- }
- }
- return smallest;
- }
-
- /**
- * Gets the previous inputNum
- *
- * @param {number} inputNum - The current input number
- * @returns {number}
- */
- getPreviousInputNum(inputNum) {
- let num = -1;
- for (let i = 0; i < this.inputs.length; i++) {
- if (this.inputs[i].inputNum < inputNum) {
- if (this.inputs[i].inputNum > num) {
- num = this.inputs[i].inputNum;
- }
- }
- }
- return num;
- }
-
- /**
- * Gets the next inputNum
- *
- * @param {number} inputNum - The current input number
- * @returns {number}
- */
- getNextInputNum(inputNum) {
- let num = this.getLargestInputNum();
- for (let i = 0; i < this.inputs.length; i++) {
- if (this.inputs[i].inputNum > inputNum) {
- if (this.inputs[i].inputNum < num) {
- num = this.inputs[i].inputNum;
- }
- }
- }
- return num;
- }
+ // move getNearbyNums / getLargest / getSmallest / getNext / getPrevious to inputWorker
/**
* Gets the number of the current active tab
@@ -1125,34 +828,136 @@ class InputWaiter {
}
/**
- * Handler for clear all IO events.
- * Resets the input, output and info areas
+ * Gets a list of tab numbers for the currently open tabs
*/
- clearAllIoClick() {
- this.manager.worker.cancelBake();
- for (let i = this.inputs.length - 1; i >= 0; i--) {
- this.manager.output.removeOutput(this.inputs[i].inputNum);
- this.removeInput(this.inputs[i].inputNum);
+ getTabList() {
+ const nums = [];
+ const tabs = document.getElementById("input-tabs").children;
+ for (let i = 0; i < tabs.length; i++) {
+ nums.push(parseInt(tabs.item(i).getAttribute("inputNum"), 10));
}
- this.refreshTabs();
+ return nums;
+ }
+
+ // clearAllIO
+ // could just re-run setup to create a new inputWorker
+
+ // clearIO
+ // reset current tab
+
+ // filter stuff should be sent to the inputWorker
+ // returns a filterResult message that is handled and used to update the UI
+
+ /**
+ * Sets the console log level in the worker.
+ *
+ * @param {string} level
+ */
+ setLogLevel(level) {
+ if (!this.inputWorker) return;
+ this.inputWorker.postMessage({
+ action: "setLogLevel",
+ data: log.getLevel()
+ });
}
/**
- * Handler for clear IO click event.
- * Resets the input for the current tab
+ * Sends a message to the inputWorker to add a new input.
*/
- clearIoClick() {
- const inputNum = this.getActiveTab();
- this.removeInput(inputNum);
+ addInput() {
+ if (!this.inputWorker) return;
+ this.inputWorker.postMessage({
+ action: "addInput"
+ });
+ }
- this.inputs.push({
- inputNum: inputNum,
- data: "",
- status: "loaded",
- progress: 0
+ /**
+ * Handler for when the inputWorker adds a new input
+ *
+ * @param {boolean} changeTab
+ * @param {number} inputNum
+ */
+ inputAdded(changeTab, inputNum) {
+ if (changeTab) {
+ this.changeTab(inputNum);
+ }
+ this.manager.output.addOutput(inputNum, changeTab);
+ }
+
+ /**
+ * Handler for when the inputWorker adds multiple inputs
+ *
+ * @param {array} inputNums
+ */
+ addInputs(inputNums) {
+ for (let i = 0; i < inputNums.length; i++) {
+ this.manager.output.addOutput(inputNums[i], false);
+ }
+ this.changeTab(inputNums[inputNums.length - 1], this.app.options.syncTabs);
+ }
+
+ /**
+ * Sends a message to the inputWorker to remove an input.
+ * If the input tab is on the screen, refreshes the tabs
+ *
+ * @param {number} inputNum
+ */
+ removeInput(inputNum) {
+ let refresh = false;
+ if (this.getTabItem(inputNum) !== null) {
+ refresh = true;
+ }
+ this.inputWorker.postMessage({
+ action: "removeInput",
+ data: {
+ inputNum: inputNum,
+ refreshTabs: refresh
+ }
});
- this.set("");
+ this.manager.output.removeOutput(inputNum);
+ }
+
+ /**
+ * Handler for clicking on a remove tab button
+ * @param {event} mouseEvent
+ */
+ removeTabClick(mouseEvent) {
+ if (!mouseEvent.target) {
+ return;
+ }
+ const tabNum = mouseEvent.target.parentElement.parentElement.getAttribute("inputNum");
+ if (tabNum) {
+ this.removeInput(parseInt(tabNum, 10));
+ }
+ }
+
+ /**
+ * Handler for clicking on next tab button
+ */
+ changeTabRight() {
+ const activeTab = this.getActiveTab();
+ this.inputWorker.postMessage({
+ action: "changeTabRight",
+ data: {
+ activeTab: activeTab,
+ nums: this.getTabList()
+ }
+ });
+ }
+
+ /**
+ * Handler for clicking on next tab button
+ */
+ changeTabLeft() {
+ const activeTab = this.getActiveTab();
+ this.inputWorker.postMessage({
+ action: "changeTabLeft",
+ data: {
+ activeTab: activeTab,
+ nums: this.getTabList()
+ }
+ });
}
}
diff --git a/src/web/InputWorker.mjs b/src/web/InputWorker.mjs
new file mode 100644
index 00000000..b5b9fc04
--- /dev/null
+++ b/src/web/InputWorker.mjs
@@ -0,0 +1,610 @@
+/**
+ * Web Worker to handle loading data
+ *
+ * @author j433866 [j433866@gmail.com]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+
+self.maxWorkers = 4;
+self.maxTabs = 1;
+self.pendingFiles = [];
+self.inputs = {};
+self.loaderWorkerPorts = [];
+self.currentInputNum = 1;
+
+/**
+ * Respond to message from parent thread.
+ */
+self.addEventListener("message", function(e) {
+ const r = e.data;
+ if (!r.hasOwnProperty("action")) {
+ log.error("No action");
+ return;
+ }
+
+ switch (r.action) {
+ case "loadUIFiles":
+ self.loadFiles(r.data);
+ break;
+ case "loaderWorkerReady":
+ self.loaderWorkerReady(r.data);
+ break;
+ case "updateMaxWorkers":
+ self.maxWorkers = r.data;
+ break;
+ case "updateMaxTabs":
+ self.maxTabs = r.data;
+ break;
+ case "updateInputValue":
+ self.updateInputValue(r.data);
+ break;
+ case "getInputProgress":
+ self.getInputProgress(r.data);
+ break;
+ case "updateInputProgress":
+ self.updateInputProgress(r.data);
+ break;
+ case "getAll":
+ self.getAllInputs();
+ break;
+ case "getLoadProgress":
+ self.getLoadProgress(r.data);
+ break;
+ case "setInput":
+ self.setInput(r.data);
+ break;
+ case "setLogLevel":
+ log.setLevel(r.data, false);
+ break;
+ case "addInput":
+ self.addInput(true, "string");
+ break;
+ case "refreshTabs":
+ self.refreshTabs(r.data.inputNum, r.data.direction);
+ break;
+ case "removeInput":
+ self.removeInput(r.data);
+ break;
+ case "changeTabRight":
+ self.changeTabRight(r.data.activeTab, r.data.nums);
+ break;
+ case "changeTabLeft":
+ self.changeTabLeft(r.data.activeTab, r.data.nums);
+ break;
+ case "autobake":
+ self.autoBake(r.data);
+ break;
+ default:
+ log.error(`Unknown action '${r.action}'.`);
+ }
+});
+
+self.getLoadProgress = function(inputNum) {
+ const inputNums = Object.keys(self.inputs);
+ const total = inputNums.length;
+ const pending = self.pendingFiles.length;
+ let loaded = 0;
+ let loading = 0;
+
+ for (let i = 0; i < inputNums.length; i++) {
+ switch (self.inputs[inputNums[i]].status) {
+ case "loading":
+ loading++;
+ break;
+ case "loaded":
+ loaded++;
+ }
+ }
+
+ self.postMessage({
+ action: "loadingInfo",
+ data: {
+ pending: pending,
+ loading: loading,
+ loaded: loaded,
+ total: total,
+ activeProgress: {
+ inputNum: inputNum,
+ progress: self.getInputProgress(inputNum)
+ }
+ }
+ });
+
+ if (loaded < total) {
+ setTimeout(function(inputNum) {
+ self.getLoadProgress(inputNum);
+ }, 100);
+ }
+};
+
+self.autoBake = function(inputNum) {
+ const input = self.getInputObj(inputNum);
+ if (input) {
+ let inputData = input.data;
+ if (typeof inputData !== "string") {
+ inputData = inputData.fileBuffer;
+ }
+ self.postMessage({
+ action: "allInputs",
+ data: [{
+ input: inputData,
+ inputNum: parseInt(inputNum, 10)
+ }]
+ });
+ }
+};
+
+self.getAllInputs = function() {
+ const inputs = [];
+ const inputNums = Object.keys(self.inputs);
+
+ for (let i = 0; i < inputNums.length; i++) {
+ if (self.inputs[inputNums[i]].status === "loaded") {
+ let inputData = self.inputs[inputNums[i]].data;
+ if (typeof inputData !== "string") {
+ inputData = inputData.fileBuffer;
+ }
+ inputs.push({
+ input: inputData,
+ inputNum: inputNums[i]
+ });
+ }
+ }
+
+ self.postMessage({
+ action: "allInputs",
+ data: inputs
+ });
+
+};
+
+self.getInputObj = function(inputNum) {
+ return self.inputs[inputNum];
+};
+
+self.getInputValue = function(inputNum) {
+ for (let i = 0; i < self.inputs.length; i++) {
+ if (self.inputs[i].inputNum === inputNum) {
+ if (self.inputs[i].status === "loaded") {
+ let inputData = self.inputs[i].data;
+ if (typeof inputData !== "string") {
+ inputData = inputData.fileBuffer;
+ }
+ return inputData;
+ }
+ }
+ }
+ return "";
+};
+
+self.getInputProgress = function(inputNum) {
+ const inputObj = self.getInputObj(inputNum);
+ if (inputObj === undefined || inputObj === null) return;
+ return inputObj.progress;
+};
+
+ /**
+ * Gets the largest inputNum
+ *
+ * @returns {number}
+ */
+self.getLargestInputNum = function() {
+ let largest = 0;
+ const inputNums = Object.keys(self.inputs);
+ for (let i = 0; i < inputNums.length; i++) {
+ const num = parseInt(inputNums[i], 10);
+ if (num > largest) {
+ largest = num;
+ }
+ }
+ return largest;
+};
+
+ /**
+ * Gets the smallest inputNum
+ *
+ * @returns {number}
+ */
+self.getSmallestInputNum = function() {
+ let smallest = self.getLargestInputNum();
+ const inputNums = Object.keys(self.inputs);
+ for (let i = 0; i < inputNums.length; i++) {
+ const num = parseInt(inputNums[i], 10);
+ if (num < smallest) {
+ smallest = num;
+ }
+ }
+ return smallest;
+};
+
+/**
+ * Gets the previous inputNum
+ *
+ * @param {number} inputNum - The current input number
+ * @returns {number}
+ */
+self.getPreviousInputNum = function(inputNum) {
+ let num = -1;
+ const inputNums = Object.keys(self.inputs);
+ for (let i = 0; i < inputNums.length; i++) {
+ const iNum = parseInt(inputNums[i], 10);
+ if (iNum < inputNum) {
+ if (iNum > num) {
+ num = iNum;
+ }
+ }
+ }
+ return num;
+};
+
+/**
+ * Gets the next inputNum
+ *
+ * @param {number} inputNum - The current input number
+ * @returns {number}
+ */
+self.getNextInputNum = function(inputNum) {
+ let num = self.getLargestInputNum();
+ const inputNums = Object.keys(self.inputs);
+ for (let i = 0; i < inputNums.length; i++) {
+ const iNum = parseInt(inputNums[i], 10);
+ if (iNum > inputNum) {
+ if (iNum < num) {
+ num = iNum;
+ }
+ }
+ }
+ return num;
+};
+
+self.getNearbyNums = function(inputNum, direction) {
+ const nums = [];
+ for (let i = 0; i < self.maxTabs; i++) {
+ let newNum;
+ if (i === 0 && self.inputs[inputNum] !== undefined) {
+ newNum = inputNum;
+ } else {
+ switch (direction) {
+ case "left":
+ newNum = self.getNextInputNum(nums[i - 1]);
+ if (newNum === nums[i - 1]) {
+ direction = "right";
+ newNum = self.getPreviousInputNum(nums[i - 1]);
+ }
+ break;
+ case "right":
+ newNum = self.getPreviousInputNum(nums[i - 1]);
+ if (newNum === nums[i - 1]) {
+ direction = "left";
+ newNum = self.getNextInputNum(nums[i - 1]);
+ }
+ }
+ }
+ if (!nums.includes(newNum) && (newNum > 0)) {
+ nums.push(newNum);
+ }
+ }
+ nums.sort(function(a, b) {
+ return a - b;
+ });
+ return nums;
+};
+
+self.updateTabHeader = function(inputNum) {
+ const input = self.getInputObj(inputNum);
+ if (input === null || input === undefined) return;
+ let inputData = input.data;
+ if (typeof inputData !== "string") {
+ inputData = input.data.name;
+ }
+ self.postMessage({
+ action: "updateTabHeader",
+ data: {
+ inputNum: inputNum,
+ input: inputData.slice(0, 100)
+ }
+ });
+};
+
+self.setInput = function(inputNum) {
+ const input = self.getInputObj(inputNum);
+ if (input === undefined || input === null) return;
+
+ const inputVal = input.data;
+ const inputObj = {
+ inputNum: inputNum,
+ input: inputVal
+ };
+ if (typeof inputVal !== "string") {
+ inputObj.input = inputVal.fileBuffer.slice(0, 4096);
+ inputObj.name = inputVal.name;
+ inputObj.size = inputVal.size;
+ inputObj.type = inputVal.type;
+ inputObj.progress = input.progress;
+ }
+
+ self.postMessage({
+ action: "setInput",
+ data: inputObj
+ });
+ self.getInputProgress(inputNum);
+};
+
+self.refreshTabs = function(inputNum, direction) {
+ const nums = self.getNearbyNums(inputNum, direction);
+ self.postMessage({
+ action: "refreshTabs",
+ data: {
+ nums: nums,
+ activeTab: (nums.includes(inputNum)) ? inputNum : self.getNextInputNum(inputNum)
+ }
+ });
+
+ for (let i = 0; i < nums.length; i++) {
+ self.updateTabHeader(nums[i]);
+ }
+
+ // self.setInput(inputNum);
+};
+
+self.updateInputStatus = function(inputNum, status) {
+ for (let i = 0; i < self.inputs.length; i++) {
+ if (self.inputs[i].inputNum === inputNum) {
+ self.inputs[i].status = status;
+ return;
+ }
+ }
+};
+
+self.updateInputProgress = function(inputData) {
+ const inputNum = inputData.inputNum;
+ const progress = inputData.progress;
+
+ if (self.inputs[inputNum] !== undefined) {
+ self.inputs[inputNum].progress = progress;
+ }
+};
+
+self.updateInputValue = function(inputData) {
+ const inputNum = inputData.inputNum;
+ if (inputNum < 1) return;
+ const value = inputData.value;
+ if (self.inputs[inputNum] !== undefined) {
+ if (typeof value === "string") {
+ self.inputs[inputNum].data = value;
+ } else {
+ self.inputs[inputNum].data.fileBuffer = value;
+ }
+ self.inputs[inputNum].status = "loaded";
+ self.inputs[inputNum].progress = 100;
+ return;
+ }
+
+ // If we get to here, an input for inputNum could not be found
+ // Only do this if the value is a string, as loadFiles will create
+ // the input object for files
+ if (typeof value === "string") {
+ self.inputs.push({
+ inputNum: inputNum,
+ data: value,
+ status: "loaded",
+ progress: 100
+ });
+ }
+};
+
+self.getLoaderWorkerIdx = function(workerId) {
+ for (let i = 0; i < self.loaderWorkerPorts.length; i++) {
+ if (self.loaderWorkerPorts[i].id === workerId) {
+ return i;
+ }
+ }
+ return -1;
+};
+
+self.loaderWorkerReady = function(workerData) {
+ const newWorkerObj = {
+ id: workerData.id,
+ port: workerData.port,
+ inputNum: -1,
+ active: true
+ };
+ newWorkerObj.port.onmessage = function (e) {
+ self.handleLoaderMessage(e);
+ };
+ self.loaderWorkerPorts.push(newWorkerObj);
+ self.loadNextFile(self.loaderWorkerPorts.indexOf(newWorkerObj));
+};
+
+self.handleLoaderMessage = function(e) {
+ const r = e.data;
+ let inputNum = 0;
+
+ if (r.hasOwnProperty("inputNum")) {
+ inputNum = r.inputNum;
+ }
+
+ if (r.hasOwnProperty("fileBuffer")) {
+ log.debug(`Input file ${inputNum} loaded.`);
+ self.updateInputValue({
+ inputNum: inputNum,
+ value: r.fileBuffer
+ });
+ const idx = self.getLoaderWorkerIdx(r.id);
+ self.loadNextFile(idx);
+ } else if (r.hasOwnProperty("progress")) {
+ self.updateInputProgress(r);
+ }
+};
+
+self.loadNextFile = function(workerIdx) {
+ if (workerIdx === -1) return; // No more workers can be created
+ const port = self.loaderWorkerPorts[workerIdx].port;
+ if (self.pendingFiles.length === 0) {
+ const workerObj = self.loaderWorkerPorts.splice(workerIdx, 1)[0];
+ self.terminateLoaderWorker(workerObj.id);
+ return;
+ }
+
+ const nextFile = self.pendingFiles.splice(0, 1)[0];
+ self.loaderWorkerPorts[workerIdx].inputNum = nextFile.inputNum;
+ port.postMessage({
+ action: "loadInput",
+ data: {
+ file: nextFile.file,
+ inputNum: nextFile.inputNum
+ }
+ });
+};
+
+self.activateLoaderWorker = function() {
+ for (let i = 0; i < self.loaderWorkerPorts.length; i++) {
+ if (!self.loaderWorkerPorts[i].active) {
+ self.loaderWorkerPorts[i].active = true;
+ self.loadNextFile(i);
+ return;
+ }
+ }
+ self.postMessage({
+ action: "activateLoaderWorker"
+ });
+};
+
+self.terminateLoaderWorker = function(id) {
+ self.postMessage({
+ action: "terminateLoaderWorker",
+ data: id
+ });
+ if (self.pendingFiles.length > 0) {
+ self.activateLoaderWorker();
+ }
+};
+
+/**
+ * Loads files using LoaderWorkers
+ */
+self.loadFiles = function(files) {
+ let lastInputNum = -1;
+ const inputNums = [];
+ for (let i = 0; i < files.length; i++) {
+ lastInputNum = self.addInput(false, "file", {
+ name: files[i].name,
+ size: files[i].size.toLocaleString(),
+ type: files[i].type || "unknown"
+ });
+ inputNums.push(lastInputNum);
+
+ self.pendingFiles.push({
+ file: files[i],
+ inputNum: lastInputNum
+ });
+ }
+ let max = self.maxWorkers;
+ if (self.pendingFiles.length < self.maxWorkers) max = self.pendingFiles.length;
+
+ for (let i = 0; i < max; i++) {
+ self.activateLoaderWorker();
+ }
+
+ // self.refreshTabs(lastInputNum, "right");
+ self.getLoadProgress();
+
+ self.postMessage({
+ action: "addInputs",
+ data: inputNums
+ });
+};
+
+/**
+ * Adds an input to the input array
+ *
+ * @param {boolean} [changetab=false] - Whether or not to send a message to the main thread that the input has been created
+ * @param {string} type - Either "string" or "file"
+ * @param {Object} fileData - Contains information about the file to be added to the input
+ * @param {string} fileData.name
+ * @param {string} fileData.size
+ * @param {string} fileData.type
+ */
+self.addInput = function(changeTab=false, type, fileData={name: "unknown", size: "unknown", type: "unknown"}) {
+ const inputNum = self.currentInputNum++;
+ const newInputObj = {
+ inputNum: inputNum
+ };
+
+ switch (type) {
+ case "string":
+ newInputObj.data = "";
+ newInputObj.status = "loaded";
+ newInputObj.progress = 100;
+ break;
+ case "file":
+ newInputObj.data = {
+ fileBuffer: new ArrayBuffer(),
+ name: fileData.name,
+ size: fileData.size,
+ type: fileData.type
+ };
+ newInputObj.status = "pending";
+ newInputObj.progress = 0;
+ break;
+ default:
+ log.error(`Invalid type '${type}'.`);
+ return -1;
+ }
+ self.inputs[inputNum] = newInputObj;
+
+ if (changeTab) {
+ self.postMessage({
+ action: "inputAdded",
+ data: {
+ changeTab: changeTab,
+ inputNum: inputNum
+ }
+ });
+ }
+
+ return inputNum;
+};
+
+self.removeInput = function(removeInputData) {
+ const inputNum = removeInputData.inputNum;
+ const refreshTabs = removeInputData.refreshTabs;
+
+ delete self.inputs[inputNum];
+
+ for (let i = 0; i < self.loaderWorkerPorts.length; i++) {
+ if (self.loaderWorkerPorts[i].inputNum === inputNum) {
+ self.terminateLoaderWorker(self.loaderWorkerPorts[i].id);
+ }
+ }
+
+ if (refreshTabs) {
+ self.refreshTabs(inputNum, "left");
+ }
+};
+
+self.changeTabRight = function(inputNum, tabNums) {
+ const newInput = self.getNextInputNum(inputNum);
+ if (tabNums.includes(newInput)) {
+ self.postMessage({
+ action: "changeTab",
+ data: newInput
+ });
+ } else {
+ self.refreshTabs(newInput, "right");
+ }
+};
+
+self.changeTabLeft = function(inputNum, tabNums) {
+ const newInput = self.getPreviousInputNum(inputNum);
+ if (tabNums.includes(newInput)) {
+ self.postMessage({
+ action: "changeTab",
+ data: newInput
+ });
+ } else {
+ self.refreshTabs(newInput, "left");
+ }
+};
diff --git a/src/web/LoaderWorker.js b/src/web/LoaderWorker.js
index 7e7d1c3e..97b4dc59 100755
--- a/src/web/LoaderWorker.js
+++ b/src/web/LoaderWorker.js
@@ -6,6 +6,21 @@
* @license Apache-2.0
*/
+self.port = null;
+self.id = null;
+
+
+self.handlePortMessage = function(e) {
+ const r = e.data;
+ log.debug(`LoaderWorker receiving command '${r.action}'`);
+
+ switch (r.action) {
+ case "loadInput":
+ self.loadFile(r.data.file, r.data.inputNum);
+ break;
+ }
+};
+
/**
* Respond to message from parent thread.
@@ -16,6 +31,12 @@ self.addEventListener("message", function(e) {
self.loadFile(r.file, r.inputNum);
} else if (r.hasOwnProperty("file")) {
self.loadFile(r.file, "");
+ } else if (r.hasOwnProperty("port")) {
+ self.port = r.port;
+ self.id = r.id;
+ self.port.onmessage = function(e) {
+ self.handlePortMessage(e);
+ };
}
});
@@ -28,17 +49,21 @@ self.addEventListener("message", function(e) {
*/
self.loadFile = function(file, inputNum) {
const reader = new FileReader();
- const data = new Uint8Array(file.size);
+ let data;
+ try {
+ data = new Uint8Array(file.size);
+ } catch (err) {
+ self.port.postMessage({"error": err, "inputNum": inputNum});
+ }
let offset = 0;
const CHUNK_SIZE = 10485760; // 10MiB
const seek = function() {
if (offset >= file.size) {
- self.postMessage({"progress": 100, "inputNum": inputNum});
- self.postMessage({"fileBuffer": data.buffer, "inputNum": inputNum}, [data.buffer]);
+ self.port.postMessage({"fileBuffer": data.buffer, "inputNum": inputNum, "id": self.id}, [data.buffer]);
return;
}
- self.postMessage({"progress": Math.round(offset / file.size * 100), "inputNum": inputNum});
+ // self.port.postMessage({"progress": Math.round(offset / file.size * 100), "inputNum": inputNum});
const slice = file.slice(offset, offset + CHUNK_SIZE);
reader.readAsArrayBuffer(slice);
};
@@ -50,7 +75,7 @@ self.loadFile = function(file, inputNum) {
};
reader.onerror = function(e) {
- self.postMessage({"error": reader.error.message});
+ self.port.postMessage({"error": reader.error.message, "inputNum": inputNum});
};
seek();
diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs
index 8b0925fb..2834575a 100755
--- a/src/web/Manager.mjs
+++ b/src/web/Manager.mjs
@@ -82,8 +82,8 @@ class Manager {
* Sets up the various components and listeners.
*/
setup() {
- this.input.addTab(true);
- this.input.setupLoaderWorker();
+ this.input.setupInputWorker();
+ this.input.addInput();
this.worker.setupChefWorker();
this.recipe.initialiseOperationDragNDrop();
this.controls.initComponents();
@@ -145,10 +145,10 @@ class Manager {
// Input
this.addMultiEventListener("#input-text", "keyup", this.input.inputChange, this.input);
- this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input);
+ // this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input);
document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app));
- document.getElementById("clr-io").addEventListener("click", this.input.clearAllIoClick.bind(this.input));
- this.addListeners("#open-file", "change", this.input.inputOpen, this.input);
+ // document.getElementById("clr-io").addEventListener("click", this.input.clearAllIoClick.bind(this.input));
+ this.addListeners("#open-file,#open-folder", "change", this.input.inputOpen, this.input);
this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input);
this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input);
this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input);
@@ -156,22 +156,37 @@ class Manager {
// document.getElementById("input-text").addEventListener("mouseup", this.highlighter.inputMouseup.bind(this.highlighter));
// document.getElementById("input-text").addEventListener("mousemove", this.highlighter.inputMousemove.bind(this.highlighter));
// this.addMultiEventListener("#input-text", "mousedown dblclick select", this.highlighter.inputMousedown, this.highlighter);
- document.querySelector("#input-file .close").addEventListener("click", this.input.clearIoClick.bind(this.input));
- document.getElementById("btn-new-tab").addEventListener("click", this.input.addTab.bind(this.input));
+ // document.querySelector("#input-file .close").addEventListener("click", this.input.clearIoClick.bind(this.input));
+ document.getElementById("btn-new-tab").addEventListener("click", this.input.addInput.bind(this.input));
document.getElementById("btn-previous-input-tab").addEventListener("click", this.input.changeTabLeft.bind(this.input));
document.getElementById("btn-next-input-tab").addEventListener("click", this.input.changeTabRight.bind(this.input));
- document.getElementById("btn-go-to-input-tab").addEventListener("click", this.input.goToTab.bind(this.input));
+ // document.getElementById("btn-go-to-input-tab").addEventListener("click", this.input.goToTab.bind(this.input));
+ // document.getElementById("btn-find-input-tab").addEventListener("click", this.input.findTab.bind(this.input));
this.addDynamicListener("#input-tabs li .btn-close-tab i", "click", this.input.removeTabClick, this.input);
this.addDynamicListener("#input-tabs li .input-tab-content", "click", this.input.changeTabClick, this.input);
+ // document.getElementById("input-show-pending").addEventListener("change", this.input.filterTabSearch.bind(this.input));
+ // document.getElementById("input-show-loading").addEventListener("change", this.input.filterTabSearch.bind(this.input));
+ // document.getElementById("input-show-loaded").addEventListener("change", this.input.filterTabSearch.bind(this.input));
+ // document.getElementById("input-filename-filter").addEventListener("change", this.input.filterTabSearch.bind(this.input));
+ // document.getElementById("input-filename-filter").addEventListener("keyup", this.input.filterTabSearch.bind(this.input));
+ // document.getElementById("input-content-filter").addEventListener("change", this.input.filterTabSearch.bind(this.input));
+ // document.getElementById("input-content-filter").addEventListener("keyup", this.input.filterTabSearch.bind(this.input));
+ // document.getElementById("input-num-results").addEventListener("change", this.input.filterTabSearch.bind(this.input));
+ // document.getElementById("input-num-results").addEventListener("keyup", this.input.filterTabSearch.bind(this.input));
+ // document.getElementById("input-filter-refresh").addEventListener("click", this.input.filterTabSearch.bind(this.input));
+ // this.addDynamicListener(".input-filter-result", "click", this.input.filterItemClick, this.input);
+ document.getElementById("btn-open-file").addEventListener("click", this.input.inputOpenClick.bind(this.input));
+ document.getElementById("btn-open-folder").addEventListener("click", this.input.folderOpenClick.bind(this.input));
+
// Output
document.getElementById("save-to-file").addEventListener("click", this.output.saveClick.bind(this.output));
document.getElementById("save-all-to-file").addEventListener("click", this.output.saveAllClick.bind(this.output));
- // document.getElementById("copy-output").addEventListener("click", this.output.copyClick.bind(this.output));
+ document.getElementById("copy-output").addEventListener("click", this.output.copyClick.bind(this.output));
// document.getElementById("switch").addEventListener("click", this.output.switchClick.bind(this.output));
// document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output));
// document.getElementById("maximise-output").addEventListener("click", this.output.maximiseOutputClick.bind(this.output));
- // document.getElementById("magic").addEventListener("click", this.output.magicClick.bind(this.output));
+ document.getElementById("magic").addEventListener("click", this.output.magicClick.bind(this.output));
// document.getElementById("output-text").addEventListener("scroll", this.highlighter.outputScroll.bind(this.highlighter));
// document.getElementById("output-text").addEventListener("mouseup", this.highlighter.outputMouseup.bind(this.highlighter));
// document.getElementById("output-text").addEventListener("mousemove", this.highlighter.outputMousemove.bind(this.highlighter));
@@ -179,8 +194,8 @@ class Manager {
// document.getElementById("output-html").addEventListener("mousemove", this.highlighter.outputHtmlMousemove.bind(this.highlighter));
// this.addMultiEventListener("#output-text", "mousedown dblclick select", this.highlighter.outputMousedown, this.highlighter);
// this.addMultiEventListener("#output-html", "mousedown dblclick select", this.highlighter.outputHtmlMousedown, this.highlighter);
- // this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output);
- // this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
+ this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output);
+ this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
// document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
// this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output);
this.addDynamicListener("#output-tabs-wrapper #output-tabs li .output-tab-content", "click", this.output.changeTabClick, this.output);
diff --git a/src/web/OptionsWaiter.mjs b/src/web/OptionsWaiter.mjs
index 3f08b91b..eb6bac18 100755
--- a/src/web/OptionsWaiter.mjs
+++ b/src/web/OptionsWaiter.mjs
@@ -168,6 +168,7 @@ OptionsWaiter.prototype.logLevelChange = function (e) {
const level = e.target.value;
log.setLevel(level, false);
this.manager.worker.setLogLevel();
+ this.manager.input.setLogLevel();
};
export default OptionsWaiter;
diff --git a/src/web/OutputWaiter.mjs b/src/web/OutputWaiter.mjs
index 62a3985a..193ae483 100755
--- a/src/web/OutputWaiter.mjs
+++ b/src/web/OutputWaiter.mjs
@@ -26,7 +26,7 @@ class OutputWaiter {
this.app = app;
this.manager = manager;
- this.outputs = [];
+ this.outputs = {};
this.maxTabs = 4; // Calculate this
}
@@ -46,31 +46,17 @@ class OutputWaiter {
* @returns {string | ArrayBuffer}
*/
getOutput(inputNum) {
- const index = this.getOutputIndex(inputNum);
- if (index === -1) return -1;
+ if (this.outputs[inputNum] === undefined || this.outputs[inputNum] === null) return -1;
- if (typeof this.outputs[index].data.dish.value === "string") {
- return this.outputs[index].data.dish.value;
+ if (this.outputs[inputNum].data === null) return "";
+
+ if (typeof this.outputs[inputNum].data.dish.value === "string") {
+ return this.outputs[inputNum].data.dish.value;
} else {
- return this.outputs[index].data.dish.value || "";
+ return this.outputs[inputNum].data.dish.value || "";
}
}
- /**
- * Gets the index of the output for the specified input number
- *
- * @param {number} inputNum
- * @returns {number}
- */
- getOutputIndex(inputNum) {
- for (let i = 0; i < this.outputs.length; i++) {
- if (this.outputs[i].inputNum === inputNum) {
- return i;
- }
- }
- return -1;
- }
-
/**
* Gets the output string or FileBuffer for the active input
*
@@ -88,10 +74,10 @@ class OutputWaiter {
* @param {boolean} [changeTab=true]
*/
addOutput(inputNum, changeTab = true) {
- const index = this.getOutputIndex(inputNum);
- if (index !== -1) {
+ const output = this.getOutput(inputNum);
+ if (output !== -1) {
// Remove the output if it already exists
- this.outputs.splice(index, 1);
+ delete this.outputs[inputNum];
}
const newOutput = {
data: null,
@@ -102,11 +88,10 @@ class OutputWaiter {
status: "inactive"
};
- this.outputs.push(newOutput);
+ this.outputs[inputNum] = newOutput;
// add new tab
this.addTab(inputNum, changeTab);
- return this.outputs.indexOf(newOutput);
}
/**
@@ -117,13 +102,11 @@ class OutputWaiter {
* @param {number} inputNum
*/
updateOutputValue(data, inputNum) {
- let index = this.getOutputIndex(inputNum);
- if (index === -1) {
- index = this.addOutput(inputNum);
- this.addTab(inputNum, true);
+ if (this.getOutput(inputNum) === -1) {
+ this.addOutput(inputNum);
}
- this.outputs[index].data = data;
+ this.outputs[inputNum].data = data;
// set output here
this.set(inputNum);
@@ -137,10 +120,8 @@ class OutputWaiter {
* @param {number} inputNum
*/
updateOutputMessage(statusMessage, inputNum) {
- const index = this.getOutputIndex(inputNum);
- if (index === -1) return;
-
- this.outputs[index].statusMessage = statusMessage;
+ if (this.getOutput(inputNum) === -1) return;
+ this.outputs[inputNum].statusMessage = statusMessage;
this.set(inputNum);
}
@@ -153,10 +134,9 @@ class OutputWaiter {
* @param {number} inputNum
*/
updateOutputError(error, inputNum) {
- const index = this.getOutputIndex(inputNum);
- if (index === -1) return;
+ if (this.getOutput(inputNum) === -1) return;
- this.outputs[index].error = error;
+ this.outputs[inputNum].error = error;
// call handle error here
// or make the error handling part of set()
@@ -170,10 +150,8 @@ class OutputWaiter {
* @param {number} inputNum
*/
updateOutputStatus(status, inputNum) {
- const index = this.getOutputIndex(inputNum);
- if (index === -1) return;
-
- this.outputs[index].status = status;
+ if (this.getOutput(inputNum) === -1) return;
+ this.outputs[inputNum].status = status;
this.set(inputNum);
}
@@ -184,10 +162,9 @@ class OutputWaiter {
* @param {number} inputNum
*/
removeOutput(inputNum) {
- const index = this.getOutputIndex(inputNum);
- if (index === -1) return;
+ if (this.getOutput(inputNum) === -1) return;
- this.outputs.splice(index, 1);
+ delete (this.outputs[inputNum]);
}
/**
@@ -196,9 +173,11 @@ class OutputWaiter {
* @param {number} inputNum
*/
set(inputNum) {
- const outputIndex = this.getOutputIndex(inputNum);
- if (outputIndex === -1) return;
- const output = this.outputs[outputIndex];
+ const output = this.outputs[inputNum];
+ if (output === undefined || output === null) return;
+
+ if (typeof inputNum !== "number") inputNum = parseInt(inputNum, 10);
+
const outputText = document.getElementById("output-text");
const outputHtml = document.getElementById("output-html");
const outputFile = document.getElementById("output-file");
@@ -208,6 +187,11 @@ class OutputWaiter {
// If pending or baking, show loader and status message
// If error, style the tab and handle the error
// If done, display the output if it's the active tab
+ if (output.status === "inactive" || output.status === "stale") {
+ this.manager.controls.showStaleIndicator();
+ } else {
+ this.manager.controls.hideStaleIndicator();
+ }
if (output.status === "inactive") {
// An output is inactive when it has been created but has not been baked at all
@@ -229,7 +213,6 @@ class OutputWaiter {
// otherwise don't do anything
if (inputNum === this.getActiveTab()) {
this.toggleLoader(true);
-
document.querySelector("#output-loader .loading-msg").textContent = output.statusMessage;
}
@@ -246,6 +229,7 @@ class OutputWaiter {
this.toggleLoader(false);
this.closeFile();
let scriptElements, lines, length;
+ const duration = output.data.duration;
switch (output.data.type) {
case "html":
@@ -267,6 +251,7 @@ class OutputWaiter {
log.error(err);
}
}
+ length = output.data.dish.value.length;
break;
case "ArrayBuffer":
@@ -277,8 +262,8 @@ class OutputWaiter {
outputText.value = "";
outputHtml.innerHTML = "";
- length = output.data.result.byteLength;
+ length = output.data.result.length;
this.setFile(output.data.result);
break;
case "string":
@@ -296,6 +281,8 @@ class OutputWaiter {
length = output.data.result.length;
break;
}
+ this.setOutputInfo(length, lines, duration);
+ this.backgroundMagic();
}
}
}
@@ -368,7 +355,7 @@ class OutputWaiter {
outputElement.disabled = true;
outputLoader.style.visibility = "visible";
outputLoader.style.opacity = 1;
- }.bind(this), 200);
+ }, 200);
} else {
// Remove the Bombe from the DOM to save resources
this.outputLoaderTimeout = setTimeout(function () {
@@ -388,7 +375,7 @@ class OutputWaiter {
* Saves the current output to a file.
*/
saveClick() {
- this.downloadFile(this.getActiveTab());
+ this.downloadFile();
}
/**
@@ -452,6 +439,8 @@ class OutputWaiter {
document.getElementById("output-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
document.getElementById("output-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
document.getElementById("output-loader").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+
+ document.getElementById("save-all-to-file").style.display = "inline-block";
}
}
@@ -468,7 +457,7 @@ class OutputWaiter {
*/
changeTab(inputNum, changeInput = false) {
const currentNum = this.getActiveTab();
- if (this.getOutputIndex(inputNum) === -1) return;
+ if (this.getOutput(inputNum) === -1) return;
const tabsWrapper = document.getElementById("output-tabs");
const tabs = tabsWrapper.children;
@@ -523,12 +512,7 @@ class OutputWaiter {
*/
changeTabLeft() {
const currentTab = this.getActiveTab();
- const currentOutput = this.getOutputIndex(currentTab);
- if (currentOutput > 0) {
- this.changeTab(this.getPreviousInputNum(currentTab), this.app.options.syncTabs);
- } else {
- this.changeTab(this.getSmallestInputNum(), this.app.options.syncTabs);
- }
+ this.changeTab(this.getPreviousInputNum(currentTab), this.app.options.syncTabs);
}
/**
@@ -594,9 +578,11 @@ class OutputWaiter {
*/
getLargestInputNum() {
let largest = 0;
- for (let i = 0; i < this.outputs.length; i++) {
- if (this.outputs[i].inputNum > largest) {
- largest = this.outputs[i].inputNum;
+ const inputNums = Object.keys(this.outputs);
+ for (let i = 0; i < inputNums.length; i++) {
+ const iNum = parseInt(inputNums[i], 10);
+ if (iNum > largest) {
+ largest = iNum;
}
}
return largest;
@@ -609,9 +595,11 @@ class OutputWaiter {
*/
getSmallestInputNum() {
let smallest = this.getLargestInputNum();
- for (let i = 0; i < this.outputs.length; i++) {
- if (this.outputs[i].inputNum < smallest) {
- smallest = this.outputs[i].inputNum;
+ const inputNums = Object.keys(this.outputs);
+ for (let i = 0; i < inputNums.length; i++) {
+ const iNum = parseInt(inputNums[i], 10);
+ if (iNum < smallest) {
+ smallest = iNum;
}
}
return smallest;
@@ -625,10 +613,12 @@ class OutputWaiter {
*/
getPreviousInputNum(inputNum) {
let num = this.getSmallestInputNum();
- for (let i = 0; i < this.outputs.length; i++) {
- if (this.outputs[i].inputNum < inputNum) {
- if (this.outputs[i].inputNum > num) {
- num = this.outputs[i].inputNum;
+ const inputNums = Object.keys(this.outputs);
+ for (let i = 0; i < inputNums.length; i++) {
+ const iNum = parseInt(inputNums[i], 10);
+ if (iNum < inputNum) {
+ if (iNum > num) {
+ num = iNum;
}
}
}
@@ -643,10 +633,12 @@ class OutputWaiter {
*/
getNextInputNum(inputNum) {
let num = this.getLargestInputNum();
- for (let i = 0; i < this.outputs.length; i++) {
- if (this.outputs[i].inputNum > inputNum) {
- if (this.outputs[i].inputNum < num) {
- num = this.outputs[i].inputNum;
+ const inputNums = Object.keys(this.outputs);
+ for (let i = 0; i < inputNums.length; i++) {
+ const iNum = parseInt(inputNums[i], 10);
+ if (iNum > inputNum) {
+ if (iNum < num) {
+ num = iNum;
}
}
}
@@ -660,7 +652,7 @@ class OutputWaiter {
*/
removeTab(inputNum) {
let activeTab = this.getActiveTab();
- if (this.getOutputIndex(inputNum) === -1) return;
+ if (this.getOutput(inputNum) === -1) return;
const tabElement = this.getTabItem(inputNum);
@@ -706,6 +698,8 @@ class OutputWaiter {
document.getElementById("output-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
document.getElementById("output-loader").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+ document.getElementById("save-all-to-file").style.display = "inline-block";
+
} else {
tabsList.parentElement.style.display = "none";
@@ -713,6 +707,8 @@ class OutputWaiter {
document.getElementById("output-highlighter").style.height = "calc(100% - var(--title-height))";
document.getElementById("output-file").style.height = "calc(100% - var(--title-height))";
document.getElementById("output-loader").style.height = "calc(100% - var(--title-height))";
+
+ document.getElementById("save-all-to-file").style.display = "none";
}
this.changeTab(activeTab);
@@ -782,6 +778,159 @@ class OutputWaiter {
tabContent.innerText = `Tab ${inputNum}`;
}
+
+ /**
+ * Displays information about the output.
+ *
+ * @param {number} length - The length of the current output string
+ * @param {number} lines - The number of the lines in the current output string
+ * @param {number} duration - The length of time (ms) it took to generate the output
+ */
+ setOutputInfo(length, lines, duration) {
+ if (!length) return;
+ let width = length.toString().length;
+ width = width < 4 ? 4 : width;
+
+ const lengthStr = length.toString().padStart(width, " ").replace(/ /g, " ");
+ const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, " ");
+
+ let msg = "time: " + timeStr + "
length: " + lengthStr;
+
+ if (typeof lines === "number") {
+ const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
+ msg += "
lines: " + linesStr;
+ }
+
+ document.getElementById("output-info").innerHTML = msg;
+ document.getElementById("input-selection-info").innerHTML = "";
+ document.getElementById("output-selection-info").innerHTML = "";
+ }
+
+ /**
+ * Triggers the BackgroundWorker to attempt Magic on the current output.
+ */
+ backgroundMagic() {
+ this.hideMagicButton();
+ if (!this.app.options.autoMagic || this.getActive()) return;
+ const sample = this.getActive().slice(0, 1000) || "";
+
+ if (sample.length) {
+ this.manager.background.magic(sample);
+ }
+ }
+
+ /**
+ * Handles the results of a background Magic call.
+ *
+ * @param {Object[]} options
+ */
+ backgroundMagicResult(options) {
+ if (!options.length ||
+ !options[0].recipe.length)
+ return;
+
+ const currentRecipeConfig = this.app.getRecipeConfig();
+ const newRecipeConfig = currentRecipeConfig.concat(options[0].recipe);
+ const opSequence = options[0].recipe.map(o => o.op).join(", ");
+
+ this.showMagicButton(opSequence, options[0].data, newRecipeConfig);
+ }
+
+ /**
+ * Handler for Magic click events.
+ *
+ * Loads the Magic recipe.
+ *
+ * @fires Manager#statechange
+ */
+ magicClick() {
+ const magicButton = document.getElementById("magic");
+ this.app.setRecipeConfig(JSON.parse(magicButton.getAttribute("data-recipe")));
+ window.dispatchEvent(this.manager.statechange);
+ this.hideMagicButton();
+ }
+
+ /**
+ * Displays the Magic button with a title and adds a link to a complete recipe.
+ *
+ * @param {string} opSequence
+ * @param {string} result
+ * @param {Object[]} recipeConfig
+ */
+ showMagicButton(opSequence, result, recipeConfig) {
+ const magicButton = document.getElementById("magic");
+ magicButton.setAttribute("data-original-title", `${opSequence} will produce "${Utils.escapeHtml(Utils.truncate(result), 30)}"`);
+ magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, "");
+ magicButton.classList.remove("hidden");
+ }
+
+
+ /**
+ * Hides the Magic button and resets its values.
+ */
+ hideMagicButton() {
+ const magicButton = document.getElementById("magic");
+ magicButton.classList.add("hidden");
+ magicButton.setAttribute("data-recipe", "");
+ magicButton.setAttribute("data-original-title", "Magic!");
+ }
+
+
+ /**
+ * Handler for file slice display events.
+ */
+ displayFileSlice() {
+ const startTime = new Date().getTime(),
+ showFileOverlay = document.getElementById("show-file-overlay"),
+ sliceFromEl = document.getElementById("output-file-slice-from"),
+ sliceToEl = document.getElementById("output-file-slice-to"),
+ sliceFrom = parseInt(sliceFromEl.value, 10),
+ sliceTo = parseInt(sliceToEl.value, 10),
+ str = Utils.arrayBufferToStr(this.getActive().slice(sliceFrom, sliceTo));
+
+ document.getElementById("output-text").classList.remove("blur");
+ showFileOverlay.style.display = "block";
+
+ }
+
+
+ /**
+ * Handler for copy click events.
+ * Copies the output to the clipboard
+ */
+ copyClick() {
+ const output = this.getActive();
+
+ // Create invisible textarea to populate with the raw dish string (not the printable version that
+ // contains dots instead of the actual bytes)
+ const textarea = document.createElement("textarea");
+ textarea.style.position = "fixed";
+ textarea.style.top = 0;
+ textarea.style.left = 0;
+ textarea.style.width = 0;
+ textarea.style.height = 0;
+ textarea.style.border = "none";
+
+ textarea.value = output;
+ document.body.appendChild(textarea);
+
+ let success = false;
+ try {
+ textarea.select();
+ success = output && document.execCommand("copy");
+ } catch (err) {
+ success = false;
+ }
+
+ if (success) {
+ this.app.alert("Copied raw output successfully.", 2000);
+ } else {
+ this.app.alert("Sorry, the output could not be copied.", 3000);
+ }
+
+ // Clean up
+ document.body.removeChild(textarea);
+ }
}
export default OutputWaiter;
diff --git a/src/web/WorkerWaiter.mjs b/src/web/WorkerWaiter.mjs
index 77898f63..2d4da8d7 100644
--- a/src/web/WorkerWaiter.mjs
+++ b/src/web/WorkerWaiter.mjs
@@ -139,33 +139,13 @@ class WorkerWaiter {
this.updateOutput(r.data, r.data.inputNum);
if (this.inputs.length > 0) {
- const nextInput = this.inputs[0];
- this.inputs.splice(0, 1);
- log.debug(`Baking input ${nextInput.inputNum}.`);
- this.manager.output.updateOutputStatus("baking", nextInput.inputNum);
- this.manager.output.updateOutputMessage("Baking...", nextInput.inputNum);
- currentWorker.inputNum = nextInput.inputNum;
- currentWorker.active = true;
- currentWorker.worker.postMessage({
- action: "bake",
- data: {
- input: nextInput.input,
- recipeConfig: nextInput.recipeConfig,
- options: nextInput.options,
- progress: nextInput.progress,
- step: nextInput.step,
- inputNum: nextInput.inputNum
- }
- });
- this.displayProgress();
+ this.bakeNextInput(this.chefWorkers.indexOf(currentWorker));
} else {
// The ChefWorker is no longer needed
log.debug("No more inputs to bake. Closing ChefWorker.");
currentWorker.active = false;
this.removeChefWorker(currentWorker);
- this.displayProgress();
-
const progress = this.getBakeProgress();
if (progress.total === progress.baked) {
this.bakingComplete();
@@ -209,6 +189,7 @@ class WorkerWaiter {
}
}
+
/**
* Update the value of an output
*
@@ -268,7 +249,6 @@ class WorkerWaiter {
this.inputs = [];
this.totalOutputs = 0;
this.manager.controls.showStaleIndicator();
- this.displayProgress();
}
/**
@@ -311,10 +291,40 @@ class WorkerWaiter {
log.debug("--- Bake complete ---");
}
+ /**
+ * Bakes the next input
+ *
+ * @param {number} workerIdx
+ */
+ bakeNextInput(workerIdx) {
+ if (this.inputs.length === 0) return;
+ if (workerIdx === -1) return;
+ if (!this.chefWorkers[workerIdx]) return;
+
+ const nextInput = this.inputs.splice(0, 1)[0];
+
+ log.debug(`Baking input ${nextInput.inputNum}.`);
+ this.manager.output.updateOutputStatus("baking", nextInput.inputNum);
+ this.manager.output.updateOutputMessage("Baking...", nextInput.inputNum);
+
+
+ this.chefWorkers[workerIdx].inputNum = nextInput.inputNum;
+ this.chefWorkers[workerIdx].active = true;
+ this.chefWorkers[workerIdx].worker.postMessage({
+ action: "bake",
+ data: {
+ input: nextInput.input,
+ recipeConfig: this.recipeConfig,
+ options: this.options,
+ progress: this.progress,
+ step: this.step,
+ inputNum: nextInput.inputNum
+ }
+ });
+ }
+
/**
* Bakes the current input using the current recipe.
- * Either sends the input and recipe to a ChefWorker,
- * or, if there's already the max running, adds it to inputs
*
* @param {string | Array} input
* @param {Object[]} recipeConfig
@@ -333,6 +343,34 @@ class WorkerWaiter {
}];
}
+ for (let i = 0; i < input.length; i++) {
+ this.manager.output.updateOutputStatus("pending", input[i].inputNum);
+
+ for (let x = 0; x < this.inputs.length; x++) {
+ if (this.inputs[x].inputNum === input[i].inputNum) {
+ this.inputs.splice(x, 1);
+ break;
+ }
+ }
+ }
+
+ this.totalOutputs += input.length;
+ this.inputs = input;
+
+ this.recipeConfig = recipeConfig;
+ this.options = options;
+ this.progress = progress;
+ this.step = step;
+
+ for (let i = 0; i < this.maxWorkers; i++) {
+ const workerIdx = this.addChefWorker();
+ if (workerIdx === -1) break;
+ this.bakeNextInput(workerIdx);
+ }
+ this.displayProgress();
+ return;
+
+
for (let i = 0; i < input.length; i++) {
this.totalOutputs++;
this.manager.output.updateOutputStatus("pending", input[i].inputNum);
@@ -469,6 +507,12 @@ class WorkerWaiter {
bakeInfo.innerHTML = msg;
+ if (progress.total !== progress.baked) {
+ setTimeout(function() {
+ this.displayProgress();
+ }.bind(this), 100);
+ }
+
}
}