mirror of
https://github.com/gchq/CyberChef
synced 2025-01-10 03:28:48 +00:00
f43a868607
Fix switching the output to input not working properly. Add nicer confirmation boxes for zipping outputs.
1470 lines
48 KiB
JavaScript
Executable file
1470 lines
48 KiB
JavaScript
Executable file
/**
|
|
* @author n1474335 [n1474335@gmail.com]
|
|
* @author j433866 [j433866@gmail.com]
|
|
* @copyright Crown Copyright 2016
|
|
* @license Apache-2.0
|
|
*/
|
|
|
|
import Utils from "../../core/Utils.mjs";
|
|
import Dish from "../../core/Dish.mjs";
|
|
import FileSaver from "file-saver";
|
|
import ZipWorker from "worker-loader?inline&fallback=false!../workers/ZipWorker";
|
|
|
|
/**
|
|
* Waiter to handle events related to the output
|
|
*/
|
|
class OutputWaiter {
|
|
|
|
/**
|
|
* OutputWaiter constructor.
|
|
*
|
|
* @param {App} app - The main view object for CyberChef.
|
|
* @param {Manager} manager - The CyberChef event manager
|
|
*/
|
|
constructor(app, manager) {
|
|
this.app = app;
|
|
this.manager = manager;
|
|
|
|
this.outputs = {};
|
|
this.zipWorker = null;
|
|
this.maxTabs = this.manager.tabs.calcMaxTabs();
|
|
this.tabTimeout = null;
|
|
}
|
|
|
|
/**
|
|
* Calculates the maximum number of tabs to display
|
|
*/
|
|
calcMaxTabs() {
|
|
const numTabs = this.manager.tabs.calcMaxTabs();
|
|
if (numTabs !== this.maxTabs) {
|
|
this.maxTabs = numTabs;
|
|
this.refreshTabs(this.manager.tabs.getActiveOutputTab(), "right");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the dish object for an output.
|
|
*
|
|
* @param inputNum - The inputNum of the output to get the dish of
|
|
* @returns {Dish}
|
|
*/
|
|
getOutputDish(inputNum) {
|
|
if (this.outputExists(inputNum) &&
|
|
this.outputs[inputNum].data &&
|
|
this.outputs[inputNum].data.dish) {
|
|
return this.outputs[inputNum].data.dish;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks if an output exists in the output dictionary
|
|
*
|
|
* @param {number} inputNum - The number of the output we're looking for
|
|
* @returns {boolean}
|
|
*/
|
|
outputExists(inputNum) {
|
|
if (this.outputs[inputNum] === undefined ||
|
|
this.outputs[inputNum] === null) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Adds a new output to the output array.
|
|
* Creates a new tab if we have less than maxtabs tabs open
|
|
*
|
|
* @param {number} inputNum - The inputNum of the new output
|
|
* @param {boolean} [changeTab=true] - If true, change to the new output
|
|
*/
|
|
addOutput(inputNum, changeTab = true) {
|
|
// Remove the output (will only get removed if it already exists)
|
|
this.removeOutput(inputNum);
|
|
|
|
const newOutput = {
|
|
data: null,
|
|
inputNum: inputNum,
|
|
statusMessage: `Input ${inputNum} has not been baked yet.`,
|
|
error: null,
|
|
status: "inactive",
|
|
bakeId: -1,
|
|
progress: false
|
|
};
|
|
|
|
this.outputs[inputNum] = newOutput;
|
|
|
|
this.addTab(inputNum, changeTab);
|
|
}
|
|
|
|
/**
|
|
* Updates the value for the output in the output array.
|
|
* If this is the active output tab, updates the output textarea
|
|
*
|
|
* @param {ArrayBuffer | String} data
|
|
* @param {number} inputNum
|
|
* @param {boolean} set
|
|
*/
|
|
updateOutputValue(data, inputNum, set=true) {
|
|
if (!this.outputExists(inputNum)) {
|
|
this.addOutput(inputNum);
|
|
}
|
|
|
|
if (Object.prototype.hasOwnProperty.call(data, "dish")) {
|
|
data.dish = new Dish(data.dish);
|
|
}
|
|
|
|
this.outputs[inputNum].data = data;
|
|
|
|
const tabItem = this.manager.tabs.getOutputTabItem(inputNum);
|
|
if (tabItem) tabItem.style.background = "";
|
|
|
|
if (set) this.set(inputNum);
|
|
}
|
|
|
|
/**
|
|
* Updates the status message for the output in the output array.
|
|
* If this is the active output tab, updates the output textarea
|
|
*
|
|
* @param {string} statusMessage
|
|
* @param {number} inputNum
|
|
* @param {boolean} [set=true]
|
|
*/
|
|
updateOutputMessage(statusMessage, inputNum, set=true) {
|
|
if (!this.outputExists(inputNum)) return;
|
|
this.outputs[inputNum].statusMessage = statusMessage;
|
|
if (set) this.set(inputNum);
|
|
}
|
|
|
|
/**
|
|
* Updates the error value for the output in the output array.
|
|
* If this is the active output tab, calls app.handleError.
|
|
* Otherwise, the error will be handled when the output is switched to
|
|
*
|
|
* @param {Error} error
|
|
* @param {number} inputNum
|
|
* @param {number} [progress=0]
|
|
*/
|
|
updateOutputError(error, inputNum, progress=0) {
|
|
if (!this.outputExists(inputNum)) return;
|
|
|
|
const errorString = error.displayStr || error.toString();
|
|
|
|
this.outputs[inputNum].error = errorString;
|
|
this.outputs[inputNum].progress = progress;
|
|
this.updateOutputStatus("error", inputNum);
|
|
}
|
|
|
|
/**
|
|
* Updates the status value for the output in the output array
|
|
*
|
|
* @param {string} status
|
|
* @param {number} inputNum
|
|
*/
|
|
updateOutputStatus(status, inputNum) {
|
|
if (!this.outputExists(inputNum)) return;
|
|
this.outputs[inputNum].status = status;
|
|
|
|
if (status !== "error") {
|
|
delete this.outputs[inputNum].error;
|
|
}
|
|
|
|
this.displayTabInfo(inputNum);
|
|
this.set(inputNum);
|
|
}
|
|
|
|
/**
|
|
* Updates the stored bake ID for the output in the ouptut array
|
|
*
|
|
* @param {number} bakeId
|
|
* @param {number} inputNum
|
|
*/
|
|
updateOutputBakeId(bakeId, inputNum) {
|
|
if (!this.outputExists(inputNum)) return;
|
|
this.outputs[inputNum].bakeId = bakeId;
|
|
}
|
|
|
|
/**
|
|
* Updates the stored progress value for the output in the output array
|
|
*
|
|
* @param {number} progress
|
|
* @param {number} total
|
|
* @param {number} inputNum
|
|
*/
|
|
updateOutputProgress(progress, total, inputNum) {
|
|
if (!this.outputExists(inputNum)) return;
|
|
this.outputs[inputNum].progress = progress;
|
|
|
|
if (progress !== false) {
|
|
this.manager.tabs.updateOutputTabProgress(inputNum, progress, total);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Removes an output from the output array.
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
removeOutput(inputNum) {
|
|
if (!this.outputExists(inputNum)) return;
|
|
|
|
delete (this.outputs[inputNum]);
|
|
}
|
|
|
|
/**
|
|
* Removes all output tabs
|
|
*/
|
|
removeAllOutputs() {
|
|
this.outputs = {};
|
|
|
|
this.resetSwitch();
|
|
|
|
const tabsList = document.getElementById("output-tabs");
|
|
const tabsListChildren = tabsList.children;
|
|
|
|
tabsList.classList.remove("tabs-left");
|
|
tabsList.classList.remove("tabs-right");
|
|
for (let i = tabsListChildren.length - 1; i >= 0; i--) {
|
|
tabsListChildren.item(i).remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the output in the output textarea.
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
async set(inputNum) {
|
|
if (inputNum !== this.manager.tabs.getActiveOutputTab() ||
|
|
!this.outputExists(inputNum)) return;
|
|
this.toggleLoader(true);
|
|
|
|
return new Promise(async function(resolve, reject) {
|
|
const output = this.outputs[inputNum],
|
|
activeTab = this.manager.tabs.getActiveOutputTab();
|
|
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");
|
|
const outputHighlighter = document.getElementById("output-highlighter");
|
|
const inputHighlighter = document.getElementById("input-highlighter");
|
|
|
|
// 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 inactive, show the last bake value (or blank)
|
|
if (output.status === "inactive" ||
|
|
output.status === "stale" ||
|
|
(output.status === "baked" && output.bakeId < this.manager.worker.bakeId)) {
|
|
this.manager.controls.showStaleIndicator();
|
|
} else {
|
|
this.manager.controls.hideStaleIndicator();
|
|
}
|
|
|
|
if (output.progress !== undefined && !this.app.baking) {
|
|
this.manager.recipe.updateBreakpointIndicator(output.progress);
|
|
} else {
|
|
this.manager.recipe.updateBreakpointIndicator(false);
|
|
}
|
|
|
|
document.getElementById("show-file-overlay").style.display = "none";
|
|
|
|
if (output.status === "pending" || output.status === "baking") {
|
|
// show the loader and the status message if it's being shown
|
|
// otherwise don't do anything
|
|
document.querySelector("#output-loader .loading-msg").textContent = output.statusMessage;
|
|
} else if (output.status === "error") {
|
|
// style the tab if it's being shown
|
|
this.toggleLoader(false);
|
|
outputText.style.display = "block";
|
|
outputText.classList.remove("blur");
|
|
outputHtml.style.display = "none";
|
|
outputFile.style.display = "none";
|
|
outputHighlighter.display = "none";
|
|
inputHighlighter.display = "none";
|
|
|
|
if (output.error) {
|
|
outputText.value = output.error;
|
|
} else {
|
|
outputText.value = output.data.result;
|
|
}
|
|
outputHtml.innerHTML = "";
|
|
} else if (output.status === "baked" || output.status === "inactive") {
|
|
document.querySelector("#output-loader .loading-msg").textContent = `Loading output ${inputNum}`;
|
|
this.closeFile();
|
|
let scriptElements, lines, length;
|
|
|
|
if (output.data === null) {
|
|
outputText.style.display = "block";
|
|
outputHtml.style.display = "none";
|
|
outputFile.style.display = "none";
|
|
outputHighlighter.display = "block";
|
|
inputHighlighter.display = "block";
|
|
|
|
outputText.value = "";
|
|
outputHtml.innerHTML = "";
|
|
|
|
lines = 0;
|
|
length = 0;
|
|
this.toggleLoader(false);
|
|
return;
|
|
}
|
|
|
|
switch (output.data.type) {
|
|
case "html":
|
|
outputText.style.display = "none";
|
|
outputHtml.style.display = "block";
|
|
outputFile.style.display = "none";
|
|
outputHighlighter.style.display = "none";
|
|
inputHighlighter.style.display = "none";
|
|
|
|
outputText.value = "";
|
|
outputHtml.innerHTML = output.data.result;
|
|
|
|
// Execute script sections
|
|
scriptElements = outputHtml.querySelectorAll("script");
|
|
for (let i = 0; i < scriptElements.length; i++) {
|
|
try {
|
|
eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval
|
|
} catch (err) {
|
|
log.error(err);
|
|
}
|
|
}
|
|
break;
|
|
case "ArrayBuffer":
|
|
outputText.style.display = "block";
|
|
outputHtml.style.display = "none";
|
|
outputHighlighter.display = "none";
|
|
inputHighlighter.display = "none";
|
|
|
|
outputText.value = "";
|
|
outputHtml.innerHTML = "";
|
|
|
|
length = output.data.result.byteLength;
|
|
this.setFile(await this.getDishBuffer(output.data.dish), activeTab);
|
|
break;
|
|
case "string":
|
|
default:
|
|
outputText.style.display = "block";
|
|
outputHtml.style.display = "none";
|
|
outputFile.style.display = "none";
|
|
outputHighlighter.display = "block";
|
|
inputHighlighter.display = "block";
|
|
|
|
outputText.value = Utils.printable(output.data.result, true);
|
|
outputHtml.innerHTML = "";
|
|
|
|
lines = output.data.result.count("\n") + 1;
|
|
length = output.data.result.length;
|
|
break;
|
|
}
|
|
this.toggleLoader(false);
|
|
|
|
if (output.data.type === "html") {
|
|
const dishStr = await this.getDishStr(output.data.dish);
|
|
length = dishStr.length;
|
|
lines = dishStr.count("\n") + 1;
|
|
}
|
|
|
|
this.setOutputInfo(length, lines, output.data.duration);
|
|
this.backgroundMagic();
|
|
}
|
|
}.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Shows file details
|
|
*
|
|
* @param {ArrayBuffer} buf
|
|
* @param {number} activeTab
|
|
*/
|
|
setFile(buf, activeTab) {
|
|
if (activeTab !== this.manager.tabs.getActiveOutputTab()) return;
|
|
// Display file overlay in output area with details
|
|
const fileOverlay = document.getElementById("output-file"),
|
|
fileSize = document.getElementById("output-file-size"),
|
|
outputText = document.getElementById("output-text"),
|
|
fileSlice = buf.slice(0, 4096);
|
|
|
|
fileOverlay.style.display = "block";
|
|
fileSize.textContent = buf.byteLength.toLocaleString() + " bytes";
|
|
|
|
outputText.classList.add("blur");
|
|
outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
|
|
}
|
|
|
|
/**
|
|
* Clears output file details
|
|
*/
|
|
closeFile() {
|
|
document.getElementById("output-file").style.display = "none";
|
|
document.getElementById("output-text").classList.remove("blur");
|
|
}
|
|
|
|
/**
|
|
* Retrieves the dish as a string, returning the cached version if possible.
|
|
*
|
|
* @param {Dish} dish
|
|
* @returns {string}
|
|
*/
|
|
async getDishStr(dish) {
|
|
return await new Promise(resolve => {
|
|
this.manager.worker.getDishAs(dish, "string", r => {
|
|
resolve(r.value);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieves the dish as an ArrayBuffer, returning the cached version if possible.
|
|
*
|
|
* @param {Dish} dish
|
|
* @returns {ArrayBuffer}
|
|
*/
|
|
async getDishBuffer(dish) {
|
|
return await new Promise(resolve => {
|
|
this.manager.worker.getDishAs(dish, "ArrayBuffer", r => {
|
|
resolve(r.value);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieves the title of the Dish as a string
|
|
*
|
|
* @param {Dish} dish
|
|
* @param {number} maxLength
|
|
* @returns {string}
|
|
*/
|
|
async getDishTitle(dish, maxLength) {
|
|
return await new Promise(resolve => {
|
|
this.manager.worker.getDishTitle(dish, maxLength, r => {
|
|
resolve(r.value);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Save bombe object then remove it from the DOM so that it does not cause performance issues.
|
|
*/
|
|
saveBombe() {
|
|
this.bombeEl = document.getElementById("bombe");
|
|
this.bombeEl.parentNode.removeChild(this.bombeEl);
|
|
}
|
|
|
|
/**
|
|
* Shows or hides the output loading screen.
|
|
* The animated Bombe SVG, whilst quite aesthetically pleasing, is reasonably CPU
|
|
* intensive, so we remove it from the DOM when not in use. We only show it if the
|
|
* recipe is taking longer than 200ms. We add it to the DOM just before that so that
|
|
* it is ready to fade in without stuttering.
|
|
*
|
|
* @param {boolean} value - If true, show the loader
|
|
*/
|
|
toggleLoader(value) {
|
|
clearTimeout(this.appendBombeTimeout);
|
|
clearTimeout(this.outputLoaderTimeout);
|
|
|
|
const outputLoader = document.getElementById("output-loader"),
|
|
outputElement = document.getElementById("output-text"),
|
|
animation = document.getElementById("output-loader-animation");
|
|
|
|
if (value) {
|
|
this.manager.controls.hideStaleIndicator();
|
|
|
|
// Don't add the bombe if it's already there!
|
|
if (animation.children.length > 0) return;
|
|
|
|
// Start a timer to add the Bombe to the DOM just before we make it
|
|
// visible so that there is no stuttering
|
|
this.appendBombeTimeout = setTimeout(function() {
|
|
animation.appendChild(this.bombeEl);
|
|
}.bind(this), 150);
|
|
|
|
// Show the loading screen
|
|
this.outputLoaderTimeout = setTimeout(function() {
|
|
outputElement.disabled = true;
|
|
outputLoader.style.visibility = "visible";
|
|
outputLoader.style.opacity = 1;
|
|
}, 200);
|
|
} else {
|
|
// Remove the Bombe from the DOM to save resources
|
|
this.outputLoaderTimeout = setTimeout(function () {
|
|
try {
|
|
animation.removeChild(this.bombeEl);
|
|
} catch (err) {}
|
|
}.bind(this), 500);
|
|
outputElement.disabled = false;
|
|
outputLoader.style.opacity = 0;
|
|
outputLoader.style.visibility = "hidden";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for save click events.
|
|
* Saves the current output to a file.
|
|
*/
|
|
saveClick() {
|
|
this.downloadFile();
|
|
}
|
|
|
|
/**
|
|
* Handler for file download events.
|
|
*/
|
|
async downloadFile() {
|
|
const dish = this.getOutputDish(this.manager.tabs.getActiveOutputTab());
|
|
if (dish === null) {
|
|
this.app.alert("Could not find any output data to download. Has this output been baked?", 3000);
|
|
return;
|
|
}
|
|
const fileName = window.prompt("Please enter a filename: ", "download.dat");
|
|
|
|
// Assume if the user clicks cancel they don't want to download
|
|
if (fileName === null) return;
|
|
|
|
const data = await dish.get(Dish.ARRAY_BUFFER),
|
|
file = new File([data], fileName);
|
|
FileSaver.saveAs(file, fileName, false);
|
|
}
|
|
|
|
/**
|
|
* Handler for save all click event
|
|
* Saves all outputs to a single archvie file
|
|
*/
|
|
async saveAllClick() {
|
|
const downloadButton = document.getElementById("save-all-to-file");
|
|
if (downloadButton.firstElementChild.innerHTML === "archive") {
|
|
this.downloadAllFiles();
|
|
} else {
|
|
const cancel = await new Promise(function(resolve, reject) {
|
|
this.app.confirm(
|
|
"Cancel zipping?",
|
|
"The outputs are currently being zipped for download.<br>Cancel zipping?",
|
|
"Continue zipping",
|
|
"Cancel zipping",
|
|
resolve, this);
|
|
}.bind(this));
|
|
if (!cancel && cancel !== undefined) {
|
|
this.terminateZipWorker();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Spawns a new ZipWorker and sends it the outputs so that they can
|
|
* be zipped for download
|
|
*/
|
|
async downloadAllFiles() {
|
|
const inputNums = Object.keys(this.outputs);
|
|
for (let i = 0; i < inputNums.length; i++) {
|
|
const iNum = inputNums[i];
|
|
if (this.outputs[iNum].status !== "baked" ||
|
|
this.outputs[iNum].bakeId !== this.manager.worker.bakeId) {
|
|
const continueDownloading = await new Promise(function(resolve, reject) {
|
|
this.app.confirm(
|
|
"Incomplete outputs",
|
|
"Not all outputs have been baked yet. Continue downloading outputs?",
|
|
"Download", "Cancel", resolve, this);
|
|
}.bind(this));
|
|
if (continueDownloading) {
|
|
break;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let fileName = window.prompt("Please enter a filename: ", "download.zip");
|
|
|
|
if (fileName === null || fileName === "") {
|
|
// Don't zip the files if there isn't a filename
|
|
this.app.alert("No filename was specified.", 3000);
|
|
return;
|
|
}
|
|
|
|
if (!fileName.match(/.zip$/)) {
|
|
fileName += ".zip";
|
|
}
|
|
|
|
let fileExt = window.prompt("Please enter a file extension for the files, or leave blank to detect automatically.", "");
|
|
|
|
if (fileExt === null) fileExt = "";
|
|
|
|
if (this.zipWorker !== null) {
|
|
this.terminateZipWorker();
|
|
}
|
|
|
|
const downloadButton = document.getElementById("save-all-to-file");
|
|
|
|
downloadButton.classList.add("spin");
|
|
downloadButton.title = `Zipping ${inputNums.length} files...`;
|
|
downloadButton.setAttribute("data-original-title", `Zipping ${inputNums.length} files...`);
|
|
|
|
downloadButton.firstElementChild.innerHTML = "autorenew";
|
|
|
|
log.debug("Creating ZipWorker");
|
|
this.zipWorker = new ZipWorker();
|
|
this.zipWorker.postMessage({
|
|
outputs: this.outputs,
|
|
filename: fileName,
|
|
fileExtension: fileExt
|
|
});
|
|
this.zipWorker.addEventListener("message", this.handleZipWorkerMessage.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Terminate the ZipWorker
|
|
*/
|
|
terminateZipWorker() {
|
|
if (this.zipWorker === null) return; // Already terminated
|
|
|
|
log.debug("Terminating ZipWorker.");
|
|
|
|
this.zipWorker.terminate();
|
|
this.zipWorker = null;
|
|
|
|
const downloadButton = document.getElementById("save-all-to-file");
|
|
|
|
downloadButton.classList.remove("spin");
|
|
downloadButton.title = "Save all outputs to a zip file";
|
|
downloadButton.setAttribute("data-original-title", "Save all outputs to a zip file");
|
|
|
|
downloadButton.firstElementChild.innerHTML = "archive";
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle messages sent back by the ZipWorker
|
|
*/
|
|
handleZipWorkerMessage(e) {
|
|
const r = e.data;
|
|
if (!("zippedFile" in r)) {
|
|
log.error("No zipped file was sent in the message.");
|
|
this.terminateZipWorker();
|
|
return;
|
|
}
|
|
if (!("filename" in r)) {
|
|
log.error("No filename was sent in the message.");
|
|
this.terminateZipWorker();
|
|
return;
|
|
}
|
|
|
|
const file = new File([r.zippedFile], r.filename);
|
|
FileSaver.saveAs(file, r.filename, false);
|
|
|
|
this.terminateZipWorker();
|
|
}
|
|
|
|
/**
|
|
* Adds a new output tab.
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {boolean} [changeTab=true]
|
|
*/
|
|
addTab(inputNum, changeTab = true) {
|
|
const tabsWrapper = document.getElementById("output-tabs");
|
|
const numTabs = tabsWrapper.children.length;
|
|
|
|
if (!this.manager.tabs.getOutputTabItem(inputNum) && numTabs < this.maxTabs) {
|
|
// Create a new tab element
|
|
const newTab = this.manager.tabs.createOutputTabElement(inputNum, changeTab);
|
|
tabsWrapper.appendChild(newTab);
|
|
} else if (numTabs === this.maxTabs) {
|
|
// Can't create a new tab
|
|
document.getElementById("output-tabs").lastElementChild.classList.add("tabs-right");
|
|
}
|
|
|
|
this.displayTabInfo(inputNum);
|
|
|
|
if (changeTab) {
|
|
this.changeTab(inputNum, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Changes the active tab
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {boolean} [changeInput = false]
|
|
*/
|
|
changeTab(inputNum, changeInput = false) {
|
|
if (!this.outputExists(inputNum)) return;
|
|
const currentNum = this.manager.tabs.getActiveOutputTab();
|
|
|
|
this.hideMagicButton();
|
|
|
|
this.manager.highlighter.removeHighlights();
|
|
getSelection().removeAllRanges();
|
|
|
|
if (!this.manager.tabs.changeOutputTab(inputNum)) {
|
|
let direction = "right";
|
|
if (currentNum > inputNum) {
|
|
direction = "left";
|
|
}
|
|
const newOutputs = this.getNearbyNums(inputNum, direction);
|
|
|
|
const tabsLeft = (newOutputs[0] !== this.getSmallestInputNum());
|
|
const tabsRight = (newOutputs[newOutputs.length - 1] !== this.getLargestInputNum());
|
|
|
|
this.manager.tabs.refreshOutputTabs(newOutputs, inputNum, tabsLeft, tabsRight);
|
|
|
|
for (let i = 0; i < newOutputs.length; i++) {
|
|
this.displayTabInfo(newOutputs[i]);
|
|
}
|
|
}
|
|
|
|
this.app.debounce(this.set, 50, "setOutput", this, [inputNum])();
|
|
|
|
document.getElementById("output-html").scroll(0, 0);
|
|
document.getElementById("output-text").scroll(0, 0);
|
|
|
|
if (changeInput) {
|
|
this.manager.input.changeTab(inputNum, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for changing tabs event
|
|
*
|
|
* @param {event} mouseEvent
|
|
*/
|
|
changeTabClick(mouseEvent) {
|
|
if (!mouseEvent.target) return;
|
|
const tabNum = mouseEvent.target.parentElement.getAttribute("inputNum");
|
|
if (tabNum) {
|
|
this.changeTab(parseInt(tabNum, 10), this.app.options.syncTabs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for scrolling on the output tabs area
|
|
*
|
|
* @param {event} wheelEvent
|
|
*/
|
|
scrollTab(wheelEvent) {
|
|
wheelEvent.preventDefault();
|
|
|
|
if (wheelEvent.deltaY > 0) {
|
|
this.changeTabLeft();
|
|
} else if (wheelEvent.deltaY < 0) {
|
|
this.changeTabRight();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for mouse down on the next tab button
|
|
*/
|
|
nextTabClick() {
|
|
this.mousedown = true;
|
|
this.changeTabRight();
|
|
const time = 200;
|
|
const func = function(time) {
|
|
if (this.mousedown) {
|
|
this.changeTabRight();
|
|
const newTime = (time > 50) ? time = time - 10 : 50;
|
|
setTimeout(func.bind(this, [newTime]), newTime);
|
|
}
|
|
};
|
|
this.tabTimeout = setTimeout(func.bind(this, [time]), time);
|
|
}
|
|
|
|
/**
|
|
* Handler for mouse down on the previous tab button
|
|
*/
|
|
previousTabClick() {
|
|
this.mousedown = true;
|
|
this.changeTabLeft();
|
|
const time = 200;
|
|
const func = function(time) {
|
|
if (this.mousedown) {
|
|
this.changeTabLeft();
|
|
const newTime = (time > 50) ? time = time - 10 : 50;
|
|
setTimeout(func.bind(this, [newTime]), newTime);
|
|
}
|
|
};
|
|
this.tabTimeout = setTimeout(func.bind(this, [time]), time);
|
|
}
|
|
|
|
/**
|
|
* Handler for mouse up event on the tab buttons
|
|
*/
|
|
tabMouseUp() {
|
|
this.mousedown = false;
|
|
|
|
clearTimeout(this.tabTimeout);
|
|
this.tabTimeout = null;
|
|
}
|
|
|
|
/**
|
|
* Handler for changing to the left tab
|
|
*/
|
|
changeTabLeft() {
|
|
const currentTab = this.manager.tabs.getActiveOutputTab();
|
|
this.changeTab(this.getPreviousInputNum(currentTab), this.app.options.syncTabs);
|
|
}
|
|
|
|
/**
|
|
* Handler for changing to the right tab
|
|
*/
|
|
changeTabRight() {
|
|
const currentTab = this.manager.tabs.getActiveOutputTab();
|
|
this.changeTab(this.getNextInputNum(currentTab), this.app.options.syncTabs);
|
|
}
|
|
|
|
/**
|
|
* Handler for go to tab button clicked
|
|
*/
|
|
goToTab() {
|
|
const min = this.getSmallestInputNum(),
|
|
max = this.getLargestInputNum();
|
|
|
|
let tabNum = window.prompt(`Enter tab number (${min} - ${max}):`, this.manager.tabs.getActiveOutputTab().toString());
|
|
if (tabNum === null) return;
|
|
tabNum = parseInt(tabNum, 10);
|
|
|
|
if (this.outputExists(tabNum)) {
|
|
this.changeTab(tabNum, this.app.options.syncTabs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 && this.outputs[inputNum] !== undefined) {
|
|
newNum = inputNum;
|
|
} else {
|
|
switch (direction) {
|
|
case "left":
|
|
newNum = this.getNextInputNum(nums[i - 1]);
|
|
if (newNum === nums[i - 1]) {
|
|
direction = "right";
|
|
newNum = this.getPreviousInputNum(nums[0]);
|
|
}
|
|
break;
|
|
case "right":
|
|
newNum = this.getPreviousInputNum(nums[i - 1]);
|
|
if (newNum === nums[i - 1]) {
|
|
direction = "left";
|
|
newNum = this.getNextInputNum(nums[0]);
|
|
}
|
|
}
|
|
}
|
|
if (!nums.includes(newNum) && (newNum > 0)) {
|
|
nums.push(newNum);
|
|
}
|
|
}
|
|
nums.sort(function(a, b) {
|
|
return a - b;
|
|
});
|
|
return nums;
|
|
}
|
|
|
|
/**
|
|
* Gets the largest inputNum
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
getLargestInputNum() {
|
|
const inputNums = Object.keys(this.outputs);
|
|
if (inputNums.length === 0) return -1;
|
|
return Math.max(...inputNums);
|
|
}
|
|
|
|
/**
|
|
* Gets the smallest inputNum
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
getSmallestInputNum() {
|
|
const inputNums = Object.keys(this.outputs);
|
|
if (inputNums.length === 0) return -1;
|
|
return Math.min(...inputNums);
|
|
}
|
|
|
|
/**
|
|
* Gets the previous inputNum
|
|
*
|
|
* @param {number} inputNum - The current input number
|
|
* @returns {number}
|
|
*/
|
|
getPreviousInputNum(inputNum) {
|
|
const inputNums = Object.keys(this.outputs);
|
|
if (inputNums.length === 0) return -1;
|
|
let num = Math.min(...inputNums);
|
|
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}
|
|
*/
|
|
getNextInputNum(inputNum) {
|
|
const inputNums = Object.keys(this.outputs);
|
|
if (inputNums.length === 0) return -1;
|
|
let num = Math.max(...inputNums);
|
|
for (let i = 0; i < inputNums.length; i++) {
|
|
const iNum = parseInt(inputNums[i], 10);
|
|
if (iNum > inputNum) {
|
|
if (iNum < num) {
|
|
num = iNum;
|
|
}
|
|
}
|
|
}
|
|
return num;
|
|
}
|
|
|
|
/**
|
|
* Removes a tab and it's corresponding output
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
removeTab(inputNum) {
|
|
if (!this.outputExists(inputNum)) return;
|
|
|
|
const tabElement = this.manager.tabs.getOutputTabItem(inputNum);
|
|
|
|
this.removeOutput(inputNum);
|
|
|
|
if (tabElement !== null) {
|
|
this.refreshTabs(this.getPreviousInputNum(inputNum), "left");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Redraw the entire tab bar to remove any outdated tabs
|
|
* @param {number} activeTab
|
|
* @param {string} direction - Either "left" or "right"
|
|
*/
|
|
refreshTabs(activeTab, direction) {
|
|
const newNums = this.getNearbyNums(activeTab, direction),
|
|
tabsLeft = (newNums[0] !== this.getSmallestInputNum() && newNums.length > 0),
|
|
tabsRight = (newNums[newNums.length - 1] !== this.getLargestInputNum() && newNums.length > 0);
|
|
|
|
this.manager.tabs.refreshOutputTabs(newNums, activeTab, tabsLeft, tabsRight);
|
|
|
|
for (let i = 0; i < newNums.length; i++) {
|
|
this.displayTabInfo(newNums[i]);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Display output information in the tab header
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
async displayTabInfo(inputNum) {
|
|
if (!this.outputExists(inputNum)) return;
|
|
|
|
const dish = this.getOutputDish(inputNum);
|
|
let tabStr = "";
|
|
|
|
if (dish !== null) {
|
|
tabStr = await this.getDishTitle(this.getOutputDish(inputNum), 100);
|
|
tabStr = tabStr.replace(/[\n\r]/g, "");
|
|
}
|
|
this.manager.tabs.updateOutputTabHeader(inputNum, tabStr);
|
|
if (this.manager.worker.recipeConfig !== undefined) {
|
|
this.manager.tabs.updateOutputTabProgress(inputNum, this.outputs[inputNum].progress, this.manager.worker.recipeConfig.length);
|
|
}
|
|
|
|
const tabItem = this.manager.tabs.getOutputTabItem(inputNum);
|
|
if (tabItem) {
|
|
if (this.outputs[inputNum].status === "error") {
|
|
tabItem.style.color = "#FF0000";
|
|
} else {
|
|
tabItem.style.color = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 + "<br>length: " + lengthStr;
|
|
|
|
if (typeof lines === "number") {
|
|
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
|
|
msg += "<br>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.
|
|
*/
|
|
async backgroundMagic() {
|
|
this.hideMagicButton();
|
|
const dish = this.getOutputDish(this.manager.tabs.getActiveOutputTab());
|
|
if (!this.app.options.autoMagic || dish === null) return;
|
|
const buffer = await this.getDishBuffer(dish);
|
|
const sample = buffer.slice(0, 1000) || "";
|
|
|
|
if (sample.length || sample.byteLength) {
|
|
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", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.escapeHtml(Utils.truncate(result), 30)}"</span>`);
|
|
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.
|
|
*/
|
|
async displayFileSlice() {
|
|
document.querySelector("#output-loader .loading-msg").textContent = "Loading file slice...";
|
|
this.toggleLoader(true);
|
|
const outputText = document.getElementById("output-text"),
|
|
outputHtml = document.getElementById("output-html"),
|
|
outputFile = document.getElementById("output-file"),
|
|
outputHighlighter = document.getElementById("output-highlighter"),
|
|
inputHighlighter = document.getElementById("input-highlighter"),
|
|
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),
|
|
output = this.outputs[this.manager.tabs.getActiveOutputTab()].data;
|
|
|
|
let str;
|
|
if (output.type === "ArrayBuffer") {
|
|
str = Utils.arrayBufferToStr(output.result.slice(sliceFrom, sliceTo));
|
|
} else {
|
|
str = Utils.arrayBufferToStr(await this.getDishBuffer(output.dish).slice(sliceFrom, sliceTo));
|
|
}
|
|
|
|
outputText.classList.remove("blur");
|
|
showFileOverlay.style.display = "block";
|
|
outputText.value = Utils.printable(str, true);
|
|
|
|
|
|
outputText.style.display = "block";
|
|
outputHtml.style.display = "none";
|
|
outputFile.style.display = "none";
|
|
outputHighlighter.display = "block";
|
|
inputHighlighter.display = "block";
|
|
|
|
this.toggleLoader(false);
|
|
}
|
|
|
|
/**
|
|
* Handler for show file overlay events
|
|
*
|
|
* @param {Event} e
|
|
*/
|
|
showFileOverlayClick(e) {
|
|
const showFileOverlay = e.target;
|
|
|
|
document.getElementById("output-text").classList.add("blur");
|
|
showFileOverlay.style.display = "none";
|
|
this.set(this.manager.tabs.getActiveOutputTab());
|
|
}
|
|
|
|
/**
|
|
* Handler for extract file events.
|
|
*
|
|
* @param {Event} e
|
|
*/
|
|
async extractFileClick(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const el = e.target.nodeName === "I" ? e.target.parentNode : e.target;
|
|
const blobURL = el.getAttribute("blob-url");
|
|
const fileName = el.getAttribute("file-name");
|
|
|
|
const blob = await fetch(blobURL).then(r => r.blob());
|
|
this.manager.input.loadUIFiles([new File([blob], fileName, {type: blob.type})]);
|
|
}
|
|
|
|
|
|
/**
|
|
* Handler for copy click events.
|
|
* Copies the output to the clipboard
|
|
*/
|
|
async copyClick() {
|
|
const dish = this.getOutputDish(this.manager.tabs.getActiveOutputTab());
|
|
if (dish === null) {
|
|
this.app.alert("Could not find data to copy. Has this output been baked yet?", 3000);
|
|
return;
|
|
}
|
|
|
|
const output = await dish.get(Dish.STRING);
|
|
|
|
// 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);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the output contains carriage returns
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
async containsCR() {
|
|
const dish = this.getOutputDish(this.manager.tabs.getActiveOutputTab());
|
|
if (dish === null) return;
|
|
|
|
if (dish.type === Dish.STRING) {
|
|
const data = await dish.get(Dish.STRING);
|
|
return data.indexOf("\r") >= 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for switch click events.
|
|
* Moves the current output into the input textarea.
|
|
*/
|
|
async switchClick() {
|
|
const activeTab = this.manager.tabs.getActiveOutputTab();
|
|
const transferable = [];
|
|
|
|
const switchButton = document.getElementById("switch");
|
|
switchButton.classList.add("spin");
|
|
switchButton.disabled = true;
|
|
switchButton.firstElementChild.innerHTML = "autorenew";
|
|
$(switchButton).tooltip("hide");
|
|
|
|
let active = await this.getDishBuffer(this.getOutputDish(activeTab));
|
|
|
|
if (!this.outputExists(activeTab)) {
|
|
this.resetSwitchButton();
|
|
return;
|
|
}
|
|
|
|
if (this.outputs[activeTab].data.type === "string" &&
|
|
active.byteLength <= this.app.options.ioDisplayThreshold * 1024) {
|
|
const dishString = await this.getDishStr(this.getOutputDish(activeTab));
|
|
if (!await this.manager.input.preserveCarriageReturns(dishString)) {
|
|
active = dishString;
|
|
}
|
|
} else {
|
|
transferable.push(active);
|
|
}
|
|
|
|
this.manager.input.inputWorker.postMessage({
|
|
action: "inputSwitch",
|
|
data: {
|
|
inputNum: activeTab,
|
|
outputData: active
|
|
}
|
|
}, transferable);
|
|
}
|
|
|
|
/**
|
|
* Handler for when the inputWorker has switched the inputs.
|
|
* Stores the old input
|
|
*
|
|
* @param {object} switchData
|
|
* @param {number} switchData.inputNum
|
|
* @param {string | object} switchData.data
|
|
* @param {ArrayBuffer} switchData.data.fileBuffer
|
|
* @param {number} switchData.data.size
|
|
* @param {string} switchData.data.type
|
|
* @param {string} switchData.data.name
|
|
*/
|
|
inputSwitch(switchData) {
|
|
this.switchOrigData = switchData;
|
|
document.getElementById("undo-switch").disabled = false;
|
|
|
|
this.resetSwitchButton();
|
|
|
|
}
|
|
|
|
/**
|
|
* Handler for undo switch click events.
|
|
* Removes the output from the input and replaces the input that was removed.
|
|
*/
|
|
undoSwitchClick() {
|
|
this.manager.input.updateInputObj(this.switchOrigData.inputNum, this.switchOrigData.data);
|
|
|
|
this.manager.input.fileLoaded(this.switchOrigData.inputNum);
|
|
|
|
this.resetSwitch();
|
|
}
|
|
|
|
/**
|
|
* Removes the switch data and resets the switch buttons
|
|
*/
|
|
resetSwitch() {
|
|
if (this.switchOrigData !== undefined) {
|
|
delete this.switchOrigData;
|
|
}
|
|
|
|
const undoSwitch = document.getElementById("undo-switch");
|
|
undoSwitch.disabled = true;
|
|
$(undoSwitch).tooltip("hide");
|
|
|
|
this.resetSwitchButton();
|
|
}
|
|
|
|
/**
|
|
* Resets the switch button to its usual state
|
|
*/
|
|
resetSwitchButton() {
|
|
const switchButton = document.getElementById("switch");
|
|
switchButton.classList.remove("spin");
|
|
switchButton.disabled = false;
|
|
switchButton.firstElementChild.innerHTML = "open_in_browser";
|
|
}
|
|
|
|
/**
|
|
* Handler for maximise output click events.
|
|
* Resizes the output frame to be as large as possible, or restores it to its original size.
|
|
*/
|
|
maximiseOutputClick(e) {
|
|
const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode;
|
|
|
|
if (el.getAttribute("data-original-title").indexOf("Maximise") === 0) {
|
|
this.app.initialiseSplitter(true);
|
|
this.app.columnSplitter.collapse(0);
|
|
this.app.columnSplitter.collapse(1);
|
|
this.app.ioSplitter.collapse(0);
|
|
|
|
$(el).attr("data-original-title", "Restore output pane");
|
|
el.querySelector("i").innerHTML = "fullscreen_exit";
|
|
} else {
|
|
$(el).attr("data-original-title", "Maximise output pane");
|
|
el.querySelector("i").innerHTML = "fullscreen";
|
|
this.app.initialiseSplitter(false);
|
|
this.app.resetLayout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for find tab button clicked
|
|
*/
|
|
findTab() {
|
|
this.filterTabSearch();
|
|
$("#output-tab-modal").modal();
|
|
}
|
|
|
|
/**
|
|
* Searches the outputs using the filter settings and displays the results
|
|
*/
|
|
async filterTabSearch() {
|
|
const showPending = document.getElementById("output-show-pending").checked,
|
|
showBaking = document.getElementById("output-show-baking").checked,
|
|
showBaked = document.getElementById("output-show-baked").checked,
|
|
showStale = document.getElementById("output-show-stale").checked,
|
|
showErrored = document.getElementById("output-show-errored").checked,
|
|
contentFilter = document.getElementById("output-content-filter").value,
|
|
resultsList = document.getElementById("output-search-results"),
|
|
numResults = parseInt(document.getElementById("output-num-results").value, 10),
|
|
inputNums = Object.keys(this.outputs),
|
|
results = [];
|
|
|
|
let contentFilterExp;
|
|
try {
|
|
contentFilterExp = new RegExp(contentFilter, "i");
|
|
} catch (error) {
|
|
this.app.handleError(error);
|
|
return;
|
|
}
|
|
|
|
// Search through the outputs for matching output results
|
|
for (let i = 0; i < inputNums.length; i++) {
|
|
const iNum = inputNums[i],
|
|
output = this.outputs[iNum];
|
|
|
|
if (output.status === "pending" && showPending ||
|
|
output.status === "baking" && showBaking ||
|
|
output.status === "error" && showErrored ||
|
|
output.status === "stale" && showStale ||
|
|
output.status === "inactive" && showStale) {
|
|
const outDisplay = {
|
|
"pending": "Not baked yet",
|
|
"baking": "Baking",
|
|
"error": output.error || "Errored",
|
|
"stale": "Stale (output is out of date)",
|
|
"inactive": "Not baked yet"
|
|
};
|
|
|
|
// If the output has a dish object, check it against the filter
|
|
if (Object.prototype.hasOwnProperty.call(output, "data") &&
|
|
output.data &&
|
|
Object.prototype.hasOwnProperty.call(output.data, "dish")) {
|
|
const data = await output.data.dish.get(Dish.STRING);
|
|
if (contentFilterExp.test(data)) {
|
|
results.push({
|
|
inputNum: iNum,
|
|
textDisplay: data.slice(0, 100)
|
|
});
|
|
}
|
|
} else {
|
|
results.push({
|
|
inputNum: iNum,
|
|
textDisplay: outDisplay[output.status]
|
|
});
|
|
}
|
|
} else if (output.status === "baked" && showBaked && output.progress === false) {
|
|
let data = await output.data.dish.get(Dish.STRING);
|
|
data = data.replace(/[\r\n]/g, "");
|
|
if (contentFilterExp.test(data)) {
|
|
results.push({
|
|
inputNum: iNum,
|
|
textDisplay: data.slice(0, 100)
|
|
});
|
|
}
|
|
} else if (output.progress !== false && showErrored) {
|
|
let data = await output.data.dish.get(Dish.STRING);
|
|
data = data.replace(/[\r\n]/g, "");
|
|
if (contentFilterExp.test(data)) {
|
|
results.push({
|
|
inputNum: iNum,
|
|
textDisplay: data.slice(0, 100)
|
|
});
|
|
}
|
|
}
|
|
|
|
if (results.length >= numResults) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (let i = resultsList.children.length - 1; i >= 0; i--) {
|
|
resultsList.children.item(i).remove();
|
|
}
|
|
|
|
for (let i = 0; i < results.length; i++) {
|
|
const newListItem = document.createElement("li");
|
|
newListItem.classList.add("output-filter-result");
|
|
newListItem.setAttribute("inputNum", results[i].inputNum);
|
|
newListItem.innerText = `${results[i].inputNum}: ${results[i].textDisplay}`;
|
|
|
|
resultsList.appendChild(newListItem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for clicking on a filter result.
|
|
* Changes to the clicked output
|
|
*
|
|
* @param {event} e
|
|
*/
|
|
filterItemClick(e) {
|
|
if (!e.target) return;
|
|
const inputNum = parseInt(e.target.getAttribute("inputNum"), 10);
|
|
if (inputNum <= 0) return;
|
|
|
|
$("#output-tab-modal").modal("hide");
|
|
this.changeTab(inputNum, this.app.options.syncTabs);
|
|
}
|
|
}
|
|
|
|
export default OutputWaiter;
|