diff --git a/static/crossroad.css b/static/crossroad.css index 5569ae5..89f0b37 100644 --- a/static/crossroad.css +++ b/static/crossroad.css @@ -61,6 +61,14 @@ abbr[title] { height: 100%; } +.flex-hcenter { + display: flex; + flex-direction: row; + justify-content: center; + width: 100%; + height: 100%; +} + .flex-row { display: flex; flex-direction: row; @@ -76,6 +84,13 @@ abbr[title] { flex-direction: column-reverse; } +.flex-vevenly { + display: flex; + flex-direction: column; + justify-content: space-evenly; + height: 100%; +} + .half-width { min-width: 50%; } diff --git a/static/crossroad.html b/static/crossroad.html index 4bff713..29e0141 100644 --- a/static/crossroad.html +++ b/static/crossroad.html @@ -29,7 +29,12 @@
- +
+
+ + +
+
diff --git a/static/crossroad.mts b/static/crossroad.mts index 4901180..beb6e88 100644 --- a/static/crossroad.mts +++ b/static/crossroad.mts @@ -13,6 +13,7 @@ const detailsDialog = findDialogOrFail(document.body, "#instanceDetails"); const addDialog = findDialogOrFail(document.body, "#addInstance"); const instanceSelectForm = findFormOrFail(document.body, "#instanceSelectForm"); const redirectButton = findButtonOrFail(document.body, "#redirect"); +const redirectAlwaysButton = findButtonOrFail(document.body, "#redirectAlways"); export const { showAddInstanceDialog, @@ -48,24 +49,73 @@ function createInstanceSelectOptions() { instanceSelectForm.appendChild(div); } const firstInput = instanceSelectForm.querySelector("input"); - if (firstInput) { - firstInput.checked = true; - redirectButton.disabled = false; - } else { - redirectButton.disabled = true; - } + if (firstInput) firstInput.checked = true; + setRedirectButtonState(firstInput !== null); } createInstanceSelectOptions(); storageManager.addSaveCallback(createInstanceSelectOptions); -function redirect() { - // 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 = findInputOrFail(instanceSelectForm, `input[name="${radioButtonName}"]:checked`).value; - const url = URL.parse(option)!; +function setRedirectButtonState(enabled: boolean) { + redirectButton.disabled = !enabled; + redirectAlwaysButton.disabled = !enabled; +} + +export function getTargetSoftwareOrGroup(): string { const currentURL = URL.parse(location.href)!; - url.pathname = currentURL.pathname.replace(/\/.*?\//, "/"); + 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="${radioButtonName}"]:checked`).value; + } catch { + return null; + } +} + +function autoRedirect() { + const targetSoftware = getTargetSoftwareOrGroup(); + const preferredFor = storageManager.storage.instances.find(instance => instance.preferredFor?.includes(targetSoftware)); + if (preferredFor) redirect(preferredFor.origin); +} + +autoRedirect(); + +function setAutoRedirect(option: string) { + const instance = storageManager.storage.instances.find(e => e.origin === option); + if (!instance) throw new Error("Invalid argument"); + instance.preferredFor ??= []; + instance.preferredFor.push(getTargetSoftwareOrGroup()); + storageManager.save(); +} + +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 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(); } -redirectButton.addEventListener("click", e => redirect()); +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()!); +}); diff --git a/static/storage_manager.mts b/static/storage_manager.mts index dc89b95..7ab3957 100644 --- a/static/storage_manager.mts +++ b/static/storage_manager.mts @@ -25,6 +25,11 @@ export type Instance = { * Make sure to sanitize this! Could lead to XSS */ iconURL?: string, + /** + * The list of software names and groups the user prefers to autoredirect to this instance + * @example ["sharkey", "misskey-compliant"] + */ + preferredFor?: string[], } type LocalStorage = {