Kinda rough but it works (mostly)
This commit is contained in:
parent
8fe980c4be
commit
da8a788658
4 changed files with 258 additions and 1 deletions
|
@ -24,6 +24,11 @@ async fn route_for_known_instance_software(
|
|||
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)]
|
||||
fn route_for_unknown_instance_software(instance: &str, route: PathBuf) -> (ContentType, String) {
|
||||
(
|
||||
|
@ -46,7 +51,8 @@ fn rocket() -> _ {
|
|||
routes![
|
||||
known_software_json,
|
||||
route_for_known_instance_software,
|
||||
route_for_unknown_instance_software
|
||||
route_for_unknown_instance_software,
|
||||
configure_page
|
||||
],
|
||||
)
|
||||
}
|
||||
|
|
111
static/config.html
Normal file
111
static/config.html
Normal 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">  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
133
static/config.mts
Normal 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]);
|
||||
}
|
|
@ -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 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 {
|
||||
const element = on.querySelector(selector);
|
||||
if (!(element instanceof HTMLPreElement))
|
||||
|
|
Loading…
Add table
Reference in a new issue