Kinda rough but it works (mostly)
This commit is contained in:
parent
64291db3ea
commit
8a02b645f3
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()
|
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
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.
|
// 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))
|
||||||
|
|
Loading…
Add table
Reference in a new issue