let footer; // Footer element
let windowIndex = 0; // Generated window index
const windowsInfo = []; // selector (for sliding), open
// slideToggle control for displayed windows ensuring only one is shown at a time
// Faster sliding transition when closing other windows than opening window
function windowControl(index) {
const windowInfo = windowsInfo[index];
if(windowInfo.open === true) { // Window currently open, close it
windowInfo.open = !windowInfo.open; // open -> closed
windowInfo.selector.slideToggle(400);
} else { // Window currently closed
// Close other windows currently open (should beonly one)
for(const otherWindowInfo of windowsInfo) {
if(otherWindowInfo.open) {
otherWindowInfo.open = !otherWindowInfo.open; // open -> closed
otherWindowInfo.selector.slideToggle(200);
}
}
windowInfo.open = !windowInfo.open; // closed -> open
windowInfo.selector.slideToggle(400);
return;
}
}
// Generates red-orange window header with text
// @param {string} text
// @returns {DOMElement} the header element
function generateWindowHeader(text) {
// Generate header wrapper element
const header = document.createElement("div");
header.classList.add("akito-header");
// Generate div containing actual text
const headerText = document.createElement("p");
headerText.classList.add("akito-headerTitle");
header.appendChild(headerText);
headerText.innerText = text;
return header;
}
// Generates and adds footer icon and window (todo: add compatibility mode for Best Buy...)
// Window is added to slide controller, and global icon and window offset are incremented per initialization.
// @param {string} iconURL
// @param {string} title
// @param {number} width
// @param {number} height
// @param {boolean} compatibility
// @returns {DOMElement}
// @returns {DOMElement}
function generateWindow(iconURL, title, width, height, compatibility = false) {
// Check whether footer has even been initialized
if(footer === undefined) {
throw 'Footer has not been initialized yet!';
}
// Increment all flexbox orders in advance
const placeIndex = footer.children.length - 2;
for(const element of footer.children) {
if(element.tagName !== "a") { // script info or donation
element.style.order++;
}
}
// Initialize a element for window toggle and add to footer
const iconClick = document.createElement("a");
const index = windowIndex++; // Copy constant for onclick
iconClick.classList.add("akito-iconClick");
iconClick.classList.add(`akito-icon${placeIndex}`);
footer.insertBefore(iconClick, footer.children[footer.children.length - 2]); // doesn't matter because order
iconClick.href = "#";
iconClick.onclick = function() {
windowControl(index);
return false;
}; // Toggle window with given index
// Initialize icon for window toggle "button"
const iconImage = document.createElement("img");
iconImage.classList.add("akito-iconImage");
iconClick.appendChild(iconImage);
iconImage.src = iconURL;
// Initialize actual window with given width, height, and left offset
const thisWindow = document.createElement("div");
thisWindow.classList.add("akito-window");
const contentDiv = document.createElement("div"); // Initialize in advance for compatibility?
thisWindow.appendChild(contentDiv);
if(compatibility === true) {
contentDiv.classList.add("akito-windowContentCompat");
} else {
thisWindow.style.width = width;
thisWindow.style.height = height;
contentDiv.classList.add("akito-windowContent");
}
// Best Buy doesn't let me set the left property that's so stupid
windowsInfo[index] = {
open: false,
selector: $(thisWindow),
}
// Initialize window header with title
const header = generateWindowHeader(title);
header.classList.add("akito-black");
thisWindow.appendChild(header);
// Add to document body and retrieve selector when loaded
document.body.appendChild(thisWindow);
$(thisWindow).hide(); // Best Buy forcing me to initially hide?
if(compatibility === false) { new SimpleBar(contentDiv); }
return [ thisWindow, contentDiv ];
}
// Generates page footer for script user interface (script info and other elements)
// @param {string} scriptText
// @param {string} messageText
function generateInterface(scriptText, messageText) {
// Full-width footer containing script controls and output
footer = document.createElement("div");
footer.classList.add("akito-footer");
// Script name/version/author information
const scriptInfo = document.createElement("p");
scriptInfo.classList.add("akito-scriptInfo");
footer.appendChild(scriptInfo);
scriptInfo.style.order = 0;
scriptInfo.innerText = scriptText;
// Miscellaneous message info (donation, link, etc.)
const messageInfo = document.createElement("p");
messageInfo.classList.add("akito-messageInfo");
footer.appendChild(messageInfo);
messageInfo.style.order = 1;
messageInfo.innerHTML = messageText;
// Append elements and process selectors on document load
$(document).ready(function() {
document.body.appendChild(footer);
});
}
// Designates div for settings with onchange modification to passed settings
// number: current (number), valid (array of numbers)
// @param {DOMElement} settingsWindow
// @param {DOMElement} settingsDiv
// @param {Object} settings
function designateSettings(settingsWindow, settingsDiv, settings) {
// Generate wrapper table element
const settingsTable = document.createElement("table");
settingsDiv.appendChild(settingsTable);
settingsTable.classList.add("akito-table");
// Transform and sort settings by type and alphabetical order
const settingsArray = [];
for(const property in settings) {
settingsArray.push(settings[property]);
}
settingsArray.sort(function(value, value2) {
if(value.index < value2.index) {
return -1;
} else if(value.index === value2.index) { // Shouldn't occur
return 0;
} else { // if value.index > value2.index
return 1;
}
});
// Add each setting as new row within table and attach onchange
for(const setting of settingsArray) {
// Generate row for specific setting
const row = document.createElement("tr");
row.classList.add("akito-settingsRow");
settingsTable.appendChild(row);
// Generate cell showing setting description (onhover?)
const descriptionCell = document.createElement("td");
descriptionCell.classList.add("akito-black");
row.appendChild(descriptionCell);
descriptionCell.innerHTML = `${setting.description}`;
// Generate cell with actual switcher (checkbox, slider, etc.)
// Currently no support for arrays because they're complicated
const settingCell = document.createElement("td");
settingCell.classList.add("akito-black");
row.appendChild(settingCell);
settingCell.style.align = "center";
switch(setting.type) {
case "boolean": // Checkbox
const checkbox = document.createElement("input");
checkbox.classList.add("akito-input");
checkbox.classList.add("akito-black");
settingCell.appendChild(checkbox);
checkbox.setAttribute("type", "checkbox");
checkbox.checked = setting.value;
checkbox.onclick = function() {
setting.value = checkbox.checked;
};
break;
case "number": // Numerical text input
const numberInput = document.createElement("input");
numberInput.classList.add("akito-input");
numberInput.classList.add("akito-black");
settingCell.appendChild(numberInput);
numberInput.setAttribute("type", "number");
numberInput.value = setting.value;
$(numberInput).change(function() {
setting.value = numberInput.value;
});
break;
case "string": // String text input
const stringInput = document.createElement("input");
stringInput.classList.add("akito-input");
stringInput.classList.add("akito-black");
settingCell.appendChild(stringInput);
stringInput.value = setting.value;
$(stringInput).change(function() {
setting.value = stringInput.value;
});
break;
case "button": // Button to click
const button = document.createElement("button");
button.classList.add("akito-input");
button.classList.add("akito-settingsButton");
settingCell.appendChild(button);
button.innerText = "Click";
button.onclick = setting.value;
break;
case "array": // Currently just string input
const arrayInput = document.createElement("input");
arrayInput.classList.add("akito-input");
arrayInput.classList.add("akito-black");
settingCell.appendChild(arrayInput);
arrayInput.value = setting.value.join(",");
$(arrayInput).change(function() {
setting.value = arrayInput.value.toString().split(",");
});
break;
}
}
}
// Designates div for logging and generates table appending function
// @param {DOMElement} contentDiv
// @returns {function}
async function designateLogging(loggingWindow, contentDiv) {
// Initialize wrapper table element
const loggingTable = document.createElement("table");
contentDiv.appendChild(loggingTable);
loggingTable.classList.add("akito-table");
// Initialize wrapper copy button
const copyDiv = document.createElement("div");
copyDiv.classList.add("akito-copyDiv");
loggingWindow.appendChild(copyDiv);
const copyClick = document.createElement("a");
copyClick.classList.add("akito-copyClick");
copyClick.href = "#";
copyDiv.appendChild(copyClick);
const copyImage = document.createElement("img");
copyImage.classList.add("akito-copyImage");
copyClick.appendChild(copyImage);
copyImage.src = "https://image.flaticon.com/icons/png/512/88/88026.png";
// Initialize wrapper trash button
const trashDiv = document.createElement("div");
trashDiv.classList.add("akito-trashDiv");
loggingWindow.appendChild(trashDiv);
const trashClick = document.createElement("a");
trashClick.classList.add("akito-copyClick");
trashClick.href = "#";
trashDiv.appendChild(trashClick);
const trashImage = document.createElement("img");
trashImage.classList.add("akito-copyImage");
trashClick.appendChild(trashImage);
trashImage.src = "https://cdn2.iconfinder.com/data/icons/cleaning-19/30/30x30-10-512.png";
// Retrieve logging data from Tampermonkey storage
let logsData = await GM_getValue(`${scriptPrefix}_cachedLogs`, []);
logsData = logsData.slice(0, 250); // Limit number of displayed logs to 250?
// Generates timestamp and appends to logging table
// @param {string} message
const loggingFunction = async function(message, time = (new Date()), fromCached = false) {
const row = document.createElement("tr");
message = message.replace("", ``);
message = message.replace("", ``); // Add manual bolding support
// Generate timestamp cell
const timestamp = "[" + (time).toTimeString().split(' ')[0] + "]";
const loggingCell = document.createElement("td");
loggingCell.classList.add("akito-black");
row.appendChild(loggingCell);
loggingCell.innerHTML = `${timestamp} ${message}`;
loggingTable.insertBefore(row, loggingTable.firstChild);
// Update cached logs if fresh
if(fromCached === false) {
logsData.push({ timestamp: time, message: message });
await GM_setValue(`${scriptPrefix}_cachedLogs`, logsData);
}
}
// Display previously cached logs via logging function
for(const logData of logsData) {
loggingFunction(logData.message, new Date(logData.timestamp), true);
}
// Delimit previous logs, don't show POST data
loggingFunction(`== ${location.href.split("?")[0]} ==`);
// Setup copy function (logs as well)
copyClick.onclick = async function() {
let copyString = "";
for(const row of loggingTable.childNodes) {
copyString += row.innerText + "\n";
}
GM_setClipboard(copyString);
loggingFunction("Successfully copied logs to clipboard");
};
// Setup trash function (logs as well)
trashClick.onclick = async function() {
logsData = []; // Clear existing logs and update
await GM_setValue(`${scriptPrefix}_cachedLogs`, logsData);
loggingTable.innerHTML = ""; // Clear data from logging "console"
}
return loggingFunction;
}