let adminToken = null; const adminWrapper = document.getElementById("adminWrapper"); const adminZone = document.getElementById("adminZone"); const spriteZone = document.getElementById("spriteZone"); const createTableForm = document.getElementById("createTableForm"); const createTokenForm = document.getElementById("createTokenForm"); const tokenWrapper = document.getElementById("adminWrapper_tokens"); const newTableName = document.getElementById("newTableName"); const newTablePass = document.getElementById("newTablePass"); const tokenSpriteDropdown = document.getElementById("token_combobox"); const tokenName = document.getElementById("token_name"); const tokenWidth = document.getElementById("token_width"); const tokenHeight = document.getElementById("token_height"); const tokenCX = document.getElementById("token_cx"); const tokenCY = document.getElementById("token_cy"); const previewZone = document.getElementById("tokenPreview_zone"); const tokenAspect = document.getElementById("tokenKeepAspect"); const aspectLockLabel = document.getElementById("aspectLockLabel"); const tokenZone = document.getElementById("tokenZone"); async function rebindUi(name, pass) { try { const headers = new Headers(); headers.set('Authorization', 'Bearer ' + adminToken.access_token); const res = await fetch(`/admin/api/table/${name}?passcode=${pass}`, { method: 'GET', headers: headers, }); const mapImgs = await fetch (`/admin/api/upload/${name}/map/?passcode=${pass}`, { method: 'GET', headers: headers, }); const tokenImgs = await fetch(`/admin/api/upload/${name}/token/?passcode=${pass}`, { method: 'GET', headers: headers, }); let infoHtml = "" if (res.ok) { document.getElementById("input_table_name").value = name; document.getElementById("input_table_pass").value = pass; dial(); const table = await res.json() infoHtml = "← table list
"; infoHtml += `
` infoHtml += "
"; infoHtml += "
" if (mapImgs.ok) { infoHtml += ""; const imgs = (await mapImgs.json()).sort(); infoHtml += ""; } else { infoHtml += ""; } adminZone.innerHTML = infoHtml; let tokenListHTML = "
"; if (tokenImgs.ok) { const tokens = (await tokenImgs.json()).sort(); tokenListHTML += ""; fillSpriteDropdown(tokens); } else { tokenListHTML += "" } spriteZone.innerHTML = tokenListHTML; tokenWrapper.style.display = "inline"; } else { console.log(res.status); } } catch (err) { setErr(`${err.name}: ${err.message}`); } } function fillSpriteDropdown(tokens) { tokens = tokens.sort(); let options = ""; for (const t of tokens) { const parts = t.split("/"); const o = `\n`; options += o; } tokenSpriteDropdown.innerHTML = options; } function previewSprite(source) { if (source) { switch (source.id) { case "token_combobox": reinitializeSpritePreview(); break; case "token_cx": case "token_cy": drawTokenOrigin(); break; default: scaleSpritePreview(source); console.log("default case"); } } } function toggleAspectLock() { if (tokenKeepAspect.checked) { aspectLockLabel.innerHTML = "🔒" } else { aspectLockLabel.innerHTML = "🔓" } } function scaleSpritePreview(source) { if (mapImg && mapImg._image) { const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth; const keepAspect = tokenAspect.checked; const img = previewZone.children[0]; tokenHeight.value = Math.floor(Number(tokenHeight.value)) tokenWidth.value = Math.floor(Number(tokenWidth.value)) if (img) { if (!keepAspect || !source) { img.width = Number(tokenWidth.value) * scaleFactor; img.height = Number(tokenHeight.value) * scaleFactor; } else { const currentAspect = img.width/img.height; switch (source.id) { case "token_width": img.width = Number(tokenWidth.value) * scaleFactor; img.height = (img.clientWidth / img.naturalWidth) * img.naturalHeight; tokenHeight.value = Math.floor(Number(tokenWidth.value)/currentAspect); break; case "token_height": img.height = Number(tokenHeight.value) * scaleFactor; img.width = (img.clientHeight / img.naturalHeight) * img.naturalWidth; tokenWidth.value = Math.floor(currentAspect * Number(tokenHeight.value)); break; } } if (!tokenCX.value || source) { tokenCX.value = Number(tokenWidth.value)/2; } if (!tokenCY.value || source) { tokenCY.value = Number(tokenHeight.value)/2; } drawTokenOrigin(); } } } function drawTokenOrigin() { if (tokenSpriteDropdown.selectedIndex >= 0) { tokenCX.value = Math.floor(Number(tokenCX.value)) tokenCY.value = Math.floor(Number(tokenCY.value)) const img = previewZone.children[0]; const x = Number(tokenWidth.value) / Number(tokenCX.value); const y = Number(tokenHeight.value) / Number(tokenCY.value); const origin = {x: img.width/x, y: img.height/y}; const originImg = document.createElement("img"); originImg.src="/table/origin.png"; originImg.style.position = "absolute"; originImg.style.left = (origin.x - 2) + "px"; originImg.style.top = (origin.y - 2) + "px"; if (previewZone.children.length > 1) { previewZone.replaceChild(originImg, previewZone.children[1]); } else { previewZone.appendChild(originImg); } } } function reinitializeSpritePreview(existing = false) { const img = document.createElement("img"); img.src = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].value; if (!existing) { const tokenNameParts = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].text.split("."); tokenNameParts.pop(); tokenName.value = tokenNameParts.join("."); } img.onload = () => { const w = img.naturalWidth; const h = img.naturalHeight; if (!existing) { tokenWidth.value = w; tokenHeight.value = h; tokenCX.value = "" tokenCY.value = "" } scaleSpritePreview(); } previewZone.innerHTML = ""; previewZone.appendChild(img); } function createToken() { const w = Number(tokenWidth.value); const h = Number(tokenHeight.value); const oX = Number(tokenCX.value); const oY = Number(tokenCY.value); const img = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].value; const name = tokenName.value; if (!isNaN(w) && !isNaN(h) && !isNaN(oX) && !isNaN(oY) && img && name) { // send it on the websocket and wait for it to come back const [x, y] = [0, 0]; sendToken({ w: w, h: h, ox:oX, oy:oY, x: x, y: y, sprite: img, name: name, active: false} ); setTokenCreateFormVisible(false); return; } setErr("All token fields are required"); } function destroyToken(id) { const existing = tokens.find(t=>t.t.id == id); if (existing) { const self = Object.assign({}, existing.t); self.active = false; self.x = null; self.y = null; sendToken(self); } } function previewExistingToken(id) { const existing = tokens.find(t=>t.t.id == id); if (existing) { tokenWidth.value = existing.t.w; tokenHeight.value = existing.t.h; tokenCX.value = existing.t.oX; tokenCY.value = existing.t.oY; tokenName.value = existing.t.name; for (let i = 0; i < tokenSpriteDropdown.options.length; i++) { if (tokenSpriteDropdown.options[i].value == existing.t.sprite) { tokenSpriteDropdown.selectedIndex = i; break; } } reinitializeSpritePreview(true); } } function copyToken(id) { setTokenCreateFormVisible(true); previewExistingToken(id); } function renderTokenMasterList() { if (tokenZone) { let tokenMasterListHTML = ""; const scroll = tokenZone.scrollTop; for (const t of tokens) { tokenMasterListHTML += `
  • ${t.t.name}
  • \n`; } tokenZone.innerHTML = tokenMasterListHTML; tokenZone.scrollTop = scroll; } } function publishAuxMsg() { const txtArea = document.getElementById("auxMsgZone"); if (txtArea != null) { publish({auxMsg: txtArea.value, auth: adminToken.access_token}); } } function sendMapImg(url) { publish({mapImg: url, auth: adminToken.access_token}); } function sendToken(t) { publish({token: t, auth: adminToken.access_token}); } async function uploadMapImg() { try { var input = document.getElementById("map_img_upload"); var data = new FormData(); data.append('file', input.files[0]); data.append('name', tableKey.name); data.append('passcode', tableKey.passcode); const headers = new Headers(); headers.set('Authorization', 'Bearer ' + adminToken.access_token); res = await fetch(`/admin/api/upload/${tableKey.name}/map/`, { headers: headers, method: "POST", body: data, }); if (res.ok) { // refresh so we can see the new entry in the list rebindUi(tableKey.name, tableKey.passcode); } else { throw new Error("Something went wrong uploading the map BG..."); } } catch (err) { setErr(`${err.name}: ${err.message}`); } } async function uploadTokenImg() { try { var input = document.getElementById("token_img_upload"); var data = new FormData(); data.append('file', input.files[0]); data.append('name', tableKey.name); data.append('passcode', tableKey.passcode); const headers = new Headers(); headers.set('Authorization', 'Bearer ' + adminToken.access_token); res = await fetch(`/admin/api/upload/${tableKey.name}/token/`, { headers: headers, method: "POST", body: data, }); if (res.ok) { // refresh so we can see the new entry in the list rebindUi(tableKey.name, tableKey.passcode); } else { throw new Error("Something went wrong uploading the token sprite..."); } } catch (err) { setErr(`${err.name}: ${err.message}`); } } async function deleteImg(url) { try { if (url.startsWith("/uploads/")) { const parts = url.split("/"); parts.shift(); const table = parts[1]; const imgType = parts[2] const file = parts[3]; const headers = new Headers(); headers.set('Authorization', 'Bearer ' + adminToken.access_token); const res = await fetch(`/admin/api/upload/${table}/${imgType}/${file}?passcode=${tableKey.passcode}`, { headers: headers, method: "DELETE", }); if (res.ok) { // refresh UI rebindUi(tableKey.name, tableKey.passcode); } else { throw new Error ("Something went wrong deleting the image..."); } } } catch (err) { setErr(`${err.name}: ${err.message}`); } } async function destroyTable() { try { if (confirm("You really want to destroy this table?")) { const headers = new Headers(); headers.set('Authorization', 'Bearer ' + adminToken.access_token); const res = await fetch(`/admin/api/table/${tableKey.name}`, { method: 'DELETE', headers: headers, body: JSON.stringify(tableKey) }); if (res.ok) { conn.close(1000); initializeMap(""); getTables(); } else { setErr(await res.json()); } } } catch (err) { setErr(`${err.name}: ${err.message}`); } } async function getTables() { try { const headers = new Headers(); headers.set('Authorization', 'Bearer ' + adminToken.access_token); const res = await fetch('/admin/api/table/', { method: 'GET', headers: headers }); if (res.ok) { const tableList = await res.json(); tableList.sort((a,b)=>{ if (a.name < b.name) { return -1; } else if (a.name > b.name) { return 1; } return 0; }); let tableListHTML = "" adminZone.innerHTML = tableListHTML; tokenWrapper.style.display = "none"; } else { // fail silently } } catch { // fail silently } } async function doLogin() { const adminUsrInput = document.getElementById("name_entry"); const adminPassInput = document.getElementById("input_admin_pass"); if (adminUsrInput && adminPassInput) { adminToken = await getAdminToken(adminUsrInput.value, adminPassInput.value); if (adminToken) { getTables(); adminWrapper.style.display="inline"; adminZone.style.display = "block"; closeErr(); replaceAdminModal(); } else { setErr("Incorrect credentials"); } } } function replaceAdminModal() { const adminModal = document.getElementById("admin_modal"); if (adminModal) { adminModal.innerHTML = "Logout"; } } async function getAdminToken(user, pass) { const headers = new Headers(); headers.set('Authorization', 'Basic ' + btoa(user + ":" + pass)); try { const res = await fetch('/admin/api/auth/', { method: 'POST', headers: headers }); if (res.ok) { return await res.json(); } return null; } catch (err) { return null; } } function setTableCreateFormVisible(v) { if (createTableForm) { createTableForm.style.display = v ? "block" : "none"; } if (!v) { if (newTableName) { newTableName.value = ""; } if (newTablePass) { newTablePass.value = ""; } } } function setTokenCreateFormVisible(v) { if (createTokenForm && tokenZone) { if (v) { // clear the form when displaying because we may have values from a preview of an existing token tokenWidth.value = ""; tokenHeight.value = ""; tokenCX.value = ""; tokenCY.value = ""; tokenName.value = ""; tokenSpriteDropdown.selectedIndex = 0; } createTokenForm.style.display = v ? "block" : "none"; tokenZone.style.display = v ? "none" : "block"; previewZone.innerHTML = ""; } } async function createTable() { const headers = new Headers(); headers.set('Authorization', 'Bearer ' + adminToken.access_token); const formData = new FormData(); formData.set("name", newTableName.value); formData.set("passcode", newTablePass.value); let bodyStr = "{"; for (const pair of formData.entries()) { bodyStr += `"${pair[0]}": "${pair[1]}",`; } bodyStr = bodyStr.slice(0, -1); bodyStr += "}"; const res = await fetch('/admin/api/table/', { method: 'POST', headers: headers, body: bodyStr, }); if (res.ok) { getTables(); setTableCreateFormVisible(false); } else if (res.status === 422) { setErr('Table name and passcode must be only alphanumeric and underscores'); } else { setErr('Error creating table'); } }