diff --git a/providers/prompt/dark-prompt.css b/providers/prompt/dark-prompt.css index 8bf39791..768b9e52 100644 --- a/providers/prompt/dark-prompt.css +++ b/providers/prompt/dark-prompt.css @@ -18,7 +18,8 @@ body { overflow: hidden; } -#data { +#data, +.keybindData { background: unset; color: whitesmoke; border: 1px solid rgb(54, 54, 54); @@ -41,12 +42,35 @@ body { } #ok, -#cancel { +#cancel, +.clearButton { background-color: rgb(0, 0, 0); color: whitesmoke; } +/* For Counter Prompt */ .minus, .plus { background: rgb(0, 0, 0); } + +/* For Select Prompt */ +option { + background-color: #07070C; +} + +/* For Keybind Prompt */ +.clearButton:focus { + outline: none; +} +.clearButton:hover { + background-color: rgb(5, 5, 5); +} +.keybindData:hover { + border: 1px solid rgb(56, 0, 0); +} + +.keybindData:focus { + outline: 3px solid #1E0919; + border: 1px solid rgb(56, 0, 0); +} diff --git a/providers/prompt/index.js b/providers/prompt/index.js index 95babc9c..a5c2230c 100644 --- a/providers/prompt/index.js +++ b/providers/prompt/index.js @@ -6,9 +6,10 @@ const url = require("url"); const path = require("path"); const DEFAULT_WIDTH = 370; +const DEFAULT_KEYBIND_WIDTH = 420; const DEFAULT_COUNTER_WIDTH = 300; -const DEFAULT_HEIGHT = 160; -const DEFAULT_COUNTER_HEIGHT = 150; +const DEFAULT_HEIGHT = 150; +const DEFAULT_KEYBIND_HEIGHT = options => (options.length * 40) + 100; function electronPrompt(options, parentWindow) { return new Promise((resolve, reject) => { @@ -18,8 +19,8 @@ function electronPrompt(options, parentWindow) { //custom options override default const options_ = Object.assign( { - width: options?.type === "counter" ? DEFAULT_COUNTER_WIDTH : DEFAULT_WIDTH, - height: options?.type === "counter" ? DEFAULT_COUNTER_HEIGHT : DEFAULT_HEIGHT, + width: options?.type === "counter" ? DEFAULT_COUNTER_WIDTH : options?.type === "keybind" ? DEFAULT_KEYBIND_WIDTH : DEFAULT_WIDTH, + height: options?.type === "keybind" && options?.keybindOptions ? DEFAULT_KEYBIND_HEIGHT(options.keybindOptions) : DEFAULT_HEIGHT, resizable: false, title: "Prompt", label: "Please input a value:", @@ -28,6 +29,7 @@ function electronPrompt(options, parentWindow) { value: null, type: "input", selectOptions: null, + keybindOptions: null, counterOptions: { minimum: null, maximum: null, multiFire: false }, icon: null, useHtmlLabel: false, @@ -41,22 +43,21 @@ function electronPrompt(options, parentWindow) { options || {} ); - options_.minWidth = options?.minWidth || options?.width || options_.width; - options_.minHeight = options?.minHeight || options?.height || options_.height; + if (options_.customStylesheet === "dark") { options_.customStylesheet = require("path").join(__dirname, "dark-prompt.css"); } - if (options_.type === "counter" && (options_.counterOptions !== null && typeof options_.selectOptions !== "object")) { - reject(new Error('"counterOptions" must be an object if specified')); - return; + for (let type of ["counter", "select", "keybind"]) { + if (options_.type === type && (!options_[`${type}Options`] || typeof options_[`${type}Options`] !== "object")) { + reject(new Error(`"${type}Options" must be an object if type = ${type}`)); + return; + } } - if (options_.type === "select" && (options_.selectOptions === null || typeof options_.selectOptions !== "object")) { - reject(new Error('"selectOptions" must be an object')); - return; - } + options_.minWidth = options?.minWidth || options?.width || options_.width; + options_.minHeight = options?.minHeight || options?.height || options_.height; let promptWindow = new BrowserWindow({ frame: options_.frame, @@ -104,6 +105,11 @@ function electronPrompt(options, parentWindow) { //get input from front const postDataListener = (event, value) => { + if (options_.type === "keybind" && value) { + for (let i=0; i < value.length ;i++) { + value[i] = JSON.parse(value[i]) + } + } resolve(value); event.returnValue = null; cleanup(); @@ -135,7 +141,7 @@ function electronPrompt(options, parentWindow) { //should never happen promptWindow.webContents.on("did-fail-load", ( - event, + _event, errorCode, errorDescription, validatedURL diff --git a/providers/prompt/page/counter.js b/providers/prompt/page/counter.js new file mode 100644 index 00000000..6f773d97 --- /dev/null +++ b/providers/prompt/page/counter.js @@ -0,0 +1,140 @@ +const { promptCreateInput } = require("./prompt"); + +module.exports = { promptCreateCounter , validateCounterInput } + +let options; + + +function promptCreateCounter(promptOptions, parentElement) { + options = promptOptions; + if (options.counterOptions?.multiFire) { + document.onmouseup = () => { + if (nextTimeoutID) { + clearTimeout(nextTimeoutID) + nextTimeoutID = null; + } + }; + } + + options.value = validateCounterInput(options.value); + + const dataElement = promptCreateInput(); + dataElement.onkeypress = function isNumberKey(e) { + if (Number.isNaN(parseInt(e.key)) && e.key !== "Backspace" && e.key !== "Delete") + return false; + return true; + } + + dataElement.style.width = "unset"; + dataElement.style["text-align"] = "center"; + + parentElement.append(createMinusButton(dataElement)); + parentElement.append(dataElement); + parentElement.append(createPlusButton(dataElement)); + + return dataElement; +} + +let nextTimeoutID = null; + +/** Function execute callback in 3 accelerated intervals based on timer. + * Terminated from document.onmouseup() that is registered from promptCreateCounter() + * @param {function} callback function to execute + * @param {object} timer { + * * time: First delay in miliseconds + * * scaleSpeed: Speed change per tick on first acceleration + * * limit: First Speed Limit, gets divided by 2 after $20 calls. $number change exponentially + * } + * @param {int} stepArgs argument for callback representing Initial steps per click, default to 1 + * steps starts to increase when speed is too fast to notice + * @param {int} counter used internally to decrease timer.limit + */ +function multiFire(callback, timer = { time: 300, scaleSpeed: 100, limit: 100 }, stepsArg = 1, counter = 0) { + callback(stepsArg); + + const nextTimeout = timer.time; + + if (counter > 20) { + counter = 0 - stepsArg; + if (timer.limit > 1) { + timer.limit /= 2; + } else { + stepsArg *= 2; + } + } + + if (timer.time !== timer.limit) { + timer.time = Math.max(timer.time - timer.scaleSpeed, timer.limit) + } + + nextTimeoutID = setTimeout( + multiFire, //callback + nextTimeout, //timer + //multiFire args: + callback, + timer, + stepsArg, + counter + 1 + ); +} + +function createMinusButton(dataElement) { + function doMinus(steps) { + dataElement.value = validateCounterInput(parseInt(dataElement.value) - steps); + } + + const minusBtn = document.createElement("span"); + minusBtn.textContent = "-"; + minusBtn.classList.add("minus"); + + if (options.counterOptions?.multiFire) { + minusBtn.onmousedown = () => { + multiFire(doMinus); + }; + } else { + minusBtn.onmousedown = () => { + doMinus(); + }; + } + + return minusBtn; +} + +function createPlusButton(dataElement) { + function doPlus(steps) { + dataElement.value = validateCounterInput(parseInt(dataElement.value) + steps); + } + + const plusBtn = document.createElement("span"); + plusBtn.textContent = "+"; + plusBtn.classList.add("plus"); + + if (options.counterOptions?.multiFire) { + plusBtn.onmousedown = () => { + multiFire(doPlus); + }; + } else { + plusBtn.onmousedown = () => { + doPlus(); + }; + } + + return plusBtn; +} + +//validate counter +function validateCounterInput(input) { + + const min = options.counterOptions?.minimum; + const max = options.counterOptions?.maximum; + //note that !min/max would proc if min/max are 0 + if (min !== null && min !== undefined && input < min) { + return min; + } + + if (max !== null && max !== undefined && input > max) { + return max; + } + + return input; +} diff --git a/providers/prompt/page/keybind.js b/providers/prompt/page/keybind.js new file mode 100644 index 00000000..2406f736 --- /dev/null +++ b/providers/prompt/page/keybind.js @@ -0,0 +1,305 @@ +/* HTML + +
+ + + +
+ +*/ +/* CSS + +div.keybind { + display: grid; + grid-template-columns: max-content max-content max-content; + grid-gap: 5px; +} + +div.keybind button { + width: auto; +} + +div.keybind label { + text-align: right; +} + +div.keybind label:after { + content: ":"; +} + +*/ +const { promptError } = require("./prompt") + +class KeybindGetter { + value = null; + modifiers = null; + key = ""; + label = null; + txt = null; + clearButton = null; + + constructor(options, parentElement) { + if (!options.label || !options.value) { + promptError("keybind option must contain label and value"); + return; + } + + this.value = options.value + this.modifiers = new Set(); + this.key = ""; + + this.label = document.createElement("label"); + this.label.classList.add("keybindLabel"); + + this.txt = document.createElement("input"); + this.txt.setAttribute('readonly', true); + this.txt.classList.add("keybindData"); + + this.clearButton = document.createElement("button"); + this.clearButton.classList.add("clearButton"); + this.clearButton.textContent = "Clear"; + this.clearButton.onclick = (e) => e.preventDefault(); + + parentElement.append(this.label, this.txt, this.clearButton); + + this.setup(options); + if (options.default) { + this.setDefault(options.default) + } + } + + focus() { + this.txt.focus(); + } + + output() { + const output = {value: this.value, accelerator: this.txt.value.replaceAll(" ", "")} + return JSON.stringify(output); + } + + updateText() { + let result = ""; + for (let modifier of this.modifiers) { + result += modifier + " + "; + } + this.txt.value = result + this.key; + } + + setDefault(defaultValue) { + const accelerator = parseAccelerator(defaultValue).split("+"); + for (let key of accelerator) { + if (isModifier(key)) + this.modifiers.add(key); + else + this.key = key; + } + this.updateText(); + } + clear() { + this.modifiers.clear(); + this.key = ""; + this.txt.value = ""; + } + + setup(options) { + this.txt.addEventListener("keydown", (event) => { + event.preventDefault(); + if (event.repeat) { + return + } + let key = event.code || event.key; + if (key in virtualKeyCodes) + key = virtualKeyCodes[event.code]; + else { + console.log('Error, key "' + event.code + '" was not found'); + return; + } + + if (isModifier(key)) { + if (this.modifiers.size < 3) + this.modifiers.add(key); + } else { // is key + this.key = key; + } + this.updateText(); + }); + + this.clearButton.addEventListener("click", () => { + this.clear() + }); + this.label.textContent = options.label + " "; + } +} + +class keybindContainer { + elements = []; + + constructor(options, parentElement) { + parentElement.classList.add("keybind"); + this.elements = options.map(option => new KeybindGetter(option, parentElement)); + document.querySelector("#buttons").style["padding-top"] = "20px"; + } + + focus() { + if (this.elements.length > 0) + this.elements[0].focus(); + } + + submit() { + return this.elements.map(element => element.output()); + } +} + +function parseAccelerator(a) { + let accelerator = a.toString(); + + if (process.platform !== 'darwin') { + accelerator = accelerator.replace(/(Cmd)|(Command)/gi, ''); + } else { + accelerator = accelerator.replace(/(Ctrl)|(Control)/gi, ''); + } + + accelerator = accelerator.replace(/(Or)/gi, ''); + + return accelerator; +} + +function isModifier(key) { + for (let modifier of ["Shift", "Control", "Ctrl", "Command", "Cmd", "Alt", "AltGr", "Super"]) { + if (key === modifier) + return true; + } + return false; +} + +const virtualKeyCodes = { + ShiftLeft: "Shift", + ShiftRight: "Shift", + ControlLeft: "Ctrl", + ControlRight: "Ctrl", + AltLeft: "Alt", + AltRight: "Alt", + MetaLeft: "Super", + MetaRight: "Super", + NumLock: "NumLock", + NumpadDivide: "NumDiv", + NumpadMultiply: "NumMult", + NumpadSubtract: "NumSub", + NumpadAdd: "NumAdd", + NumpadDecimal: "NumDec ", + Numpad0: "Num0", + Numpad1: "Num1", + Numpad2: "Num2", + Numpad3: "Num3", + Numpad4: "Num4", + Numpad5: "Num5", + Numpad6: "Num6", + Numpad7: "Num7", + Numpad8: "Num8", + Numpad9: "Num9", + Digit0: "0", + Digit1: "1", + Digit2: "2", + Digit3: "3", + Digit4: "4", + Digit5: "5", + Digit6: "6", + Digit7: "7", + Digit8: "8", + Digit9: "9", + Minus: "-", + Equal: "=", + KeyQ: "Q", + KeyW: "W", + KeyE: "E", + KeyR: "R", + KeyT: "T", + KeyY: "Y", + KeyU: "U", + KeyI: "I", + KeyO: "O", + KeyP: "P", + KeyA: "A", + KeyS: "S", + KeyD: "D", + KeyF: "F", + KeyG: "G", + KeyH: "H", + KeyJ: "J", + KeyK: "K", + KeyL: "L", + KeyZ: "Z", + KeyX: "X", + KeyC: "C", + KeyV: "V", + KeyB: "B", + KeyN: "N", + KeyM: "M", + BracketLeft: "[", + BracketRight: "]", + Semicolon: ";", + Quote: "'", + Backquote: '"', + Backslash: "\\", + Comma: ",", + Period: "'.'", + Slash: "/", + plus: '+', + Space: "Space", + Tab: "Tab", + Backspace: "Backspace", + Delete: "Delete", + Insert: "Insert", + Return: "Return", + Enter: "Enter", + ArrowUp: "Up", + ArrowDown: "Down", + ArrowLeft: "Left", + ArrowRight: "Right", + Home: "Home", + End: "End", + PageUp: "PageUp", + PageDown: "PageDown", + Escape: "Escape", + AudioVolumeUp: "VolumeUp", + AudioVolumeDown: "VolumeDown", + AudioVolumeMute: "VolumeMute", + MediaTrackNext: "MediaNextTrack", + MediaTrackPrevious: "MediaPreviousTrack", + MediaStop: "MediaStop", + MediaPlayPause: "MediaPlayPause", + ScrollLock: "ScrollLock", + PrintScreen: "PrintScreen", + F1: "F1", + F2: "F2", + F3: "F3", + F4: "F4", + F5: "F5", + F6: "F6", + F7: "F7", + F8: "F8", + F9: "F9", + F10: "F10", + F11: "F11", + F12: "F12", + F13: "F13", + F14: "F14", + F15: "F15", + F16: "F16", + F17: "F17", + F18: "F18", + F19: "F19", + F20: "F20", + F21: "F21", + F22: "F22", + F23: "F23", + F24: "F24", +}; + + + +module.exports = function promptCreateKeybind(options, parentElement) { + return new keybindContainer(options, parentElement); +} diff --git a/providers/prompt/page/prompt.css b/providers/prompt/page/prompt.css index 3224cf6a..6e52379a 100644 --- a/providers/prompt/page/prompt.css +++ b/providers/prompt/page/prompt.css @@ -79,7 +79,7 @@ select#data { color: black; } -/* Counter mode css */ +/* Counter mode */ span { cursor: pointer; } @@ -96,3 +96,25 @@ span { vertical-align: middle; text-align: center; } + +/** Keybind mode */ +div.keybind { + display: grid; + grid-template-columns: max-content max-content max-content; + row-gap: 20px; + column-gap: 10px; + margin: auto 0; + justify-content: center; + } + + div.keybind button { + width: auto; + } + + div.keybind label { + text-align: right; + } + + div.keybind label:after { + content: ":"; + } \ No newline at end of file diff --git a/providers/prompt/page/prompt.js b/providers/prompt/page/prompt.js index 7f609b52..e09816d3 100644 --- a/providers/prompt/page/prompt.js +++ b/providers/prompt/page/prompt.js @@ -1,16 +1,15 @@ const fs = require("fs"); const { ipcRenderer } = require("electron"); - let promptId = null; let promptOptions = null; +let dataElement = null; -function $(selector) { - return document.querySelector(selector); -} +function $(selector) { return document.querySelector(selector); } document.addEventListener("DOMContentLoaded", promptRegister); function promptRegister() { + //get custom session id promptId = document.location.hash.replace("#", ""); @@ -56,13 +55,12 @@ function promptRegister() { $("#form").addEventListener("submit", promptSubmit); $("#cancel").addEventListener("click", promptCancel); - //create input/select + //create input/select/counter/keybind const dataContainerElement = $("#data-container"); - let dataElement; switch (promptOptions.type) { case "counter": - dataElement = promptCreateCounter(); + dataElement = promptCreateCounter(dataContainerElement); break; case "input": dataElement = promptCreateInput(); @@ -70,22 +68,24 @@ function promptRegister() { case "select": dataElement = promptCreateSelect(); break; + case "keybind": + dataElement = require("./keybind")(promptOptions.keybindOptions, dataContainerElement); + break; default: return promptError(`Unhandled input type '${promptOptions.type}'`); } - if (promptOptions.type === "counter") { - dataContainerElement.append(createMinusButton(dataElement)); - dataContainerElement.append(dataElement); - dataContainerElement.append(createPlusButton(dataElement)); - } else { - dataContainerElement.append(dataElement); + if (promptOptions.type != "keybind") { + dataElement.setAttribute("id", "data"); + + if (promptOptions.type !== "counter") { + dataContainerElement.append(dataElement); + } } - dataElement.setAttribute("id", "data"); dataElement.focus(); - if (promptOptions.type === "input" || promptOptions.type === "counter") { + if (promptOptions.type !== "select" && promptOptions.type !== "keybind") { dataElement.select(); } @@ -100,10 +100,10 @@ function promptRegister() { } } -window.addEventListener("error", error => { +window.addEventListener("error", event => { if (promptId) { promptError("An error has occured on the prompt window: \n" + - JSON.stringify(error, ["message", "arguments", "type", "name"]) + `Message: ${event.message}\nURL: ${event.url}\nLine: ${event.lineNo}, Column: ${event.columnNo}\nStack: ${event.error.stack}` ); } }); @@ -111,7 +111,7 @@ window.addEventListener("error", error => { //send error to back function promptError(error) { if (error instanceof Error) { - error = error.message; + error = error.message + "\n" + error.stack; } ipcRenderer.sendSync("prompt-error:" + promptId, error); @@ -124,20 +124,18 @@ function promptCancel() { //transfer input data to back function promptSubmit() { - const dataElement = $("#data"); let data = null; switch (promptOptions.type) { case "input": + case "select": data = dataElement.value; break; case "counter": data = validateCounterInput(dataElement.value); break; - case "select": - data = promptOptions.selectMultiple ? - dataElement.querySelectorAll("option[selected]").map(o => o.getAttribute("value")) : - dataElement.value; + case "keybind": + data = dataElement.submit(); break; default: //will never happen return promptError(`Unhandled input type '${promptOptions.type}'`); @@ -152,9 +150,6 @@ function promptCreateInput() { dataElement.setAttribute("type", "text"); if (promptOptions.value) { - if (promptOptions.type === "counter") { - promptOptions.value = validateCounterInput(promptOptions.value); - } dataElement.value = promptOptions.value; } else { dataElement.value = ""; @@ -212,21 +207,50 @@ function promptCreateSelect() { return dataElement; } +function promptCreateCounter(parentElement) { + if (promptOptions.counterOptions?.multiFire) { + document.onmouseup = () => { + if (nextTimeoutID) { + clearTimeout(nextTimeoutID) + nextTimeoutID = null; + } + }; + } + + promptOptions.value = validateCounterInput(promptOptions.value); + + const dataElement = promptCreateInput(); + dataElement.onkeypress = function isNumberKey(e) { + if (Number.isNaN(parseInt(e.key)) && e.key !== "Backspace" && e.key !== "Delete") + return false; + return true; + } + + dataElement.style.width = "unset"; + dataElement.style["text-align"] = "center"; + + parentElement.append(createMinusButton(dataElement)); + parentElement.append(dataElement); + parentElement.append(createPlusButton(dataElement)); + + return dataElement; +} + let nextTimeoutID = null; -/* Function execute callback in 3 accelerated intervals based on timer. +/** Function execute callback in 3 accelerated intervals based on timer. * Terminated from document.onmouseup() that is registered from promptCreateCounter() - * @param {function} callback: function to execute - * @param {object} timer: { - * * time: First delay in miliseconds. - * * limit: First Speed Limit, gets divided by 2 after $20 calls. $number change exponentially + * @param {function} callback function to execute + * @param {object} timer { + * * time: First delay in miliseconds * * scaleSpeed: Speed change per tick on first acceleration - * } - * @param {int} stepArgs: argument for callback representing Initial steps per click, default to 1 + * * limit: First Speed Limit, gets divided by 2 after $20 calls. $number change exponentially + * } + * @param {int} stepArgs argument for callback representing Initial steps per click, default to 1 * steps starts to increase when speed is too fast to notice - * @param {int} counter: used internally to decrease timer.limit + * @param {int} counter used internally to decrease timer.limit */ -function multiFire(callback, timer = { time: 500, scaleSpeed: 140, limit: 100 }, stepsArg = 1, counter = 0) { +function multiFire(callback, timer = { time: 300, scaleSpeed: 100, limit: 100 }, stepsArg = 1, counter = 0) { callback(stepsArg); const nextTimeout = timer.time; @@ -241,9 +265,7 @@ function multiFire(callback, timer = { time: 500, scaleSpeed: 140, limit: 100 }, } if (timer.time !== timer.limit) { - timer.time = timer.time > timer.limit ? - timer.time - timer.scaleSpeed : - timer.limit; + timer.time = Math.max(timer.time - timer.scaleSpeed, timer.limit) } nextTimeoutID = setTimeout( @@ -301,26 +323,9 @@ function createPlusButton(dataElement) { return plusBtn; } -function promptCreateCounter() { - if (promptOptions.counterOptions?.multiFire) { - document.onmouseup = () => { - if (nextTimeoutID) { - clearTimeout(nextTimeoutID) - nextTimeoutID = null; - } - }; - } - - const dataElement = promptCreateInput(); - - dataElement.style.width = "unset"; - dataElement.style["text-align"] = "center"; - - return dataElement; -} - //validate counter function validateCounterInput(input) { + const min = promptOptions.counterOptions?.minimum; const max = promptOptions.counterOptions?.maximum; //note that !min/max would proc if min/max are 0 @@ -334,3 +339,6 @@ function validateCounterInput(input) { return input; } + +module.exports.promptError = promptError; +