From 7e1416a721f35c929689076bffceed926dc7a770 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Mon, 3 Feb 2025 19:00:15 +0100 Subject: [PATCH] Editing & saving changes & fixes --- static/add_instance_flow.mts | 18 ++++------- static/config.mts | 46 ++++++++++++++++++----------- static/confirm_instance_details.mts | 25 ++++++++++++++++ static/crossroad.mts | 2 +- static/main.css | 14 +++++++++ 5 files changed, 75 insertions(+), 30 deletions(-) diff --git a/static/add_instance_flow.mts b/static/add_instance_flow.mts index e59c6c9..6f22ae5 100644 --- a/static/add_instance_flow.mts +++ b/static/add_instance_flow.mts @@ -1,5 +1,5 @@ import { AddInstanceDialog } from "./add_an_instance.mjs"; -import { InstanceDetailsDialog, InstanceDetailsDialogData } from "./confirm_instance_details.mjs"; +import { dialogDetailsToInstance, InstanceDetailsDialog, InstanceDetailsDialogData } from "./confirm_instance_details.mjs"; import { Dialog } from "./dialog.mjs"; import storageManager, { Instance } from "./storage_manager.mjs"; @@ -26,14 +26,14 @@ export class AddInstanceFlow { this.detailsDialog = new InstanceDetailsDialog(detailsDialog, true); } - async start() { + async start(autoSave: boolean) { const { autoQueryMetadata, host, secure, } = await this.addDialog.present(); - let detailsDialogData: InstanceDetailsDialogData = { + const detailsDialogData: InstanceDetailsDialogData = { name: host, host, hostSecure: secure, @@ -62,17 +62,11 @@ export class AddInstanceFlow { } catch { } this.spinnerDialog.close(); - detailsDialogData = await this.detailsDialog.present(detailsDialogData); - - const instance: Instance = { - name: detailsDialogData.name, - origin: `http${detailsDialogData.hostSecure ? "s" : ""}://${detailsDialogData.host}`, - software: detailsDialogData.software, - iconURL: detailsDialogData.iconURL ?? undefined - }; + const finalData = await this.detailsDialog.present(detailsDialogData); + const instance = dialogDetailsToInstance(finalData, {}); storageManager.storage.instances.push(instance); - storageManager.save(); + if (autoSave) storageManager.save(); console.log("Successfully added new instance:", instance); } } diff --git a/static/config.mts b/static/config.mts index 8a1c257..5e90af8 100644 --- a/static/config.mts +++ b/static/config.mts @@ -1,10 +1,11 @@ import { parseHost } from "./add_an_instance.mjs"; import { AddInstanceFlow } from "./add_instance_flow.mjs"; -import { InstanceDetailsDialog } from "./confirm_instance_details.mjs"; +import { dialogDetailsFromInstance, dialogDetailsToInstance, InstanceDetailsDialog, InstanceDetailsDialogData } from "./confirm_instance_details.mjs"; import { findButtonOrFail, findDialogOrFail, findOlOrFail } from "./dom.mjs"; -import storageManager from "./storage_manager.mjs"; +import storageManager, { Instance } from "./storage_manager.mjs"; let reordering = false; +let unsaved = false; // Dragging code is a heavily modified version of https://stackoverflow.com/a/28962290 let elementBeingDragged: HTMLLIElement | undefined; @@ -20,12 +21,12 @@ const reorderButton = findButtonOrFail(document.body, "#reorder"); let instanceDetailsDialog = new InstanceDetailsDialog(detailsDialog, true); let addInstanceFlow = new AddInstanceFlow(addDialog, spinnerDialog, instanceDetailsDialog); -startAddInstanceFlowButton.addEventListener("click", e => addInstanceFlow.start()); - -saveButton.addEventListener("click", e => { - storageManager.save(); +startAddInstanceFlowButton.addEventListener("click", e => { + addInstanceFlow.start(false).then(_ => unsavedChanges()); }); +saveButton.addEventListener("click", e => saveChanges()); + reorderButton.addEventListener("click", () => { reordering = !reordering; if (!reordering) applyReordering(); @@ -38,6 +39,26 @@ storageManager.addSaveCallback(updateInstanceList); mainDialog.show(); +function saveChanges() { + storageManager.save(); + unsaved = false; + saveButton.classList.remove("pulse-red"); +} + +function unsavedChanges() { + if (!unsaved) { + unsaved = true; + saveButton.classList.add("pulse-red"); + } +} + +async function editInstance(instance: Instance) { + const data = dialogDetailsFromInstance(instance); + const newData = await instanceDetailsDialog.present(data); + dialogDetailsToInstance(newData, instance); + unsavedChanges(); +} + function updateInstanceList() { instanceList.replaceChildren(); // Erase all child nodes instanceList.style.listStyleType = reordering ? "\"≡ \"" : "disc"; @@ -83,17 +104,7 @@ function updateInstanceList() { 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(); - }); + editLink.addEventListener("click", e => editInstance(instance)); const deleteLink = document.createElement("a"); deleteLink.innerText = `Delete`; deleteLink.href = "#"; @@ -127,4 +138,5 @@ function applyReordering() { indices.push(parseInt(option)); } storageManager.storage.instances = indices.map(i => storageManager.storage.instances[i]); + unsavedChanges(); } diff --git a/static/confirm_instance_details.mts b/static/confirm_instance_details.mts index 530c1f6..a69f0c4 100644 --- a/static/confirm_instance_details.mts +++ b/static/confirm_instance_details.mts @@ -1,11 +1,17 @@ // This file handles the "Confirm instance details" dialog +import { parseHost } from "./add_an_instance.mjs"; import { FormDialog, ONCE } from "./dialog.mjs"; import { findButtonOrFail, findFormOrFail, findImageOrFail, findInputOrFail, findSelectOrFail } from "./dom.mjs"; import knownSoftware from "./known_software.mjs"; +import { Instance } from "./storage_manager.mjs"; const blankImage = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; +export function mergeHost(host: string, secure: boolean): string { + return `http${secure ? "s" : ""}://${host}`; +} + export type InstanceDetailsDialogData = { name: string, host: string, @@ -14,6 +20,25 @@ export type InstanceDetailsDialogData = { iconURL: string | null }; +export function dialogDetailsFromInstance(instance: Instance): InstanceDetailsDialogData { + const host = parseHost(instance.origin)!; + return { + name: instance.name, + host: host.host, + hostSecure: host.secure, + software: instance.software, + iconURL: instance.iconURL ?? null + }; +} + +export function dialogDetailsToInstance(data: InstanceDetailsDialogData, instance: Partial): Instance { + instance.name = data.name; + instance.origin = mergeHost(data.host, data.hostSecure); + instance.software = data.software; + instance.iconURL = data.iconURL ?? undefined; + return instance as Instance; +} + export class InstanceDetailsDialog extends FormDialog { protected instanceName: HTMLInputElement; protected instanceHost: HTMLInputElement; diff --git a/static/crossroad.mts b/static/crossroad.mts index bcab3c1..067d956 100644 --- a/static/crossroad.mts +++ b/static/crossroad.mts @@ -31,7 +31,7 @@ if (!autoRedirect()) { mainDialog.show(); }; -startAddInstanceFlowButton.addEventListener("click", e => addInstanceFlow?.start()); +startAddInstanceFlowButton.addEventListener("click", e => addInstanceFlow?.start(true)); redirectButton.addEventListener("click", e => { // Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default diff --git a/static/main.css b/static/main.css index e60921f..a1b0706 100644 --- a/static/main.css +++ b/static/main.css @@ -184,4 +184,18 @@ abbr[title] { .buttonPanel>* { margin-top: min(var(--xl), 6vh); +} + +.pulse-red { + animation: 1s ease-in-out 0s infinite alternate both running pulse-red-anim; +} + +@keyframes pulse-red-anim { + 0% { + box-shadow: 0px 0px 0px var(--red); + } + + 100% { + box-shadow: 0px 0px 20px var(--red); + } } \ No newline at end of file