From 406cbb0f41a2c2cd4d059bfc7a29e17db961569d Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 25 Apr 2019 16:32:48 +0100 Subject: [PATCH] Move input logic into a new worker (InputWorker) Change OutputWaiter to use dict of outputs instead of list LoaderWorker communicates with InputWorker using a messagechannel --- src/web/App.mjs | 28 +- src/web/InputWaiter.mjs | 1263 ++++++++++++++++--------------------- src/web/InputWorker.mjs | 610 ++++++++++++++++++ src/web/LoaderWorker.js | 35 +- src/web/Manager.mjs | 39 +- src/web/OptionsWaiter.mjs | 1 + src/web/OutputWaiter.mjs | 297 ++++++--- src/web/WorkerWaiter.mjs | 92 ++- 8 files changed, 1504 insertions(+), 861 deletions(-) create mode 100644 src/web/InputWorker.mjs 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); + } + } }