diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 5179cad62..d7f4025e0 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -1120,7 +1120,7 @@ export function initRossMods() { const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', { customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }], onClose: (popup) => { - regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false; + regenerateWithCtrlEnter = Boolean(popup.inputResults.get('regenerateWithCtrlEnter') ?? false); }, }); if (!result) { diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 5c22da7a0..92d1cb239 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -1061,7 +1061,7 @@ async function getExtensionVersion(extensionName, abortSignal) { * @param {boolean} global Is the extension global? * @returns {Promise} */ -export async function installExtension(url, global) { +export async function installExtension(url, global, branch = '') { console.debug('Extension installation started', url); toastr.info(t`Please wait...`, t`Installing extension`); @@ -1072,6 +1072,7 @@ export async function installExtension(url, global) { body: JSON.stringify({ url, global, + branch, }), }); @@ -1406,9 +1407,17 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { await popup.complete(POPUP_RESULT.AFFIRMATIVE); }, }; + /** @type {import('./popup.js').CustomPopupInput} */ + const branchNameInput = { + id: 'extension_branch_name', + label: t`Branch name (optional)`, + type: 'text', + tooltip: 'e.g. main, master, dev', + }; const customButtons = isCurrentUserAdmin ? [installForAllButton] : []; - const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons }); + const customInputs = [branchNameInput]; + const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons, customInputs }); const input = await popup.show(); if (!input) { @@ -1417,7 +1426,8 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { } const url = String(input).trim(); - await installExtension(url, global); + const branchName = String(popup.inputResults.get('extension_branch_name') ?? '').trim(); + await installExtension(url, global, branchName); } export async function initExtensions() { diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js index 014db82cd..c3e14fda4 100644 --- a/public/scripts/extensions/assets/index.js +++ b/public/scripts/extensions/assets/index.js @@ -291,7 +291,7 @@ async function installAsset(url, assetType, filename) { try { if (category === 'extension') { console.debug(DEBUG_PREFIX, 'Installing extension ', url); - await installExtension(url); + await installExtension(url, false); console.debug(DEBUG_PREFIX, 'Extension installed.'); return; } @@ -309,7 +309,7 @@ async function installAsset(url, assetType, filename) { console.debug(DEBUG_PREFIX, 'Importing character ', filename); const blob = await result.blob(); const file = new File([blob], filename, { type: blob.type }); - await processDroppedFiles([file], true); + await processDroppedFiles([file]); console.debug(DEBUG_PREFIX, 'Character downloaded.'); } } diff --git a/public/scripts/popup.js b/public/scripts/popup.js index aaadce1b1..a41626740 100644 --- a/public/scripts/popup.js +++ b/public/scripts/popup.js @@ -71,7 +71,8 @@ export const POPUP_RESULT = { * @property {string} id - The id for the html element * @property {string} label - The label text for the input * @property {string?} [tooltip=null] - Optional tooltip icon displayed behind the label - * @property {boolean?} [defaultState=false] - The default state when opening the popup (false if not set) + * @property {boolean|string|undefined} [defaultState=false] - The default state when opening the popup (false if not set) + * @property {string?} [type='checkbox'] - The type of the input (default is checkbox) */ /** @@ -157,7 +158,7 @@ export class Popup { /** @type {POPUP_RESULT|number} */ result; /** @type {any} */ value; - /** @type {Map?} */ inputResults; + /** @type {Map?} */ inputResults; /** @type {any} */ cropData; /** @type {HTMLElement} */ lastFocus; @@ -260,28 +261,52 @@ export class Popup { return; } - const label = document.createElement('label'); - label.classList.add('checkbox_label', 'justifyCenter'); - label.setAttribute('for', input.id); - const inputElement = document.createElement('input'); - inputElement.type = 'checkbox'; - inputElement.id = input.id; - inputElement.checked = input.defaultState ?? false; - label.appendChild(inputElement); - const labelText = document.createElement('span'); - labelText.innerText = input.label; - labelText.dataset.i18n = input.label; - label.appendChild(labelText); + if (!input.type || input.type === 'checkbox') { + const label = document.createElement('label'); + label.classList.add('checkbox_label', 'justifyCenter'); + label.setAttribute('for', input.id); + const inputElement = document.createElement('input'); + inputElement.type = 'checkbox'; + inputElement.id = input.id; + inputElement.checked = Boolean(input.defaultState ?? false); + label.appendChild(inputElement); + const labelText = document.createElement('span'); + labelText.innerText = input.label; + labelText.dataset.i18n = input.label; + label.appendChild(labelText); - if (input.tooltip) { - const tooltip = document.createElement('div'); - tooltip.classList.add('fa-solid', 'fa-circle-info', 'opacity50p'); - tooltip.title = input.tooltip; - tooltip.dataset.i18n = '[title]' + input.tooltip; - label.appendChild(tooltip); + if (input.tooltip) { + const tooltip = document.createElement('div'); + tooltip.classList.add('fa-solid', 'fa-circle-info', 'opacity50p'); + tooltip.title = input.tooltip; + tooltip.dataset.i18n = '[title]' + input.tooltip; + label.appendChild(tooltip); + } + + this.inputControls.appendChild(label); } - this.inputControls.appendChild(label); + if (input.type === 'text') { + const label = document.createElement('label'); + label.classList.add('text_label', 'justifyCenter'); + label.setAttribute('for', input.id); + + const inputElement = document.createElement('input'); + inputElement.classList.add('text_pole'); + inputElement.type = 'text'; + inputElement.id = input.id; + inputElement.value = String(input.defaultState ?? ''); + inputElement.placeholder = input.tooltip ?? ''; + + const labelText = document.createElement('span'); + labelText.innerText = input.label; + labelText.dataset.i18n = input.label; + + label.appendChild(labelText); + label.appendChild(inputElement); + + this.inputControls.appendChild(label); + } }); // Set the default button class @@ -529,7 +554,8 @@ export class Popup { this.inputResults = new Map(this.customInputs.map(input => { /** @type {HTMLInputElement} */ const inputControl = this.dlg.querySelector(`#${input.id}`); - return [inputControl.id, inputControl.checked]; + const value = input.type === 'text' ? inputControl.value : inputControl.checked; + return [inputControl.id, value]; })); } @@ -619,7 +645,7 @@ export class Popup { /** @readonly @type {Popup[]} Remember all popups */ popups: [], - /** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map?}?} Last popup result */ + /** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map?}?} Last popup result */ lastResult: null, /** @returns {boolean} Checks if any modal popup dialog is open */ diff --git a/src/endpoints/extensions.js b/src/endpoints/extensions.js index 2827d8058..cd8a31cd9 100644 --- a/src/endpoints/extensions.js +++ b/src/endpoints/extensions.js @@ -76,7 +76,7 @@ router.post('/install', async (request, response) => { fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); } - const { url, global } = request.body; + const { url, global, branch } = request.body; if (global && !request.user.profile.admin) { console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`); @@ -90,8 +90,12 @@ router.post('/install', async (request, response) => { return response.status(409).send(`Directory already exists at ${extensionPath}`); } - await git.clone(url, extensionPath, { '--depth': 1 }); - console.info(`Extension has been cloned at ${extensionPath}`); + const cloneOptions = { '--depth': 1 }; + if (branch) { + cloneOptions['--branch'] = branch; + } + await git.clone(url, extensionPath, cloneOptions); + console.info(`Extension has been cloned to ${extensionPath} from ${url} at ${branch || '(default)'} branch`); const { version, author, display_name } = await getManifest(extensionPath);