vrchat-emoji-manager/manage.js

469 lines
12 KiB
JavaScript

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 += `<br>Please open <a href=\"https://vrchat.com/home\" target=\"_blank\">https://vrchat.com/home</a>`;
if (error.includes("Missing Credentials"))
html += `<br>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 += `<br>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;
}