diff --git a/static/crossroad.html b/static/crossroad.html index d303d16..8ca3031 100644 --- a/static/crossroad.html +++ b/static/crossroad.html @@ -30,7 +30,17 @@ instance.

You currently don't have any instances. You should add one!

-
+
+ + + +

diff --git a/static/crossroad.mts b/static/crossroad.mts index 94ab9eb..a525f19 100644 --- a/static/crossroad.mts +++ b/static/crossroad.mts @@ -1,7 +1,7 @@ import { AddInstanceFlow } from "./add_instance_flow.mjs"; -import { findButtonOrFail, findDialogOrFail, findFormOrFail, findInputOrFail, findParagraphOrFail, findPreOrFail, findSpanOrFail } from "./dom.mjs"; +import { findButtonOrFail, findDialogOrFail, findDivOrFail, findFormOrFail, findInputOrFail, findOlOrFail, findParagraphOrFail, findPreOrFail, findSpanOrFail } from "./dom.mjs"; import knownSoftware, { getName } from "./known_software.mjs"; -import storageManager from "./storage_manager.mjs"; +import storageManager, { Instance } from "./storage_manager.mjs"; const RADIO_BUTTON_NAME = "instanceSelect"; @@ -13,6 +13,12 @@ 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"); @@ -58,34 +64,112 @@ function updateNoInstanceHint() { storageManager.storage.instances.length > 0; } -function createInstanceSelectOptions() { - instanceSelectForm.replaceChildren(); // Erase all child nodes - for (const instance of storageManager.storage.instances) { - 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, " "); +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); } - const small = document.createElement("small"); - const softwareName = knownSoftware.software[instance.software].name; - small.innerText = `(${softwareName})`; - label.appendChild(small); - div.appendChild(radio); - div.appendChild(label); - instanceSelectForm.appendChild(div); } + 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; + 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); diff --git a/static/dom.mts b/static/dom.mts index aec7536..a4bf983 100644 --- a/static/dom.mts +++ b/static/dom.mts @@ -1,6 +1,13 @@ // I would've LOVED to use generics for this but unfortunately that's not possible. // Type safety, but at what cost... >~< thanks TypeScript +export function findDivOrFail(on: Element, selector: string): HTMLDivElement { + const element = on.querySelector(selector); + if (!(element instanceof HTMLDivElement)) + throw new Error(`${selector} isn't a div`); + return element; +} + export function findSpanOrFail(on: Element, selector: string): HTMLSpanElement { const element = on.querySelector(selector); if (!(element instanceof HTMLSpanElement)) diff --git a/static/main.css b/static/main.css index 517e16f..bc6e75a 100644 --- a/static/main.css +++ b/static/main.css @@ -61,6 +61,10 @@ abbr[title] { text-align: start; } +.align-center { + text-align: center; +} + .inline-block { display: inline-block; } @@ -157,6 +161,10 @@ abbr[title] { margin-bottom: var(--large); } +.margin-none-bottom { + margin-bottom: 0; +} + .square { aspect-ratio: 1; }