469 lines
12 KiB
JavaScript
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;
|
|
} |