Something went wrong. Try again.
Paper Mario mod hosting site - AT Protocol appview starhaven.dev
appview
Something went wrong. Try again.
7.1 kB · 187 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188import { html } from "./dom.js";
class MediaInput extends HTMLElement { connectedCallback() { requestAnimationFrame(() => this.#setup()); }
#setup() { const endpoint = this.getAttribute("endpoint"); const fileInput = this.querySelector("input[type=file]"); const progressEl = this.querySelector("progress"); const field = this.querySelector("input[type=hidden]"); const gallery = this;
function itemTemplate(cid, mimeType, size, previewUrl, alt, url = "") { const what = url ? "video" : "image"; const preview = url ? html`<span class="video-preview" aria-hidden="true">▶</span>` : html`<img src="${previewUrl}" alt="" />`; const item = html` <div class="item" data-cid="${cid}" data-mime-type="${mimeType}" data-size="${size}" data-url="${url}" > <span class="handle" draggable="true" aria-hidden="true"></span> ${preview} <input type="text" class="alt" placeholder="Describe this ${what}" aria-label="Describe this ${what}" value="${alt}" /> <button type="button" class="remove" aria-label="Remove ${what}">×</button> </div> `.firstElementChild; return item; }
// The id of the YouTube video a link is to, as a watch, short, embed, // live, or youtu.be link. function youtubeId(text) { let url; try { url = new URL(text.trim()); } catch { return null; } if (!["http:", "https:"].includes(url.protocol)) return null; const host = url.hostname.replace(/^(www|m)\./, ""); const [, first, second] = url.pathname.split("/"); let id = null; if (host === "youtu.be") id = first; else if (host === "youtube.com" || host === "youtube-nocookie.com") { if (["embed", "shorts", "live", "v"].includes(first)) id = second; else if (first === "watch") id = url.searchParams.get("v"); } return /^[A-Za-z0-9_-]{11}$/.test(id ?? "") ? id : null; }
const youtubeInput = this.querySelector(".youtube-url"); function addYoutube() { const id = youtubeId(youtubeInput.value); if (!id) { youtubeInput.setCustomValidity("Enter a link to a YouTube video"); youtubeInput.reportValidity(); return; } gallery.insertBefore( itemTemplate("", "", 0, "", "", `https://www.youtube.com/watch?v=${id}`), fileInput.closest("label"), ); youtubeInput.value = ""; } youtubeInput.addEventListener("input", () => youtubeInput.setCustomValidity("")); // Enter here would otherwise save the whole form. youtubeInput.addEventListener("keydown", (event) => { if (event.key !== "Enter") return; event.preventDefault(); addYoutube(); }); this.querySelector(".youtube-add button").addEventListener("click", addYoutube);
async function upload(file) { const response = await fetch(endpoint, { method: "POST", headers: { "content-type": file.type }, body: file, }); if (!response.ok) { throw new Error( (await response.text()) || "The selected file could not be uploaded - try again", ); } return response.json(); }
async function addFiles(files) { const images = Array.from(files).filter((f) => f.type.startsWith("image/")); if (!images.length) return;
progressEl.hidden = false; progressEl.max = images.length; progressEl.value = 0;
for (const file of images) { try { const blob = await upload(file); gallery.insertBefore( itemTemplate( blob.cid, blob.mimeType, blob.size, URL.createObjectURL(file), "", ), fileInput.closest("label"), ); } catch (failure) { alert(failure.message); } progressEl.value += 1; }
progressEl.hidden = true; fileInput.value = ""; }
fileInput.addEventListener("change", () => addFiles(fileInput.files));
// On the images the page came with as well as those added since. gallery.addEventListener("click", (event) => { event.target.closest(".remove")?.closest(".item")?.remove(); });
let dragging = null;
gallery.addEventListener("dragstart", (event) => { const item = event.target.closest(".item"); if (!item) return; dragging = item; item.classList.add("dragging"); });
gallery.addEventListener("dragend", () => { if (dragging) dragging.classList.remove("dragging"); dragging = null; });
gallery.addEventListener("dragover", (event) => { if (!dragging) return; event.preventDefault(); const target = event.target.closest(".item"); if (!target || target === dragging) return; const rect = target.getBoundingClientRect(); const before = event.clientY - rect.top < rect.height / 2; gallery.insertBefore(dragging, before ? target : target.nextSibling); });
function sync() { const items = Array.from(gallery.querySelectorAll(".item")).map((li) => { const alt = li.querySelector(".alt").value.trim(); if (li.dataset.url) return { url: li.dataset.url, alt }; return { cid: li.dataset.cid, mimeType: li.dataset.mimeType, size: Number(li.dataset.size), alt, }; }); field.value = JSON.stringify(items); }
const form = gallery.closest("form"); if (form) form.addEventListener("submit", sync); gallery.addEventListener("input", sync); gallery.addEventListener("click", sync); new MutationObserver(sync).observe(gallery, { childList: true }); sync(); }}
customElements.define("media-input", MediaInput);