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;
}