Merge pull request 'Finish Add Instance Flow' (#1) from feat/add-instance-2 into main
Reviewed-on: https://git.gay/Nekomata/FeDirect/pulls/1
This commit is contained in:
commit
6a12b59f87
13 changed files with 388 additions and 32 deletions
28
Cargo.lock
generated
28
Cargo.lock
generated
|
@ -279,6 +279,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
name = "fedirect"
|
name = "fedirect"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
"semver",
|
"semver",
|
||||||
|
@ -367,6 +368,17 @@ version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
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]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
@ -388,6 +400,7 @@ dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -1318,11 +1331,13 @@ dependencies = [
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"windows-registry",
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
@ -2159,6 +2174,19 @@ version = "0.2.99"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
|
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]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.76"
|
version = "0.3.76"
|
||||||
|
|
|
@ -4,7 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = "0.12.12"
|
bytes = "1.9.0"
|
||||||
|
reqwest = { version = "0.12.12", features = ["stream"] }
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
semver = "1.0.24"
|
semver = "1.0.24"
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
|
|
||||||
pub mod instance_info;
|
pub mod instance_info;
|
||||||
|
pub mod proxy;
|
||||||
|
|
||||||
pub fn get_routes() -> Vec<Route> {
|
pub fn get_routes() -> Vec<Route> {
|
||||||
routes![instance_info::instance_info]
|
routes![instance_info::instance_info, proxy::proxy]
|
||||||
}
|
}
|
||||||
|
|
30
src/api/proxy.rs
Normal file
30
src/api/proxy.rs
Normal file
|
@ -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<T, E = Debug<reqwest::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
pub async fn get(url: &str) -> Result<ByteStream![Bytes]> {
|
||||||
|
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/<url>")]
|
||||||
|
pub async fn proxy(url: &str) -> req::Result<ByteStream![Bytes]> {
|
||||||
|
req::get(url).await
|
||||||
|
}
|
|
@ -1,29 +1,36 @@
|
||||||
// This file handles the "Add an instance" dialog
|
// 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 {
|
export function parseHost(host: string): { host: string, secure: boolean } | null {
|
||||||
let parsedInstance = URL.parse(host);
|
let parsedInstance = URL.parse(host);
|
||||||
parsedInstance ??= URL.parse("https://" + host);
|
parsedInstance ??= URL.parse("https://" + host);
|
||||||
if (!parsedInstance?.host) return null;
|
if (!parsedInstance?.host) return null;
|
||||||
|
if (!/https?:/.test(parsedInstance.protocol)) return null;
|
||||||
return {
|
return {
|
||||||
host: parsedInstance.host,
|
host: parsedInstance.host,
|
||||||
secure: parsedInstance.protocol === "https:"
|
secure: parsedInstance.protocol === "https:"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initializeAddInstanceDialog(dialog: HTMLDialogElement): {
|
export function initializeAddInstanceDialog(
|
||||||
|
dialog: HTMLDialogElement,
|
||||||
|
callback: (
|
||||||
|
host: string,
|
||||||
|
secure: boolean,
|
||||||
|
autoQueryMetadata: boolean,
|
||||||
|
) => void
|
||||||
|
): {
|
||||||
showAddInstanceDialog: () => void,
|
showAddInstanceDialog: () => void,
|
||||||
hideAddInstanceDialog: () => void,
|
hideAddInstanceDialog: () => void,
|
||||||
} {
|
} {
|
||||||
const showAddInstanceDialog = () => dialog.showModal();
|
const showAddInstanceDialog = () => dialog.showModal();
|
||||||
const hideAddInstanceDialog = () => dialog.close();
|
const hideAddInstanceDialog = () => dialog.close();
|
||||||
|
|
||||||
const form = dialog.querySelector(".addInstanceForm");
|
const form = findFormOrFail(dialog, ".addInstanceForm");
|
||||||
if (!(form instanceof HTMLFormElement))
|
const instanceHost = findInputOrFail(form, "#instanceHost");
|
||||||
throw new Error(".addInstanceForm isn't a form");
|
const autoQueryMetadata = findInputOrFail(form, "#autoQueryMetadata");
|
||||||
|
const closeButton = findButtonOrFail(form, ".close");
|
||||||
const instanceHost = form.querySelector("#instanceHost");
|
|
||||||
if (!(instanceHost instanceof HTMLInputElement))
|
|
||||||
throw new Error("#instanceHost isn't an input");
|
|
||||||
|
|
||||||
instanceHost.addEventListener("input", e => {
|
instanceHost.addEventListener("input", e => {
|
||||||
if (parseHost(instanceHost.value) === null)
|
if (parseHost(instanceHost.value) === null)
|
||||||
|
@ -32,19 +39,13 @@ export function initializeAddInstanceDialog(dialog: HTMLDialogElement): {
|
||||||
instanceHost.setCustomValidity("");
|
instanceHost.setCustomValidity("");
|
||||||
});
|
});
|
||||||
|
|
||||||
form.addEventListener("submit", async e => {
|
form.addEventListener("submit", e => {
|
||||||
// A sane browser doesn't allow for submitting the form if the above validation fails
|
// A sane browser doesn't allow for submitting the form if the above validation fails
|
||||||
const { host, secure } = parseHost(instanceHost.value)!;
|
const { host, secure } = parseHost(instanceHost.value)!;
|
||||||
console.log(
|
callback(host, secure, autoQueryMetadata.checked);
|
||||||
await fetch(`/api/instance_info/${secure}/${encodeURI(host)}`).then(r => r.json())
|
|
||||||
);
|
|
||||||
form.reset();
|
form.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
const closeButton = form.querySelector(".close");
|
|
||||||
if (!(closeButton instanceof HTMLButtonElement))
|
|
||||||
throw new Error(".close isn't a button");
|
|
||||||
|
|
||||||
closeButton.addEventListener("click", e => hideAddInstanceDialog());
|
closeButton.addEventListener("click", e => hideAddInstanceDialog());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
69
static/add_instance_flow.mts
Normal file
69
static/add_instance_flow.mts
Normal file
|
@ -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, host, secure, "", null);
|
||||||
|
} finally {
|
||||||
|
showInstanceDetailsDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
showAddInstanceDialog,
|
||||||
|
hideAddInstanceDialog
|
||||||
|
} = initializeAddInstanceDialog(addDialog, addInstanceDialogCallback);
|
||||||
|
|
||||||
|
return {
|
||||||
|
showAddInstanceDialog,
|
||||||
|
hideAddInstanceDialog
|
||||||
|
};
|
||||||
|
}
|
88
static/confirm_instance_details.mts
Normal file
88
static/confirm_instance_details.mts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// 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";
|
||||||
|
|
||||||
|
const blankImage = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
|
||||||
|
|
||||||
|
export function initializeInstanceDetailsDialog(
|
||||||
|
dialog: HTMLDialogElement,
|
||||||
|
callback: (
|
||||||
|
instanceName: string,
|
||||||
|
instanceHost: string,
|
||||||
|
instanceHostSecure: boolean,
|
||||||
|
instanceSoftware: string,
|
||||||
|
instanceIcon: string | null
|
||||||
|
) => void
|
||||||
|
): {
|
||||||
|
showInstanceDetailsDialog: () => void,
|
||||||
|
hideInstanceDetailsDialog: () => void,
|
||||||
|
populateInstanceDetailsDialog: (
|
||||||
|
instanceNameValue: string,
|
||||||
|
instanceHostValue: string,
|
||||||
|
instanceHostSecureValue: boolean,
|
||||||
|
instanceSoftwareValue: string,
|
||||||
|
instanceIconValue: string | null
|
||||||
|
) => void
|
||||||
|
} {
|
||||||
|
const showInstanceDetailsDialog = () => dialog.showModal();
|
||||||
|
const hideInstanceDetailsDialog = () => dialog.close();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceIcon.src = blankImage;
|
||||||
|
|
||||||
|
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 === null ? blankImage : `/api/proxy/${encodeURIComponent(instanceIconValue)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
form.addEventListener("submit", e => {
|
||||||
|
let image: string | null = null;
|
||||||
|
if (instanceIcon.src !== blankImage) {
|
||||||
|
try {
|
||||||
|
image = resize(instanceIcon);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
callback(
|
||||||
|
instanceName.value,
|
||||||
|
instanceHost.value,
|
||||||
|
instanceHostSecure.checked,
|
||||||
|
instanceSoftware.value,
|
||||||
|
image
|
||||||
|
);
|
||||||
|
form.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
closeButton.addEventListener("click", e => {
|
||||||
|
instanceIcon.src = blankImage;
|
||||||
|
hideInstanceDetailsDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
showInstanceDetailsDialog,
|
||||||
|
hideInstanceDetailsDialog,
|
||||||
|
populateInstanceDetailsDialog
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
:root {
|
:root {
|
||||||
--red: #cb0b0b;
|
--red: #cb0b0b;
|
||||||
--blue: #2081c3;
|
--blue: #2081c3;
|
||||||
|
--transparent-black: #0008;
|
||||||
--large: 2em;
|
--large: 2em;
|
||||||
--medium: 1em;
|
--medium: 1em;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +26,7 @@ dialog {
|
||||||
|
|
||||||
dialog::backdrop {
|
dialog::backdrop {
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
background-color: #0008;
|
background-color: var(--transparent-black);
|
||||||
transition: background-color 0.125s ease-out;
|
transition: background-color 0.125s ease-out;
|
||||||
|
|
||||||
@starting-style {
|
@starting-style {
|
||||||
|
@ -65,6 +66,16 @@ abbr[title] {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-row-reverse {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-column-reverse {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
.half-width {
|
.half-width {
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
}
|
}
|
||||||
|
@ -73,8 +84,12 @@ abbr[title] {
|
||||||
min-height: 50%;
|
min-height: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full-height {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.separator-bottom {
|
.separator-bottom {
|
||||||
border-bottom: solid 1px #0008;
|
border-bottom: solid 1px var(--transparent-black);
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-auto-top {
|
.margin-auto-top {
|
||||||
|
@ -83,4 +98,25 @@ abbr[title] {
|
||||||
|
|
||||||
.margin-large-bottom {
|
.margin-large-bottom {
|
||||||
margin-bottom: var(--large);
|
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%;
|
||||||
}
|
}
|
|
@ -47,7 +47,7 @@
|
||||||
<br>
|
<br>
|
||||||
<input id="autoQueryMetadata" type="checkbox" name="autoQueryMetadata" checked />
|
<input id="autoQueryMetadata" type="checkbox" name="autoQueryMetadata" checked />
|
||||||
<label for="autoQueryMetadata">
|
<label for="autoQueryMetadata">
|
||||||
<abbr title="This makes a single API request to FeDirect to automatically try to detect instance metadata, such as the name, software, and an icon.
|
<abbr title="This uses FeDirect's API to automatically try to detect instance metadata, such as the name, software, and an icon.
|
||||||
We do this on the backend to avoid CORS problems.
|
We do this on the backend to avoid CORS problems.
|
||||||
We do not track or save any requests or data.">
|
We do not track or save any requests or data.">
|
||||||
Automatically query metadata
|
Automatically query metadata
|
||||||
|
@ -58,6 +58,50 @@ We do not track or save any requests or data.">
|
||||||
<button type="reset" class="close">Cancel</button>
|
<button type="reset" class="close">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<dialog id="instanceDetails">
|
||||||
|
<h1>Confirm instance details</h1>
|
||||||
|
<form method="dialog" class="instanceDetailsForm">
|
||||||
|
<div class="flex-row">
|
||||||
|
<div class="half-width">
|
||||||
|
<label for="instanceName">Instance name</label>
|
||||||
|
<br>
|
||||||
|
<input id="instanceName" type="text" name="instanceName" required />
|
||||||
|
<br><br>
|
||||||
|
<label for="instanceHost">Instance hostname</label>
|
||||||
|
<br>
|
||||||
|
<input id="instanceHost" type="text" name="instanceHost" required />
|
||||||
|
<br>
|
||||||
|
<input type="checkbox" name="instanceHostSecure" id="instanceHostSecure" checked />
|
||||||
|
<label for="instanceHostSecure">
|
||||||
|
<abbr title="Whether to use HTTPS (as opposed to HTTP).
|
||||||
|
Unchecking this is not recommended, and this option only exists for exceptional cases">
|
||||||
|
Secure?
|
||||||
|
</abbr>
|
||||||
|
</label>
|
||||||
|
<br><br>
|
||||||
|
<label for="instanceSoftware">Instance software</label>
|
||||||
|
<br>
|
||||||
|
<select id="instanceSoftware" type="text" name="instanceSoftware" required>
|
||||||
|
<option value="">(Please select)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="half-width flex-row-reverse">
|
||||||
|
<div class="full-height flex-column-reverse">
|
||||||
|
<div>
|
||||||
|
<label for="iconContainer">Instance icon</label>
|
||||||
|
<div id="iconContainer" class="square iconContainer">
|
||||||
|
<!-- This data URI is for a transparent gif image -->
|
||||||
|
<img id="instanceIcon" alt="Icon for the selected instance" class="icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<button type="submit">OK</button>
|
||||||
|
<button type="reset" class="close">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -1,15 +1,14 @@
|
||||||
import { initializeAddInstanceDialog } from "./add_an_instance.mjs";
|
import { initializeAddInstanceFlow } from "./add_instance_flow.mjs";
|
||||||
import knownSoftware from "./known_software.mjs";
|
import { findDialogOrFail } from "./dom.mjs";
|
||||||
import storageManager from "./storage_manager.mjs";
|
|
||||||
console.log(knownSoftware);
|
|
||||||
|
|
||||||
console.log(storageManager.storage.instances);
|
|
||||||
|
|
||||||
export function getMainDialog(): HTMLDialogElement {
|
export function getMainDialog(): HTMLDialogElement {
|
||||||
return document.getElementById('mainDialog') as HTMLDialogElement;
|
return document.getElementById('mainDialog') as HTMLDialogElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialog = document.querySelector("#addInstance");
|
const detailsDialog = findDialogOrFail(document.body, "#instanceDetails");
|
||||||
if (!(dialog instanceof HTMLDialogElement))
|
const addDialog = findDialogOrFail(document.body, "#addInstance");
|
||||||
throw new Error("Couldn't find addInstance dialog");
|
|
||||||
export const { showAddInstanceDialog, hideAddInstanceDialog } = initializeAddInstanceDialog(dialog);
|
export const {
|
||||||
|
showAddInstanceDialog,
|
||||||
|
hideAddInstanceDialog
|
||||||
|
} = initializeAddInstanceFlow(detailsDialog, addDialog);
|
||||||
|
|
44
static/dom.mts
Normal file
44
static/dom.mts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// 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))
|
||||||
|
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;
|
||||||
|
}
|
15
static/image.mts
Normal file
15
static/image.mts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
type Instance = {
|
export type Instance = {
|
||||||
/**
|
/**
|
||||||
* The instance's (nick)name
|
* The instance's (nick)name
|
||||||
* @example "eepy.moe"
|
* @example "eepy.moe"
|
||||||
|
|
Loading…
Add table
Reference in a new issue