import { AddInstanceFlow } from "./add_instance_flow.mjs"; import { findButtonOrFail, findDialogOrFail, findDivOrFail, findFormOrFail, findInputOrFail, findOlOrFail, findParagraphOrFail, findPreOrFail, findSpanOrFail } from "./dom.mjs"; import knownSoftware, { getName } from "./known_software.mjs"; import storageManager, { Instance } from "./storage_manager.mjs"; const RADIO_BUTTON_NAME = "instanceSelect"; let addInstanceFlow: AddInstanceFlow | undefined; const mainDialog = findDialogOrFail(document.body, "#mainDialog"); const startAddInstanceFlowButton = findButtonOrFail(document.body, "#startAddInstanceFlow"); const addDialog = findDialogOrFail(document.body, "#addInstance"); const spinnerDialog = findDialogOrFail(document.body, "#spinner"); const detailsDialog = findDialogOrFail(document.body, "#instanceDetails"); const instanceSelectForm = findFormOrFail(document.body, "#instanceSelectForm"); const preferredList = findOlOrFail(document.body, "#preferredList"); const forksDiv = findDivOrFail(document.body, "#forks"); const forkOfSpan = findSpanOrFail(document.body, "#forkOf"); const forksList = findOlOrFail(document.body, "#forksList"); const othersDiv = findDivOrFail(document.body, "#others"); const othersList = findOlOrFail(document.body, "#othersList"); const redirectButton = findButtonOrFail(document.body, "#redirect"); const redirectAlwaysButton = findButtonOrFail(document.body, "#redirectAlways"); const noInstanceParagraph = findParagraphOrFail(document.body, "#noInstance"); const pathText = findPreOrFail(document.body, "#path"); const aOrAnText = findSpanOrFail(document.body, "#aOrAn"); const destinationText = findSpanOrFail(document.body, "#destination"); // Don't bother initializing if we're performing autoredirect if (!autoRedirect()) { createInstanceSelectOptions(); storageManager.addSaveCallback(createInstanceSelectOptions); updateNoInstanceHint(); storageManager.addSaveCallback(updateNoInstanceHint); pathText.innerText = getTargetPath(); const targetID = getTargetSoftwareOrGroup(); const targetName = getName(knownSoftware, targetID) ?? targetID; aOrAnText.innerText = "aeiou".includes(targetName[0].toLowerCase()) ? "an" : "a"; destinationText.innerText = targetName; addInstanceFlow = new AddInstanceFlow(addDialog, spinnerDialog, detailsDialog); mainDialog.show(); }; startAddInstanceFlowButton.addEventListener("click", e => addInstanceFlow?.start(true)); redirectButton.addEventListener("click", e => { // Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default redirect(getSelectedOption()!); }); redirectAlwaysButton.addEventListener("click", e => { // Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default const option = getSelectedOption()!; setAutoRedirect(option); redirect(option); }); function updateNoInstanceHint() { noInstanceParagraph.hidden = storageManager.storage.instances.length > 0; } type PreferenceGroups = { preferred: Instance[], forks?: { list: Instance[], of: string, }, others: Instance[], }; function sortInstancesIntoPreferenceGroups(): PreferenceGroups { const targetID = getTargetSoftwareOrGroup(); const pGroups: PreferenceGroups = { preferred: [], others: [], }; // If targetID is a group if (knownSoftware.groups[targetID]) { for (const instance of storageManager.storage.instances) { const software = knownSoftware.software[instance.software]; // If the instance's software is in the target group if (software.groups.includes(targetID)) { pGroups.preferred.push(instance); } else { pGroups.others.push(instance); } } } else { const isFork = knownSoftware.software[targetID].forkOf !== undefined; const forkOf = knownSoftware.software[targetID].forkOf ?? targetID; const hasForks = isFork || Object.values(knownSoftware.software).some(s => s.forkOf === forkOf); if (hasForks) { pGroups.forks = { list: [], of: forkOf, }; } for (const instance of storageManager.storage.instances) { if (instance.software === targetID) { pGroups.preferred.push(instance); continue; } const software = knownSoftware.software[instance.software]; // Checking pGroups.forks is the TypeScript safe way of checking hasForks if (pGroups.forks && (instance.software === forkOf || software.forkOf === forkOf)) { pGroups.forks.list.push(instance); continue; } pGroups.others.push(instance); } } return pGroups; } function constructOptionFromInstance(instance: Instance): HTMLDivElement { const div = document.createElement("div"); div.setAttribute("x-option", instance.origin); const radio = document.createElement("input"); radio.id = instance.origin; radio.value = instance.origin; radio.type = "radio"; radio.name = RADIO_BUTTON_NAME; const label = document.createElement("label"); label.htmlFor = instance.origin; label.innerText = instance.name + " "; if (instance.iconURL) { const img = new Image(); img.src = instance.iconURL; img.alt = `${instance.name} icon`; img.className = "inlineIcon medium-height"; label.append(img, " "); } const small = document.createElement("small"); const softwareName = knownSoftware.software[instance.software].name; small.innerText = `(${softwareName})`; label.appendChild(small); div.appendChild(radio); div.appendChild(label); return div; } function createInstanceSelectOptions() { // Erase all child nodes preferredList.replaceChildren(); forksList.replaceChildren(); othersList.replaceChildren(); const { preferred, forks, others } = sortInstancesIntoPreferenceGroups(); preferredList.hidden = preferred.length === 0; for (const instance of preferred) { preferredList.appendChild(constructOptionFromInstance(instance)); } forksDiv.hidden = forks === undefined || forks?.list.length === 0; if (forks) { forkOfSpan.innerText = getName(knownSoftware, forks.of) ?? forks.of; for (const instance of forks.list) { forksList.appendChild(constructOptionFromInstance(instance)); } } othersDiv.hidden = others.length === 0; for (const instance of others) { othersList.appendChild(constructOptionFromInstance(instance)); } const firstInput = instanceSelectForm.querySelector("input"); if (firstInput) firstInput.checked = true; setRedirectButtonState(firstInput !== null); } function setRedirectButtonState(enabled: boolean) { redirectButton.disabled = !enabled; redirectAlwaysButton.disabled = !enabled; } function getTargetSoftwareOrGroup(): string { const currentURL = URL.parse(location.href)!; const target = currentURL.pathname.match(/\/+([^\/]*)\/?/)?.[1]; if (target == null) throw new Error("Crossroad was served on an invalid path (likely a backend routing mistake)"); const softwareName = Object.entries(knownSoftware.software).find(([name, software]) => software.aliases.includes(target))?.[0]; if (softwareName) return softwareName; const groupName = Object.entries(knownSoftware.groups).find(([name, group]) => group.aliases.includes(target))?.[0]; if (groupName) return groupName; throw new Error("Could not identify target software or group"); } function getTargetPath(): string { const currentURL = URL.parse(location.href)!; return currentURL.pathname.replace(/\/+[^\/]*\/?/, "/"); } function getSelectedOption(): string | null { try { return findInputOrFail(instanceSelectForm, `input[name="${RADIO_BUTTON_NAME}"]:checked`).value; } catch { return null; } } function autoRedirect(): boolean { const targetSoftware = getTargetSoftwareOrGroup(); const preferredFor = storageManager.storage.instances.find(instance => instance.preferredFor?.includes(targetSoftware)); if (preferredFor) { redirect(preferredFor.origin); return true; } return false; } function setAutoRedirect(option: string) { const instance = storageManager.storage.instances.find(e => e.origin === option); if (!instance) throw new Error("Invalid argument"); instance.preferredFor.push(getTargetSoftwareOrGroup()); storageManager.save(); } function redirect(to: string) { const url = URL.parse(to); if (url === null) throw new Error("Couldn't parse destination"); url.pathname = getTargetPath(); location.href = url.toString(); }