const ANIMATION_STYLES = `default Aura Bats Bees Bounce Cloud Confetti Crying Dislike Fire Idea Lasers Like Magnet Mistletoe Money Noise Orbit Pizza Rain Rotate Shake Snow Snowball Spin Splash Stop ZZZ`.split('\n'); function createAnimationStyleSelect() { let select = document.createElement("select"); ANIMATION_STYLES.forEach(a => { let option = document.createElement("option"); option.innerText = a; if (a == "default") { option.value = ''; } else { option.value = a.toLowerCase(); } select.appendChild(option); }); return select; } addbtn.onclick = async () => { var fileHandles = await showOpenFilePicker({ id: "add", multiple: true, types: [ { description: "Any image supported by browser", accept: { "image/*": [], } } ] }); if (!fileHandles.length) return; var files = []; for (let fileHandle of fileHandles) { try { files.push(await fileHandle.getFile()); } catch (error) { displayError(error); } } await addFiles(files); }; document.body.onpaste = event => { if (event.clipboardData.files.length) { addFiles(event.clipboardData.files); } }; document.body.ondragover = event => event.preventDefault(); document.body.ondrop = event => { event.preventDefault(); if (event.dataTransfer.files.length) { addFiles(event.dataTransfer.files); } }; async function addFiles(files) { var newEmojis = []; var errors = []; for (let file of files) { try { let image = await loadImage(URL.createObjectURL(file)); /* let width = image.width; let height = image.height; const MIN = 64, MAX = 2048; let aspectRatio = width / height; if (aspectRatio < (MIN/MAX) || aspectRatio > (MAX/MIN)) throw new Error("Invalid aspect ratio"); if (height > width) { if (height > MAX) { height = MAX; width = height * aspectRatio; } if (width < MIN) { width = MIN; height = width * (1/aspectRatio); } } else { if (width > MAX) { width = MAX; height = width * (1/aspectRatio); } if (height < MIN) { height = MIN; width = height * aspectRatio; } } */ // Although VRChat will store the original image within 64-2048px, it looks like the game resizes it to 512x512. let width = 512, height = 512; let canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; let ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, width, height); let data = canvas.toDataURL(); URL.revokeObjectURL(image.src); let e = { internalId: randomId(), date: new Date().toISOString() }; await chrome.storage.local.set({["data-"+e.internalId]: data}); newEmojis.push(e); createEmojiSquare(e, data); errorDiv.innerText = ""; } catch(error) { console.error(error); errors.push(error.message); } } if (errors.length) { displayError(`Errors occured adding ${errors.length} files: ${errors.join(', ')}`); } var {emojis} = await chrome.storage.local.get("emojis"); emojis ||= []; emojis = emojis.concat(newEmojis); await chrome.storage.local.set({emojis}); } var deleteMode = false; deletebtn.onclick = () => { if (deleteMode) { deleteMode = false; document.body.classList.remove("deletemode"); deletebtn.innerText = "Delete Emojis"; return; } deleteMode = true; document.body.classList.add("deletemode"); deletebtn.innerText = "Stop Delete"; }; chrome.storage.local.get("defaultAnimationStyle").then(({defaultAnimationStyle}) => { default_animation_style_select.value = defaultAnimationStyle || "aura"; }); default_animation_style_select.onchange = async function () { chrome.storage.local.set({defaultAnimationStyle: this.value}); var enabled = [...document.querySelectorAll("[data-state=enabled]")].filter(e => !e.querySelector("select").value); if (enabled.length) { default_animation_style_select.disabled = true; enabled.forEach(e => e.querySelector("select").disabled = true); let {emojis} = await chrome.storage.local.get("emojis"); for (let div of enabled) { div.dataset.state = "pending"; try { await callContentScript("deleteEmoji", div.dataset.currentId); } catch(error) { displayError(error); } try { let newEmoji = await callContentScript("createEmoji", div.querySelector("img").src, div.querySelector("select").value || default_animation_style_select.value); emojis.find(e => e.internalId == div.dataset.internalId).currentId = newEmoji.id; div.dataset.currentId = newEmoji.id; div.dataset.state = "enabled"; } catch (error) { displayError(error); } div.querySelector("select").disabled = false; } await chrome.storage.local.set({emojis}); default_animation_style_select.disabled = false; } }; importbtn.onclick = async () => { try { var [fileHandle] = await showOpenFilePicker({ id: "import", types: [ { description: "vrcem export file", accept: { "application/json": ".json", } } ] }); if (!fileHandle) return; var file = await fileHandle.getFile(); var text = await file.text(); var store = JSON.parse(text); var set = {}; for (let key in store) { if (key.startsWith("data-")) { set[key] = store[key]; } } var {emojis} = chrome.storage.local.get("emojis"); emojis ||= []; store.emojis.forEach(emoji => { var existing = emojis.find(e => e.internalId == emoji.internalId); if (existing) Object.apply(existing, emoji); else emojis.push(emoji); }); set.emojis = emojis; await chrome.storage.local.set(set); location.reload(); } catch(error) { displayError(error); } }; exportbtn.onclick = async () => { try { var store = await chrome.storage.local.get(); store = JSON.stringify(store); var blob = new Blob([store],{type: "application/json"}); var url = URL.createObjectURL(blob); var a = document.createElement("a"); a.href = url; a.download = `vrcem-${new Date().toISOString()}.json`; a.click(); URL.revokeObjectURL(url); } catch (error) { displayError(error); } }; clearbtn.onclick = async () => { try { if (!confirm("Remove all emojis from storage. Are you SURE?????")) return; await chrome.storage.local.clear(); location.reload(); } catch (error) { displayError(error); } }; onfocus = () => loadToggleState().catch(displayError); loadEmojis().catch(displayError); async function loadEmojis() { errorDiv.innerText = ""; var store = await chrome.storage.local.get(); if (!store.emojis) throw new Error("No emoji."); for (let emoji of store.emojis) { createEmojiSquare(emoji, store["data-" + emoji.internalId]); } await loadToggleState(); } function createEmojiSquare(emoji, url) { let div = document.createElement("div"); div.className = "emojisquare"; div.dataset.internalId = emoji.internalId; if (emoji.currentId) div.dataset.currentId = emoji.currentId; div.onclick = async function onEmojiClick(event) { if (this.dataset.state == "pending") return; var selectedState = this.dataset.state; this.dataset.state = "pending"; div.querySelector("select").disabled = true; errorDiv.innerText = ""; if (deleteMode) { let {emojis} = await chrome.storage.local.get("emojis"); emojis = emojis.filter(e => e.internalId != this.dataset.internalId); await chrome.storage.local.set({emojis}); this.remove(); if (this.dataset.currentId) callContentScript("deleteEmoji", this.dataset.currentId); return; } if (emojigrid.querySelector(".emojisquare:not([data-state])")) { try { await loadToggleState(); } catch (error) { displayError(error); this.dataset.state = selectedState; return; } } if (selectedState == "enabled") { // disable callContentScript("deleteEmoji", this.dataset.currentId).then(() => { this.dataset.state = "disabled"; div.querySelector("select").disabled = false; }).catch(error => { this.dataset.state = selectedState; div.querySelector("select").disabled = false; displayError(error); }); return; } // enable callContentScript("createEmoji", this.querySelector("img").src, this.querySelector("select").value || default_animation_style_select.value).then(newEmoji => { chrome.storage.local.get("emojis").then(({emojis}) => { emojis.find(e => e.internalId == this.dataset.internalId).currentId = newEmoji.id; chrome.storage.local.set({emojis}); this.dataset.currentId = newEmoji.id; this.dataset.state = "enabled"; div.querySelector("select").disabled = false; }); }).catch(error => { this.dataset.state = selectedState; div.querySelector("select").disabled = false; displayError(error); }); }; let imgdiv = document.createElement("div"); imgdiv.className = "imgcontainer"; let img = document.createElement("img"); img.src = url; imgdiv.appendChild(img); div.appendChild(imgdiv); let select = createAnimationStyleSelect(); select.value = emoji.animationStyle || ''; select.onclick = event => event.stopPropagation(); select.onchange = async event => { var {emojis} = await chrome.storage.local.get("emojis"); var e = emojis.find(e => e.internalId == div.dataset.internalId); e.animationStyle = event.target.value; await chrome.storage.local.set({emojis}); if (div.dataset.state == "enabled") { //recreate div.dataset.state = "pending"; select.disabled = true; callContentScript("deleteEmoji", div.dataset.currentId).finally(() => { callContentScript("createEmoji", img.src, select.value || default_animation_style_select.value).then(newEmoji => { chrome.storage.local.get("emojis").then(({emojis}) => { emojis.find(e => e.internalId == div.dataset.internalId).currentId = newEmoji.id; chrome.storage.local.set({emojis}); div.dataset.currentId = newEmoji.id; div.dataset.state = "enabled"; select.disabled = false; }); }).catch(error => { displayError(error); }); }).catch(error => { displayError(error); }); } }; div.appendChild(select); emojigrid.appendChild(div); return div; } async function loadToggleState() { console.debug("loadToggleState"); var elements = document.querySelectorAll(".emojisquare"); if (elements.length === 0) return; var currentEmojis = await callContentScript("getEmojis"); var active = currentEmojis?.map(e => e.id); elements.forEach(e => { e.dataset.state = active.includes(e.dataset.currentId) ? "enabled" : "disabled"; }); errorDiv.innerText = ""; } function displayError(error) { console.error(error); error = error.message || String(error); var html = error; if (error.includes("VRChat tab not found.")) html += `
Please open https://vrchat.com/home`; if (error.includes("Missing Credentials")) html += `
Please log in to VRChat.com`; if (error.includes("No emoji.")) html += ` Click "Add Emojis" to get started.`; if (error.includes("Could not establish connection. Receiving end does not exist.")) html += `
Please reload your VRChat tab.`; errorDiv.innerHTML = html; } async function getVRchatTab() { var tab = (await chrome.tabs.query({url:"https://vrchat.com/home*"}))?.[0]; if (!tab) throw new Error("VRChat tab not found."); return tab; } async function callContentScript(method, ...args) { var tab = await getVRchatTab(); var response = await chrome.tabs.sendMessage(tab.id, [method, ...args]); console.debug(response); if (response.error) throw response.error; else return response; }; function loadImage(src) { return new Promise((resolve, reject) => { var image = new Image(); image.onload = () => resolve(image); image.onerror = reject; image.src = src; }); } function fileToDataURL(file) { return new Promise((resolve, reject) => { let reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { resolve(reader.result); }; reader.onerror = reject; }); } function randomId() { let id = ""; const CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < 16; i++) { id += CHARS[Math.floor(Math.random() * CHARS.length)]; } return id; }