Kinda rough but it works (mostly)

This commit is contained in:
CenTdemeern1 2025-01-28 23:26:21 +01:00
parent 64291db3ea
commit 8a02b645f3
4 changed files with 258 additions and 1 deletions

View file

@ -24,6 +24,11 @@ async fn route_for_known_instance_software(
NamedFile::open("static/crossroad.html").await.ok() NamedFile::open("static/crossroad.html").await.ok()
} }
#[get("/")]
async fn configure_page() -> Option<NamedFile> {
NamedFile::open("static/config.html").await.ok()
}
#[get("/<instance>/<route..>", rank = 2)] #[get("/<instance>/<route..>", rank = 2)]
fn route_for_unknown_instance_software(instance: &str, route: PathBuf) -> (ContentType, String) { fn route_for_unknown_instance_software(instance: &str, route: PathBuf) -> (ContentType, String) {
( (
@ -46,7 +51,8 @@ fn rocket() -> _ {
routes![ routes![
known_software_json, known_software_json,
route_for_known_instance_software, route_for_known_instance_software,
route_for_unknown_instance_software route_for_unknown_instance_software,
configure_page
], ],
) )
} }

111
static/config.html Normal file
View file

@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FeDirect</title>
<link rel="stylesheet" href="/static/main.css">
</head>
<body>
<script type="module">
Object.assign(globalThis, await import("/static/config.mjs"));
getMainDialog().show(); // Don't show until the page is ready
</script>
<div class="flex-vcenter">
<dialog id="mainDialog" class="half-width half-height">
<header class="separator-bottom margin-large-bottom">
<div class="flex-row">
<h1>FeDirect</h1>
<p class="margin-auto-top">&ThickSpace;By Nekomata</p>
</div>
<img src="/static/nekomata_small.png" alt="Nekomata Logo" class="logo" />
</header>
<div class="flex-row align-content-center">
<div class="flex-vcenter full-height">
<center class="half-width">
<ol id="instanceList" class="align-start wfit-content"></ol>
<br>
<button onclick="showAddInstanceDialog()">Add an instance</button>
</center>
</div>
<div class="half-width align-self-start">
<div class="flex-hcenter">
<div class="flex-column buttonPanel">
<button id="save">Save</button>
<button id="reorder">Reorder</button>
<button id="reset">Destroy all data</button>
</div>
</div>
</div>
</div>
</dialog>
</div>
<dialog id="addInstance">
<h1>Add an instance</h1>
<form method="dialog" class="addInstanceForm">
<label for="instanceHost">Instance hostname or URL<br>
(for example <code>mastodon.social</code> or <code>https://kitsunes.club/</code>)<br>
</label>
<input id="instanceHost" type="text" name="instanceHost" />
<br>
<input id="autoQueryMetadata" type="checkbox" name="autoQueryMetadata" checked />
<label for="autoQueryMetadata">
<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 not track or save any requests or data.">
Automatically query metadata
</abbr>
</label>
<br><br>
<button type="submit">OK</button>
<button type="reset" class="close">Cancel</button>
</form>
</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">
<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>
</html>

133
static/config.mts Normal file
View file

@ -0,0 +1,133 @@
import { parseHost } from "./add_an_instance.mjs";
import { initializeAddInstanceFlow } from "./add_instance_flow.mjs";
import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs";
import { findButtonOrFail, findDialogOrFail, findOlOrFail } from "./dom.mjs";
import storageManager from "./storage_manager.mjs";
let reordering = false;
// Dragging code is a heavily modified version of https://stackoverflow.com/a/28962290
let elementBeingDragged: HTMLLIElement | undefined;
const detailsDialog = findDialogOrFail(document.body, "#instanceDetails");
const addDialog = findDialogOrFail(document.body, "#addInstance");
const instanceList = findOlOrFail(document.body, "#instanceList");
const saveButton = findButtonOrFail(document.body, "#save");
const reorderButton = findButtonOrFail(document.body, "#reorder");
saveButton.addEventListener("click", e => {
storageManager.save();
});
reorderButton.addEventListener("click", () => {
reordering = !reordering;
if (!reordering) applyReordering();
updateInstanceList();
reorderButton.innerText = reordering ? "Finish reordering" : "Reorder";
});
export const getMainDialog = () => findDialogOrFail(document.body, "#mainDialog");
const {
showInstanceDetailsDialog,
hideInstanceDetailsDialog,
populateInstanceDetailsDialog,
} = initializeInstanceDetailsDialog(detailsDialog, () => { });
export const {
showAddInstanceDialog,
hideAddInstanceDialog
} = initializeAddInstanceFlow(detailsDialog, addDialog);
updateInstanceList();
storageManager.addSaveCallback(updateInstanceList);
function updateInstanceList() {
instanceList.replaceChildren(); // Erase all child nodes
instanceList.style.listStyleType = reordering ? "\"≡ \"" : "disc";
for (let n = 0; n < storageManager.storage.instances.length; n++) {
const instance = storageManager.storage.instances[n];
const li = document.createElement("li");
li.setAttribute("x-option", n.toString());
const label = document.createElement("label");
label.htmlFor = instance.origin;
label.innerText = instance.name + " ";
label.style.cursor = "inherit";
if (instance.iconURL) {
const img = new Image();
img.src = instance.iconURL;
img.alt = `${instance.name} icon`;
img.className = "inlineIcon medium-height";
label.append(img, " ");
}
if (reordering) {
li.draggable = true;
li.addEventListener("dragstart", e => {
if (e.dataTransfer === null) return;
if (!(e.target instanceof HTMLLIElement)) return;
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", "");
elementBeingDragged = e.target;
});
li.addEventListener("dragover", e => {
if (elementBeingDragged === undefined) return;
if (!(e.target instanceof HTMLElement)) return;
const listElement = e.target.closest("li");
if (listElement === null) return;
if (listElement.parentNode === null) return;
if (isBefore(elementBeingDragged, listElement))
listElement.parentNode.insertBefore(elementBeingDragged, listElement);
else
listElement.parentNode.insertBefore(elementBeingDragged, listElement.nextSibling);
e.preventDefault();
});
li.addEventListener("dragenter", e => e.preventDefault());
li.style.cursor = "grab";
} else {
const editLink = document.createElement("a");
editLink.innerText = `Edit`;
editLink.href = "#";
editLink.addEventListener("click", e => {
const host = parseHost(instance.origin)!;
populateInstanceDetailsDialog(
instance.name,
host.host,
host.secure,
instance.software,
instance.iconURL ?? null
);
showInstanceDetailsDialog();
});
const deleteLink = document.createElement("a");
deleteLink.innerText = `Delete`;
deleteLink.href = "#";
deleteLink.addEventListener("click", e => {
storageManager.storage.instances.splice(
storageManager.storage.instances.indexOf(instance)
);
updateInstanceList();
});
label.append(editLink, " ", deleteLink);
}
li.appendChild(label);
instanceList.appendChild(li);
}
}
function isBefore(el1: HTMLLIElement, el2: HTMLLIElement) {
if (el2.parentNode === el1.parentNode)
for (let cur = el1.previousSibling; cur && cur.nodeType !== 9; cur = cur.previousSibling)
if (cur === el2)
return true;
return false;
}
function applyReordering() {
const indices: number[] = [];
for (const el of instanceList.children) {
if (!(el instanceof HTMLLIElement)) continue;
const option = el.getAttribute("x-option");
if (option === null) continue;
indices.push(parseInt(option));
}
storageManager.storage.instances = indices.map(i => storageManager.storage.instances[i]);
}

View file

@ -1,6 +1,13 @@
// I would've LOVED to use generics for this but unfortunately that's not possible. // I would've LOVED to use generics for this but unfortunately that's not possible.
// Type safety, but at what cost... >~< thanks TypeScript // Type safety, but at what cost... >~< thanks TypeScript
export function findOlOrFail(on: Element, selector: string): HTMLOListElement {
const element = on.querySelector(selector);
if (!(element instanceof HTMLOListElement))
throw new Error(`${selector} isn't an ol`);
return element;
}
export function findPreOrFail(on: Element, selector: string): HTMLPreElement { export function findPreOrFail(on: Element, selector: string): HTMLPreElement {
const element = on.querySelector(selector); const element = on.querySelector(selector);
if (!(element instanceof HTMLPreElement)) if (!(element instanceof HTMLPreElement))