From 0f935f545338f0755070be48c344de388a1b0e81 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Tue, 14 Jan 2025 10:49:44 +0100 Subject: [PATCH 01/10] Add confirm instance details dialog --- static/add_an_instance.mts | 19 +++++--- static/confirm_instance_details.mts | 70 +++++++++++++++++++++++++++++ static/crossroad.css | 40 ++++++++++++++++- static/crossroad.html | 32 +++++++++++++ static/crossroad.mts | 41 +++++++++++++++-- 5 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 static/confirm_instance_details.mts diff --git a/static/add_an_instance.mts b/static/add_an_instance.mts index e305637..5503dc1 100644 --- a/static/add_an_instance.mts +++ b/static/add_an_instance.mts @@ -10,7 +10,14 @@ export function parseHost(host: string): { host: string, secure: boolean } | nul }; } -export function initializeAddInstanceDialog(dialog: HTMLDialogElement): { +export function initializeAddInstanceDialog( + dialog: HTMLDialogElement, + callback: ( + host: string, + secure: boolean, + autoQueryMetadata: boolean, + ) => void +): { showAddInstanceDialog: () => void, hideAddInstanceDialog: () => void, } { @@ -32,12 +39,14 @@ export function initializeAddInstanceDialog(dialog: HTMLDialogElement): { instanceHost.setCustomValidity(""); }); - form.addEventListener("submit", async e => { + const autoQueryMetadata = form.querySelector("#autoQueryMetadata"); + if (!(autoQueryMetadata instanceof HTMLInputElement)) + throw new Error("#autoQueryMetadata isn't an input"); + + form.addEventListener("submit", e => { // A sane browser doesn't allow for submitting the form if the above validation fails const { host, secure } = parseHost(instanceHost.value)!; - console.log( - await fetch(`/api/instance_info/${secure}/${encodeURI(host)}`).then(r => r.json()) - ); + callback(host, secure, autoQueryMetadata.checked); form.reset(); }); diff --git a/static/confirm_instance_details.mts b/static/confirm_instance_details.mts new file mode 100644 index 0000000..0a19609 --- /dev/null +++ b/static/confirm_instance_details.mts @@ -0,0 +1,70 @@ +// This file handles the "Confirm instance details" dialog + +import knownSoftware from "./known_software.mjs"; + +const blankImage = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + +export function initializeInstanceDetailsDialog(dialog: HTMLDialogElement): { + showInstanceDetailsDialog: () => void, + hideInstanceDetailsDialog: () => void, + populateInstanceDetailsDialog: ( + instanceNameValue: string, + instanceSoftwareValue: string, + instanceIconValue: string | null + ) => void, +} { + const showInstanceDetailsDialog = () => dialog.showModal(); + const hideInstanceDetailsDialog = () => dialog.close(); + + const form = dialog.querySelector(".instanceDetailsForm"); + if (!(form instanceof HTMLFormElement)) + throw new Error(".instanceDetailsForm isn't a form"); + + const instanceName = form.querySelector("#instanceName"); + if (!(instanceName instanceof HTMLInputElement)) + throw new Error("#instanceName isn't an input"); + + const instanceSoftware = form.querySelector("#instanceSoftware"); + if (!(instanceSoftware instanceof HTMLSelectElement)) + throw new Error("#instanceSoftware isn't a select"); + + for (const [name, software] of Object.entries(knownSoftware.software)) { + const option = new Option(software.name, name); + instanceSoftware.appendChild(option); + } + + const instanceIcon = form.querySelector("#instanceIcon"); + if (!(instanceIcon instanceof HTMLImageElement)) + throw new Error("#instanceIcon isn't an image"); + + instanceIcon.src = blankImage; + + const populateInstanceDetailsDialog = ( + instanceNameValue: string, + instanceSoftwareValue: string, + instanceIconValue: string | null + ) => { + instanceName.value = instanceNameValue; + instanceSoftware.value = instanceSoftwareValue; + instanceIcon.src = instanceIconValue ?? blankImage; + }; + + form.addEventListener("submit", e => { + form.reset(); + }); + + const closeButton = form.querySelector(".close"); + if (!(closeButton instanceof HTMLButtonElement)) + throw new Error(".close isn't a button"); + + closeButton.addEventListener("click", e => { + instanceIcon.src = blankImage; + hideInstanceDetailsDialog(); + }); + + return { + showInstanceDetailsDialog, + hideInstanceDetailsDialog, + populateInstanceDetailsDialog + }; +} diff --git a/static/crossroad.css b/static/crossroad.css index a390c14..92bcd3c 100644 --- a/static/crossroad.css +++ b/static/crossroad.css @@ -1,6 +1,7 @@ :root { --red: #cb0b0b; --blue: #2081c3; + --transparent-black: #0008; --large: 2em; --medium: 1em; } @@ -25,7 +26,7 @@ dialog { dialog::backdrop { backdrop-filter: blur(5px); - background-color: #0008; + background-color: var(--transparent-black); transition: background-color 0.125s ease-out; @starting-style { @@ -65,6 +66,16 @@ abbr[title] { flex-direction: row; } +.flex-row-reverse { + display: flex; + flex-direction: row-reverse; +} + +.flex-column-reverse { + display: flex; + flex-direction: column-reverse; +} + .half-width { min-width: 50%; } @@ -73,8 +84,12 @@ abbr[title] { min-height: 50%; } +.full-height { + min-height: 100%; +} + .separator-bottom { - border-bottom: solid 1px #0008; + border-bottom: solid 1px var(--transparent-black); } .margin-auto-top { @@ -83,4 +98,25 @@ abbr[title] { .margin-large-bottom { margin-bottom: var(--large); +} + +.square { + aspect-ratio: 1; +} + +.iconContainer { + width: 64px; + height: 64px; + padding: 1px; + border: solid 1px var(--transparent-black); +} + +.icon { + position: relative; + max-width: 64px; + max-height: 64px; + margin: auto; + top: 50%; + left: 50%; + translate: -50% -50%; } \ No newline at end of file diff --git a/static/crossroad.html b/static/crossroad.html index 405032e..7dd8312 100644 --- a/static/crossroad.html +++ b/static/crossroad.html @@ -58,6 +58,38 @@ We do not track or save any requests or data."> + +

Confirm instance details

+
+
+
+ +
+ +

+ +
+ +
+
+
+
+ +
+ + Icon for the selected instance +
+
+
+
+
+
+ + +
+
\ No newline at end of file diff --git a/static/crossroad.mts b/static/crossroad.mts index 00332d4..0585374 100644 --- a/static/crossroad.mts +++ b/static/crossroad.mts @@ -1,4 +1,5 @@ import { initializeAddInstanceDialog } from "./add_an_instance.mjs"; +import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs"; import knownSoftware from "./known_software.mjs"; import storageManager from "./storage_manager.mjs"; console.log(knownSoftware); @@ -9,7 +10,41 @@ export function getMainDialog(): HTMLDialogElement { return document.getElementById('mainDialog') as HTMLDialogElement; } -const dialog = document.querySelector("#addInstance"); -if (!(dialog instanceof HTMLDialogElement)) +const detailsDialog = document.querySelector("#instanceDetails"); +if (!(detailsDialog instanceof HTMLDialogElement)) + throw new Error("Couldn't find instanceDetails dialog"); +export const { + showInstanceDetailsDialog, + hideInstanceDetailsDialog, + populateInstanceDetailsDialog +} = initializeInstanceDetailsDialog(detailsDialog); + +const addInstanceDialogCallback = async ( + host: string, + secure: boolean, + autoQueryMetadata: boolean, +) => { + if (!autoQueryMetadata) { + showInstanceDetailsDialog(); + return; + } + const { name, software, iconURL } = + await fetch(`/api/instance_info/${secure}/${encodeURI(host)}`) + .then(r => r.json()); + if ( + typeof name !== "string" + || typeof software !== "string" + || !(typeof iconURL === "string" || iconURL === null) + ) + throw new Error("Invalid API response"); + populateInstanceDetailsDialog(name, software, iconURL as string | null); + showInstanceDetailsDialog(); +} + +const addDialog = document.querySelector("#addInstance"); +if (!(addDialog instanceof HTMLDialogElement)) throw new Error("Couldn't find addInstance dialog"); -export const { showAddInstanceDialog, hideAddInstanceDialog } = initializeAddInstanceDialog(dialog); +export const { + showAddInstanceDialog, + hideAddInstanceDialog +} = initializeAddInstanceDialog(addDialog, addInstanceDialogCallback); From e68a498cb3b98d4b76024ddaa18085d17da79bef Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Tue, 14 Jan 2025 13:10:29 +0100 Subject: [PATCH 02/10] Instance host & secure fields --- static/add_an_instance.mts | 1 + static/confirm_instance_details.mts | 15 +++++++++++++++ static/crossroad.html | 14 +++++++++++++- static/crossroad.mts | 27 ++++++++++++++------------- static/image.mts | 15 +++++++++++++++ 5 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 static/image.mts diff --git a/static/add_an_instance.mts b/static/add_an_instance.mts index 5503dc1..44af221 100644 --- a/static/add_an_instance.mts +++ b/static/add_an_instance.mts @@ -4,6 +4,7 @@ export function parseHost(host: string): { host: string, secure: boolean } | nul let parsedInstance = URL.parse(host); parsedInstance ??= URL.parse("https://" + host); if (!parsedInstance?.host) return null; + if (!/https?:/.test(parsedInstance.protocol)) return null; return { host: parsedInstance.host, secure: parsedInstance.protocol === "https:" diff --git a/static/confirm_instance_details.mts b/static/confirm_instance_details.mts index 0a19609..889f891 100644 --- a/static/confirm_instance_details.mts +++ b/static/confirm_instance_details.mts @@ -9,6 +9,8 @@ export function initializeInstanceDetailsDialog(dialog: HTMLDialogElement): { hideInstanceDetailsDialog: () => void, populateInstanceDetailsDialog: ( instanceNameValue: string, + instanceHostValue: string, + instanceHostSecureValue: boolean, instanceSoftwareValue: string, instanceIconValue: string | null ) => void, @@ -24,6 +26,14 @@ export function initializeInstanceDetailsDialog(dialog: HTMLDialogElement): { if (!(instanceName instanceof HTMLInputElement)) throw new Error("#instanceName isn't an input"); + const instanceHost = form.querySelector("#instanceHost"); + if (!(instanceHost instanceof HTMLInputElement)) + throw new Error("#instanceHost isn't an input"); + + const instanceHostSecure = form.querySelector("#instanceHostSecure"); + if (!(instanceHostSecure instanceof HTMLInputElement)) + throw new Error("#instanceHostSecure isn't an input"); + const instanceSoftware = form.querySelector("#instanceSoftware"); if (!(instanceSoftware instanceof HTMLSelectElement)) throw new Error("#instanceSoftware isn't a select"); @@ -41,12 +51,17 @@ export function initializeInstanceDetailsDialog(dialog: HTMLDialogElement): { const populateInstanceDetailsDialog = ( instanceNameValue: string, + instanceHostValue: string, + instanceHostSecureValue: boolean, instanceSoftwareValue: string, instanceIconValue: string | null ) => { instanceName.value = instanceNameValue; + instanceHost.value = instanceHostValue; + instanceHostSecure.checked = instanceHostSecureValue; instanceSoftware.value = instanceSoftwareValue; instanceIcon.src = instanceIconValue ?? blankImage; + }; form.addEventListener("submit", e => { diff --git a/static/crossroad.html b/static/crossroad.html index 7dd8312..e6a32d6 100644 --- a/static/crossroad.html +++ b/static/crossroad.html @@ -65,7 +65,19 @@ We do not track or save any requests or data.">

- + +

+ +
+ +
+ +


diff --git a/static/crossroad.mts b/static/crossroad.mts index 0585374..7f3dffb 100644 --- a/static/crossroad.mts +++ b/static/crossroad.mts @@ -24,21 +24,22 @@ const addInstanceDialogCallback = async ( secure: boolean, autoQueryMetadata: boolean, ) => { - if (!autoQueryMetadata) { + try { + if (!autoQueryMetadata) throw new Error("Don't"); + const { name, software, iconURL } = + await fetch(`/api/instance_info/${secure}/${encodeURI(host)}`) + .then(r => r.json()); + if ( + typeof name !== "string" + || typeof software !== "string" + || !(typeof iconURL === "string" || iconURL === null) + ) + throw new Error("Invalid API response"); + populateInstanceDetailsDialog(name, host, secure, software, iconURL as string | null); + } finally { + populateInstanceDetailsDialog("", host, secure, "", null); showInstanceDetailsDialog(); - return; } - const { name, software, iconURL } = - await fetch(`/api/instance_info/${secure}/${encodeURI(host)}`) - .then(r => r.json()); - if ( - typeof name !== "string" - || typeof software !== "string" - || !(typeof iconURL === "string" || iconURL === null) - ) - throw new Error("Invalid API response"); - populateInstanceDetailsDialog(name, software, iconURL as string | null); - showInstanceDetailsDialog(); } const addDialog = document.querySelector("#addInstance"); diff --git a/static/image.mts b/static/image.mts new file mode 100644 index 0000000..e85e407 --- /dev/null +++ b/static/image.mts @@ -0,0 +1,15 @@ +export function resize(image: HTMLImageElement, width: number = 16, height: number = 16): string { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.style.display = "none"; + document.body.appendChild(canvas); + const ctx = canvas.getContext("2d"); + if (ctx === null) throw Error("Resize failed"); + const w = Math.min(image.width / image.height, 1) * width; + const h = Math.min(image.height / image.width, 1) * height; + ctx.drawImage(image, (width - w) / 2, (height - h) / 2, w, h); + const url = canvas.toDataURL(); + document.body.removeChild(canvas); + return url; +} \ No newline at end of file From 4e9f97abfb3285e691d6bafa615bad0bf95cfc36 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Tue, 14 Jan 2025 13:24:45 +0100 Subject: [PATCH 03/10] Should work apart from CORS issues --- static/confirm_instance_details.mts | 22 +++++++++++++++++++--- static/crossroad.mts | 20 ++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/static/confirm_instance_details.mts b/static/confirm_instance_details.mts index 889f891..4a4c808 100644 --- a/static/confirm_instance_details.mts +++ b/static/confirm_instance_details.mts @@ -1,10 +1,20 @@ // This file handles the "Confirm instance details" dialog +import { resize } from "./image.mjs"; import knownSoftware from "./known_software.mjs"; const blankImage = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; -export function initializeInstanceDetailsDialog(dialog: HTMLDialogElement): { +export function initializeInstanceDetailsDialog( + dialog: HTMLDialogElement, + callback: ( + instanceName: string, + instanceHost: string, + instanceHostSecure: boolean, + instanceSoftware: string, + instanceIcon: string | null + ) => void +): { showInstanceDetailsDialog: () => void, hideInstanceDetailsDialog: () => void, populateInstanceDetailsDialog: ( @@ -13,7 +23,7 @@ export function initializeInstanceDetailsDialog(dialog: HTMLDialogElement): { instanceHostSecureValue: boolean, instanceSoftwareValue: string, instanceIconValue: string | null - ) => void, + ) => void } { const showInstanceDetailsDialog = () => dialog.showModal(); const hideInstanceDetailsDialog = () => dialog.close(); @@ -61,10 +71,16 @@ export function initializeInstanceDetailsDialog(dialog: HTMLDialogElement): { instanceHostSecure.checked = instanceHostSecureValue; instanceSoftware.value = instanceSoftwareValue; instanceIcon.src = instanceIconValue ?? blankImage; - }; form.addEventListener("submit", e => { + callback( + instanceName.value, + instanceHost.value, + instanceHostSecure.checked, + instanceSoftware.value, + instanceIcon.src == blankImage ? null : resize(instanceIcon) + ); form.reset(); }); diff --git a/static/crossroad.mts b/static/crossroad.mts index 7f3dffb..03e5a8c 100644 --- a/static/crossroad.mts +++ b/static/crossroad.mts @@ -10,6 +10,21 @@ export function getMainDialog(): HTMLDialogElement { return document.getElementById('mainDialog') as HTMLDialogElement; } +const instanceDetailsDialogCallback = ( + name: string, + host: string, + hostSecure: boolean, + software: string, + icon: string | null +) => { + storageManager.storage.instances.push({ + name, + origin: `http${hostSecure ? "s" : ""}://${host}`, + software, + iconURL: icon ?? undefined + }); +}; + const detailsDialog = document.querySelector("#instanceDetails"); if (!(detailsDialog instanceof HTMLDialogElement)) throw new Error("Couldn't find instanceDetails dialog"); @@ -17,7 +32,7 @@ export const { showInstanceDetailsDialog, hideInstanceDetailsDialog, populateInstanceDetailsDialog -} = initializeInstanceDetailsDialog(detailsDialog); +} = initializeInstanceDetailsDialog(detailsDialog, instanceDetailsDialogCallback); const addInstanceDialogCallback = async ( host: string, @@ -36,8 +51,9 @@ const addInstanceDialogCallback = async ( ) throw new Error("Invalid API response"); populateInstanceDetailsDialog(name, host, secure, software, iconURL as string | null); - } finally { + } catch { populateInstanceDetailsDialog("", host, secure, "", null); + } finally { showInstanceDetailsDialog(); } } From 91e6150be4b9021f7fda9652f8d0a8a9a626ad9b Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Tue, 14 Jan 2025 14:52:31 +0100 Subject: [PATCH 04/10] A little spring cleaning --- static/add_an_instance.mts | 21 +++++----------- static/confirm_instance_details.mts | 35 +++++++-------------------- static/dom.mts | 37 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 42 deletions(-) create mode 100644 static/dom.mts diff --git a/static/add_an_instance.mts b/static/add_an_instance.mts index 44af221..c8130fc 100644 --- a/static/add_an_instance.mts +++ b/static/add_an_instance.mts @@ -1,5 +1,7 @@ // This file handles the "Add an instance" dialog +import { findButtonOrFail, findFormOrFail, findInputOrFail } from "./dom.mjs"; + export function parseHost(host: string): { host: string, secure: boolean } | null { let parsedInstance = URL.parse(host); parsedInstance ??= URL.parse("https://" + host); @@ -25,13 +27,10 @@ export function initializeAddInstanceDialog( const showAddInstanceDialog = () => dialog.showModal(); const hideAddInstanceDialog = () => dialog.close(); - const form = dialog.querySelector(".addInstanceForm"); - if (!(form instanceof HTMLFormElement)) - throw new Error(".addInstanceForm isn't a form"); - - const instanceHost = form.querySelector("#instanceHost"); - if (!(instanceHost instanceof HTMLInputElement)) - throw new Error("#instanceHost isn't an input"); + const form = findFormOrFail(dialog, ".addInstanceForm"); + const instanceHost = findInputOrFail(form, "#instanceHost"); + const autoQueryMetadata = findInputOrFail(form, "#autoQueryMetadata"); + const closeButton = findButtonOrFail(form, ".close"); instanceHost.addEventListener("input", e => { if (parseHost(instanceHost.value) === null) @@ -40,10 +39,6 @@ export function initializeAddInstanceDialog( instanceHost.setCustomValidity(""); }); - const autoQueryMetadata = form.querySelector("#autoQueryMetadata"); - if (!(autoQueryMetadata instanceof HTMLInputElement)) - throw new Error("#autoQueryMetadata isn't an input"); - form.addEventListener("submit", e => { // A sane browser doesn't allow for submitting the form if the above validation fails const { host, secure } = parseHost(instanceHost.value)!; @@ -51,10 +46,6 @@ export function initializeAddInstanceDialog( form.reset(); }); - const closeButton = form.querySelector(".close"); - if (!(closeButton instanceof HTMLButtonElement)) - throw new Error(".close isn't a button"); - closeButton.addEventListener("click", e => hideAddInstanceDialog()); return { diff --git a/static/confirm_instance_details.mts b/static/confirm_instance_details.mts index 4a4c808..f175a4a 100644 --- a/static/confirm_instance_details.mts +++ b/static/confirm_instance_details.mts @@ -1,5 +1,6 @@ // This file handles the "Confirm instance details" dialog +import { findButtonOrFail, findFormOrFail, findImageOrFail, findInputOrFail, findSelectOrFail } from "./dom.mjs"; import { resize } from "./image.mjs"; import knownSoftware from "./known_software.mjs"; @@ -28,35 +29,19 @@ export function initializeInstanceDetailsDialog( const showInstanceDetailsDialog = () => dialog.showModal(); const hideInstanceDetailsDialog = () => dialog.close(); - const form = dialog.querySelector(".instanceDetailsForm"); - if (!(form instanceof HTMLFormElement)) - throw new Error(".instanceDetailsForm isn't a form"); - - const instanceName = form.querySelector("#instanceName"); - if (!(instanceName instanceof HTMLInputElement)) - throw new Error("#instanceName isn't an input"); - - const instanceHost = form.querySelector("#instanceHost"); - if (!(instanceHost instanceof HTMLInputElement)) - throw new Error("#instanceHost isn't an input"); - - const instanceHostSecure = form.querySelector("#instanceHostSecure"); - if (!(instanceHostSecure instanceof HTMLInputElement)) - throw new Error("#instanceHostSecure isn't an input"); - - const instanceSoftware = form.querySelector("#instanceSoftware"); - if (!(instanceSoftware instanceof HTMLSelectElement)) - throw new Error("#instanceSoftware isn't a select"); + const form = findFormOrFail(dialog, ".instanceDetailsForm"); + const instanceName = findInputOrFail(form, "#instanceName"); + const instanceHost = findInputOrFail(form, "#instanceHost"); + const instanceHostSecure = findInputOrFail(form, "#instanceHostSecure"); + const instanceSoftware = findSelectOrFail(form, "#instanceSoftware"); + const instanceIcon = findImageOrFail(form, "#instanceIcon"); + const closeButton = findButtonOrFail(form, ".close"); for (const [name, software] of Object.entries(knownSoftware.software)) { const option = new Option(software.name, name); instanceSoftware.appendChild(option); } - const instanceIcon = form.querySelector("#instanceIcon"); - if (!(instanceIcon instanceof HTMLImageElement)) - throw new Error("#instanceIcon isn't an image"); - instanceIcon.src = blankImage; const populateInstanceDetailsDialog = ( @@ -84,10 +69,6 @@ export function initializeInstanceDetailsDialog( form.reset(); }); - const closeButton = form.querySelector(".close"); - if (!(closeButton instanceof HTMLButtonElement)) - throw new Error(".close isn't a button"); - closeButton.addEventListener("click", e => { instanceIcon.src = blankImage; hideInstanceDetailsDialog(); diff --git a/static/dom.mts b/static/dom.mts new file mode 100644 index 0000000..0d013fa --- /dev/null +++ b/static/dom.mts @@ -0,0 +1,37 @@ +// 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 findFormOrFail(on: Element, selector: string): HTMLFormElement { + const element = on.querySelector(selector); + if (!(element instanceof HTMLFormElement)) + throw new Error(`${selector} isn't a form`); + return element; +} + +export function findInputOrFail(on: Element, selector: string): HTMLInputElement { + const element = on.querySelector(selector); + if (!(element instanceof HTMLInputElement)) + throw new Error(`${selector} isn't an input`); + return element; +} + +export function findButtonOrFail(on: Element, selector: string): HTMLButtonElement { + const element = on.querySelector(selector); + if (!(element instanceof HTMLButtonElement)) + throw new Error(`${selector} isn't a button`); + return element; +} + +export function findSelectOrFail(on: Element, selector: string): HTMLSelectElement { + const element = on.querySelector(selector); + if (!(element instanceof HTMLSelectElement)) + throw new Error(`${selector} isn't a select`); + return element; +} + +export function findImageOrFail(on: Element, selector: string): HTMLImageElement { + const element = on.querySelector(selector); + if (!(element instanceof HTMLImageElement)) + throw new Error(`${selector} isn't an image`); + return element; +} \ No newline at end of file From 490c2a80807a5d6dc47427ff422091e1f475af03 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Tue, 14 Jan 2025 15:40:49 +0100 Subject: [PATCH 05/10] Proxying --- Cargo.lock | 28 +++++++++++++++++++++++++++ Cargo.toml | 3 ++- src/api/mod.rs | 3 ++- src/api/proxy.rs | 30 +++++++++++++++++++++++++++++ static/confirm_instance_details.mts | 2 +- 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 src/api/proxy.rs diff --git a/Cargo.lock b/Cargo.lock index 9369bf3..1b3bf92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,6 +279,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" name = "fedirect" version = "0.1.0" dependencies = [ + "bytes", "reqwest", "rocket", "semver", @@ -367,6 +368,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -388,6 +400,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1318,11 +1331,13 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-util", "tower", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "windows-registry", ] @@ -2159,6 +2174,19 @@ version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.76" diff --git a/Cargo.toml b/Cargo.toml index 9df5c41..df36732 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -reqwest = "0.12.12" +bytes = "1.9.0" +reqwest = { version = "0.12.12", features = ["stream"] } rocket = { version = "0.5.1", features = ["json"] } semver = "1.0.24" serde = { version = "1.0.217", features = ["derive"] } diff --git a/src/api/mod.rs b/src/api/mod.rs index 2dd826b..38de871 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,7 +1,8 @@ use rocket::Route; pub mod instance_info; +pub mod proxy; pub fn get_routes() -> Vec { - routes![instance_info::instance_info] + routes![instance_info::instance_info, proxy::proxy] } diff --git a/src/api/proxy.rs b/src/api/proxy.rs new file mode 100644 index 0000000..fa17a37 --- /dev/null +++ b/src/api/proxy.rs @@ -0,0 +1,30 @@ +use bytes::Bytes; +use rocket::response::stream::ByteStream; + +/// Copy-pasted and modified from https://github.com/rwf2/Rocket/issues/1521 +mod req { + use super::*; + use rocket::response::Debug; + + pub type Result> = std::result::Result; + + pub async fn get(url: &str) -> Result { + let bytes_stream = reqwest::get(url).await?.bytes_stream(); + Ok(ByteStream! { + for await bytes in bytes_stream { + match bytes { + Ok(bytes) => yield bytes, + Err(e) => { + eprintln!("error while streaming: {}", e); + break; + } + } + } + }) + } +} + +#[get("/proxy/")] +pub async fn proxy(url: &str) -> req::Result { + req::get(url).await +} diff --git a/static/confirm_instance_details.mts b/static/confirm_instance_details.mts index f175a4a..b4e9545 100644 --- a/static/confirm_instance_details.mts +++ b/static/confirm_instance_details.mts @@ -55,7 +55,7 @@ export function initializeInstanceDetailsDialog( instanceHost.value = instanceHostValue; instanceHostSecure.checked = instanceHostSecureValue; instanceSoftware.value = instanceSoftwareValue; - instanceIcon.src = instanceIconValue ?? blankImage; + instanceIcon.src = instanceIconValue === null ? blankImage : `/api/proxy/${encodeURIComponent(instanceIconValue)}`; }; form.addEventListener("submit", e => { From fc14eec7ebbbba1f1bce637993da097df36630e9 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Tue, 14 Jan 2025 15:41:31 +0100 Subject: [PATCH 06/10] The last few tweaks that were needed to make this work --- static/crossroad.mts | 11 +++++++---- static/storage_manager.mts | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/static/crossroad.mts b/static/crossroad.mts index 03e5a8c..852b3e0 100644 --- a/static/crossroad.mts +++ b/static/crossroad.mts @@ -1,7 +1,7 @@ import { initializeAddInstanceDialog } from "./add_an_instance.mjs"; import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs"; import knownSoftware from "./known_software.mjs"; -import storageManager from "./storage_manager.mjs"; +import storageManager, { Instance } from "./storage_manager.mjs"; console.log(knownSoftware); console.log(storageManager.storage.instances); @@ -17,12 +17,15 @@ const instanceDetailsDialogCallback = ( software: string, icon: string | null ) => { - storageManager.storage.instances.push({ + const instance: Instance = { name, origin: `http${hostSecure ? "s" : ""}://${host}`, software, iconURL: icon ?? undefined - }); + }; + storageManager.storage.instances.push(instance); + storageManager.save(); + console.log("Successfully added new instance:", instance); }; const detailsDialog = document.querySelector("#instanceDetails"); @@ -42,7 +45,7 @@ const addInstanceDialogCallback = async ( try { if (!autoQueryMetadata) throw new Error("Don't"); const { name, software, iconURL } = - await fetch(`/api/instance_info/${secure}/${encodeURI(host)}`) + await fetch(`/api/instance_info/${secure}/${encodeURIComponent(host)}`) .then(r => r.json()); if ( typeof name !== "string" diff --git a/static/storage_manager.mts b/static/storage_manager.mts index 0f22bbb..da88774 100644 --- a/static/storage_manager.mts +++ b/static/storage_manager.mts @@ -1,4 +1,4 @@ -type Instance = { +export type Instance = { /** * The instance's (nick)name * @example "eepy.moe" From 7430b19a7d2bb30cb730850095ff30a7b4b2a9fa Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Tue, 14 Jan 2025 15:58:30 +0100 Subject: [PATCH 07/10] Move Add Instance Flow to its own file --- static/add_instance_flow.mts | 69 ++++++++++++++++++++++++++++++++++++ static/crossroad.mts | 66 +++------------------------------- static/dom.mts | 7 ++++ 3 files changed, 81 insertions(+), 61 deletions(-) create mode 100644 static/add_instance_flow.mts diff --git a/static/add_instance_flow.mts b/static/add_instance_flow.mts new file mode 100644 index 0000000..14ac6f7 --- /dev/null +++ b/static/add_instance_flow.mts @@ -0,0 +1,69 @@ +import { initializeAddInstanceDialog } from "./add_an_instance.mjs"; +import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs"; +import storageManager, { Instance } from "./storage_manager.mjs"; + +export function initializeAddInstanceFlow( + detailsDialog: HTMLDialogElement, + addDialog: HTMLDialogElement +): { + showAddInstanceDialog: () => void, + hideAddInstanceDialog: () => void +} { + const instanceDetailsDialogCallback = ( + name: string, + host: string, + hostSecure: boolean, + software: string, + icon: string | null + ) => { + const instance: Instance = { + name, + origin: `http${hostSecure ? "s" : ""}://${host}`, + software, + iconURL: icon ?? undefined + }; + storageManager.storage.instances.push(instance); + storageManager.save(); + console.log("Successfully added new instance:", instance); + }; + + const { + showInstanceDetailsDialog, + hideInstanceDetailsDialog, + populateInstanceDetailsDialog + } = initializeInstanceDetailsDialog(detailsDialog, instanceDetailsDialogCallback); + + const addInstanceDialogCallback = async ( + host: string, + secure: boolean, + autoQueryMetadata: boolean, + ) => { + try { + if (!autoQueryMetadata) throw new Error("Don't"); + const { name, software, iconURL } = + await fetch(`/api/instance_info/${secure}/${encodeURIComponent(host)}`) + .then(r => r.json()); + if ( + typeof name !== "string" + || typeof software !== "string" + || !(typeof iconURL === "string" || iconURL === null) + ) + throw new Error("Invalid API response"); + populateInstanceDetailsDialog(name, host, secure, software, iconURL as string | null); + } catch { + populateInstanceDetailsDialog("", host, secure, "", null); + } finally { + showInstanceDetailsDialog(); + } + } + + const { + showAddInstanceDialog, + hideAddInstanceDialog + } = initializeAddInstanceDialog(addDialog, addInstanceDialogCallback); + + return { + showAddInstanceDialog, + hideAddInstanceDialog + }; +} diff --git a/static/crossroad.mts b/static/crossroad.mts index 852b3e0..8d212c4 100644 --- a/static/crossroad.mts +++ b/static/crossroad.mts @@ -1,70 +1,14 @@ -import { initializeAddInstanceDialog } from "./add_an_instance.mjs"; -import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs"; -import knownSoftware from "./known_software.mjs"; -import storageManager, { Instance } from "./storage_manager.mjs"; -console.log(knownSoftware); - -console.log(storageManager.storage.instances); +import { initializeAddInstanceFlow } from "./add_instance_flow.mjs"; +import { findDialogOrFail } from "./dom.mjs"; export function getMainDialog(): HTMLDialogElement { return document.getElementById('mainDialog') as HTMLDialogElement; } -const instanceDetailsDialogCallback = ( - name: string, - host: string, - hostSecure: boolean, - software: string, - icon: string | null -) => { - const instance: Instance = { - name, - origin: `http${hostSecure ? "s" : ""}://${host}`, - software, - iconURL: icon ?? undefined - }; - storageManager.storage.instances.push(instance); - storageManager.save(); - console.log("Successfully added new instance:", instance); -}; +const detailsDialog = findDialogOrFail(document.body, "#instanceDetails"); +const addDialog = findDialogOrFail(document.body, "#addInstance"); -const detailsDialog = document.querySelector("#instanceDetails"); -if (!(detailsDialog instanceof HTMLDialogElement)) - throw new Error("Couldn't find instanceDetails dialog"); -export const { - showInstanceDetailsDialog, - hideInstanceDetailsDialog, - populateInstanceDetailsDialog -} = initializeInstanceDetailsDialog(detailsDialog, instanceDetailsDialogCallback); - -const addInstanceDialogCallback = async ( - host: string, - secure: boolean, - autoQueryMetadata: boolean, -) => { - try { - if (!autoQueryMetadata) throw new Error("Don't"); - const { name, software, iconURL } = - await fetch(`/api/instance_info/${secure}/${encodeURIComponent(host)}`) - .then(r => r.json()); - if ( - typeof name !== "string" - || typeof software !== "string" - || !(typeof iconURL === "string" || iconURL === null) - ) - throw new Error("Invalid API response"); - populateInstanceDetailsDialog(name, host, secure, software, iconURL as string | null); - } catch { - populateInstanceDetailsDialog("", host, secure, "", null); - } finally { - showInstanceDetailsDialog(); - } -} - -const addDialog = document.querySelector("#addInstance"); -if (!(addDialog instanceof HTMLDialogElement)) - throw new Error("Couldn't find addInstance dialog"); export const { showAddInstanceDialog, hideAddInstanceDialog -} = initializeAddInstanceDialog(addDialog, addInstanceDialogCallback); +} = initializeAddInstanceFlow(detailsDialog, addDialog); diff --git a/static/dom.mts b/static/dom.mts index 0d013fa..7a7aff5 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 findDialogOrFail(on: Element, selector: string): HTMLDialogElement { + const element = on.querySelector(selector); + if (!(element instanceof HTMLDialogElement)) + throw new Error(`${selector} isn't a dialog`); + return element; +} + export function findFormOrFail(on: Element, selector: string): HTMLFormElement { const element = on.querySelector(selector); if (!(element instanceof HTMLFormElement)) From 6db0c3fc19099ae4e83eeaefb63668bff17cc367 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Tue, 14 Jan 2025 17:43:50 +0100 Subject: [PATCH 08/10] Correct a piece of text --- static/crossroad.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/crossroad.html b/static/crossroad.html index e6a32d6..0ab8ba3 100644 --- a/static/crossroad.html +++ b/static/crossroad.html @@ -47,7 +47,7 @@