Compare commits

..

15 Commits

Author SHA1 Message Date
a74163b29a Merge branch 'wip' of gitea.moe:lamp/vrchat-emoji-manager into wip 2024-10-09 21:12:31 -07:00
349c7bd551 keep in sync with db 2024-10-09 21:12:25 -07:00
779624a722 add nsfw hint 2024-10-09 17:38:15 -07:00
a31550e205 fix auto import 2024-10-09 17:34:11 -07:00
d8167a3a3d fix delete 2024-10-09 17:33:58 -07:00
4ed59e5b66 fix db backcompat upgrade 2024-10-09 17:18:49 -07:00
0a6a6df29b display ratelimit seconds 2024-10-09 16:45:06 -07:00
2f76dd3df2 stickers 2024-10-09 16:14:42 -07:00
15198ca301 version 2024-10-08 21:45:17 -07:00
f19ce27f2b backward compat 2024-10-08 20:27:11 -07:00
9f020332cc arrange code 2024-10-08 17:45:55 -07:00
5f1b0db47c consolidate redundant enable/disable code 2024-10-08 15:32:31 -07:00
ebed204c9d zip import use addFiles 2024-10-08 14:49:00 -07:00
9ec6b8d9cd import export zip 2024-10-08 02:14:56 -07:00
c5c47c111b basic indexeddb working 2024-10-08 00:41:57 -07:00
7 changed files with 431 additions and 300 deletions

View File

@ -1,50 +1,29 @@
import { getDb, getKnownEmojiIds, getKnownStickerIds, storeImage } from "./db.js";
getDb();
var functions = { var functions = {
async storeEmojis(emojis) { async storeFiles(files, type) {
console.debug("storeEmojis", emojis); console.debug("storeFiles", files, type);
var emojiStore = (await chrome.storage.local.get(["emojis"])).emojis || []; var knownIds = type == "sticker" ? await getKnownStickerIds() : await getKnownEmojiIds();
console.debug("knownIds", knownIds);
loop1: for (let emoji of emojis) { for (let file of files) {
for (let existingEmoji of emojiStore) { if (knownIds.includes(file.id)) continue;
if (existingEmoji.currentId == emoji.id) continue loop1; console.log("store", file.id);
await storeImage({
date: file.versions[1].created_at,
currentId_emoji: file.tags.includes("emoji") ? file.id : null,
currentId_sticker: file.tags.includes("sticker") ? file.id : null,
internalId: file.id,
animationStyle: file.animationStyle,
data: await fetch(file.versions[1].file.url).then(res => res.blob())
});
} }
console.log("store", emoji.id);
var blob = await fetch(emoji.versions[1].file.url).then(res => res.blob());
var dataString = await blobToDataURL(blob);
var emojiDoc = {
date: emoji.versions[1].created_at,
currentId: emoji.id,
internalId: emoji.id,
animationStyle: emoji.animationStyle
};
await chrome.storage.local.set({[`data-${emojiDoc.internalId}`]: dataString});
emojiStore.push(emojiDoc);
}
chrome.storage.local.set({emojis: emojiStore});
} }
}; };
chrome.runtime.onMessage.addListener(function([method, ...args], sender, sendResponse) { chrome.runtime.onMessage.addListener(function([method, ...args], sender, sendResponse) {
console.debug(arguments); console.debug(arguments);
functions[method]?.apply(null, args).then(sendResponse).catch(error => sendResponse({error: error.toString()}));; functions[method].apply(null, args).then(sendResponse);
return true; return true;
}); });
function blobToDataURL(blob) {
return new Promise((resolve, reject) => {
var reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
});
}

View File

@ -1,32 +1,52 @@
fetch("https://vrchat.com/api/1/files?tag=emoji&n=100&offset=0").then(res => res.json()).then(data => {
if (!data.error)
chrome.runtime.sendMessage(["storeEmojis", data]);
});
var functions = { var functions = {
async getEmojis() { async getFiles(tag = "emoji") {
return await fetch("https://vrchat.com/api/1/files?tag=emoji&n=100&offset=0").then(res => res.json()); var res = await fetch(`https://vrchat.com/api/1/files?tag=${tag}&n=100&offset=0`);
return {
status: res.status,
body: await res.json(),
retryAfter: res.headers.get("Retry-After") || undefined
};
}, },
async deleteEmoji(id) { async deleteFile(id) {
return await fetch(`https://vrchat.com/api/1/file/${id}`, {method: "DELETE"}).then(res => res.json()); var res = await fetch(`https://vrchat.com/api/1/file/${id}`, {method: "DELETE"});
return {
status: res.status,
body: await res.json(),
retryAfter: res.headers.get("Retry-After") || undefined
};
}, },
async createEmoji(url, animationStyle) { async createFile(url, tag = "emoji", animationStyle) {
var blob = await fetch(url).then(res => res.blob()); var blob = await fetch(url).then(res => res.blob());
var form = new FormData(); var form = new FormData();
form.append("tag", "emoji"); form.append("tag", tag);
form.append("animationStyle", animationStyle); if (tag == "emoji") form.append("animationStyle", animationStyle);
form.append("maskTag", "square"); form.append("maskTag", "square");
form.append("file", blob); form.append("file", blob);
return await fetch("https://vrchat.com/api/1/file/image", { var res = await fetch("https://vrchat.com/api/1/file/image", {
method: "POST", method: "POST",
body: form body: form
}).then(res => res.json()); });
return {
status: res.status,
body: await res.json(),
retryAfter: res.headers.get("Retry-After") || undefined
};
} }
}; };
functions.getFiles("emoji").then(data => {
if (!data.error) chrome.runtime.sendMessage(["storeFiles", data.body, "emoji"]);
else console.error(data.error);
});
functions.getFiles("sticker").then(data => {
if (!data.error) chrome.runtime.sendMessage(["storeFiles", data.body, "sticker"]);
else console.error(data.error);
});
chrome.runtime.onMessage.addListener(function([method, ...args], sender, sendResponse) { chrome.runtime.onMessage.addListener(function([method, ...args], sender, sendResponse) {
console.debug(arguments); console.debug(arguments);
functions[method]?.apply(null, args).then(sendResponse).catch(error => sendResponse({error: error.toString()})); functions[method].apply(null, args)
.then(response => sendResponse({response}))
.catch(error => sendResponse({error: error.toString()}));
return true; return true;
}); });

139
db.js Normal file
View File

@ -0,0 +1,139 @@
var db;
export function getDb() {
return db ||= initDb();
}
function initDb() {
return new Promise((resolve, reject) => {
var request = indexedDB.open("database");
request.onerror = () => reject(request.error);
request.onsuccess = event => {
console.debug("onsuccess");
resolve(event.target.result);
};
request.onupgradeneeded = event => {
console.debug("onupgradeneeded");
//request.onsuccess = null;
var db = event.target.result;
var objectStore = db.createObjectStore("images", { keyPath: "internalId", autoIncrement: true });
objectStore.createIndex("currentId_emoji", "currentId_emoji", {unique: true});
objectStore.createIndex("currentId_sticker", "currentId_sticker", {unique: true});
db.createObjectStore("settings");
chrome.storage.local.get().then(async storage => {
console.debug("storage", storage);
for (let emoji of storage.emojis) {
try {
emoji.data = await fetch(storage[`data-${emoji.internalId}`]).then(res => res.blob());
await storeImage(emoji);
} catch (error) {
console.error(error);
}
}
await setSetting("defaultAnimationStyle", storage.defaultAnimationStyle);
//resolve(db);
console.debug("finished upgrade");
});
};
request.onversionchange = () => {
db = undefined; // idk
};
});
}
export async function getKnownEmojiIds() {
var db = await getDb();
return await new Promise((resolve, reject) => {
var request = db.transaction(["images"], "readonly").objectStore("images").index("currentId_emoji").getAllKeys();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
export async function getKnownStickerIds() {
var db = await getDb();
return await new Promise((resolve, reject) => {
var request = db.transaction(["images"], "readonly").objectStore("images").index("currentId_sticker").getAllKeys();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
export async function getAllImages() {
var db = await getDb();
return await new Promise((resolve, reject) => {
var request = db.transaction(["images"], "readonly").objectStore("images").getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
export async function storeImage(entry) {
var db = await getDb();
return await new Promise((resolve, reject) => {
var request = db.transaction(["images"], "readwrite").objectStore("images").add(entry);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
export async function getImage(internalId) {
var db = await getDb();
return await new Promise((resolve, reject) => {
var request = db.transaction(["images"], "readonly").objectStore("images").get(internalId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
export async function updateImage(internalId, update) {
var db = await getDb();
return await new Promise((resolve, reject) => {
var objectStore = db.transaction(["images"], "readwrite").objectStore("images");
var request = objectStore.get(internalId);
request.onsuccess = event => {
var request = objectStore.put(Object.assign(event.target.result, update));
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
};
request.onerror = () => reject(request.error);
});
}
export async function deleteImage(internalId) {
var db = await getDb();
return await new Promise((resolve, reject) => {
var request = db.transaction(["images"], "readwrite").objectStore("images").delete(internalId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
export async function deleteAllImages() {
var db = await getDb();
return await new Promise((resolve, reject) => {
var request = db.transaction(["images"], "readwrite").objectStore("images").clear();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
export async function setSetting(key, value) {
var db = await getDb();
return await new Promise((resolve, reject) => {
var request = db.transaction(["settings"], "readwrite").objectStore("settings").put(value, key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
export async function getSetting(key) {
var db = await getDb();
return await new Promise((resolve, reject) => {
var request = db.transaction(["settings"], "readonly").objectStore("settings").get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}

13
dep/jszip.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
<!DOCTYPE html><html><head> <!DOCTYPE html><html><head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>VRChat Emoji Manager</title> <title>VRChat Emoji and Sticker Manager</title>
<style> <style>
@ -67,18 +67,20 @@
flex-grow: 1; flex-grow: 1;
} }
</style> </style>
<style id="dynamicstyle"></style>
</head><body> </head><body>
<div id="emojigrid"></div> <div id="emojigrid"></div>
<div id="bottompanel"> <div id="bottompanel">
<div id="errorDiv"></div> <div id="errorDiv"></div>
<div>Default Animation Style: <select id="default_animation_style_select"><option value="aura">Aura</option><option value="bats">Bats</option><option value="bees">Bees</option><option value="bounce">Bounce</option><option value="cloud">Cloud</option><option value="confetti">Confetti</option><option value="crying">Crying</option><option value="dislike">Dislike</option><option value="fire">Fire</option><option value="idea">Idea</option><option value="lasers">Lasers</option><option value="like">Like</option><option value="magnet">Magnet</option><option value="mistletoe">Mistletoe</option><option value="money">Money</option><option value="noise">Noise</option><option value="orbit">Orbit</option><option value="pizza">Pizza</option><option value="rain">Rain</option><option value="rotate">Rotate</option><option value="shake">Shake</option><option value="snow">Snow</option><option value="snowball">Snowball</option><option value="spin">Spin</option><option value="splash">Splash</option><option value="stop">Stop</option><option value="zzz">ZZZ</option></select></div> <div class="emojistuff">Default Animation Style: <select id="default_animation_style_select"><option value="aura">Aura</option><option value="bats">Bats</option><option value="bees">Bees</option><option value="bounce">Bounce</option><option value="cloud">Cloud</option><option value="confetti">Confetti</option><option value="crying">Crying</option><option value="dislike">Dislike</option><option value="fire">Fire</option><option value="idea">Idea</option><option value="lasers">Lasers</option><option value="like">Like</option><option value="magnet">Magnet</option><option value="mistletoe">Mistletoe</option><option value="money">Money</option><option value="noise">Noise</option><option value="orbit">Orbit</option><option value="pizza">Pizza</option><option value="rain">Rain</option><option value="rotate">Rotate</option><option value="shake">Shake</option><option value="snow">Snow</option><option value="snowball">Snowball</option><option value="spin">Spin</option><option value="splash">Splash</option><option value="stop">Stop</option><option value="zzz">ZZZ</option></select></div>
<div><button id="addbtn">Add Emojis</button></div> <div><button id="modebtn">Sticker Mode</button></div>
<div><button id="deletebtn">Delete Emojis</button></div> <div><button id="addbtn">Add</button></div>
<div><button id="deletebtn">Delete</button></div>
<div><button id="importbtn">Import</button></div> <div><button id="importbtn">Import</button></div>
<div><button id="exportbtn">Export</button></div> <div><button id="exportbtn">Export</button></div>
<div><button id="clearbtn">Clear</button></div> <div><button id="clearbtn">Clear</button></div>
</div> </div>
<script src="dep/jszip.min.js"></script>
<script src="manage.js"></script> <script type="module" src="manage.js"></script>
</body></html> </body></html>

428
manage.js
View File

@ -1,34 +1,36 @@
const ANIMATION_STYLES = `default import * as db from "../db.js";
Aura
Bats var mode = localStorage.mode ||= "emoji";
Bees if (mode == "sticker") stickerMode();
Bounce
Cloud function stickerMode() {
Confetti localStorage.mode = mode = "sticker";
Crying modebtn.innerText = "Emoji Mode";
Dislike dynamicstyle.innerHTML = `.emojistuff {display: none}`;
Fire }
Idea function emojiMode() {
Lasers localStorage.mode = mode = "emoji";
Like modebtn.innerText = "Sticker Mode";
Magnet dynamicstyle.innerHTML = ``;
Mistletoe }
Money
Noise modebtn.onclick = function switchMode() {
Orbit if (mode == "emoji") {
Pizza stickerMode();
Rain } else {
Rotate emojiMode();
Shake }
Snow loadToggleState();
Snowball }
Spin
Splash
Stop
ZZZ`.split('\n'); 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(' ');
const animationStyleRegex = new RegExp(`(${ANIMATION_STYLES.join('|')})`, 'i');
function createAnimationStyleSelect() { function createAnimationStyleSelect() {
let select = document.createElement("select"); let select = document.createElement("select");
select.classList.add("emojistuff");
ANIMATION_STYLES.forEach(a => { ANIMATION_STYLES.forEach(a => {
let option = document.createElement("option"); let option = document.createElement("option");
option.innerText = a; option.innerText = a;
@ -43,24 +45,34 @@ function createAnimationStyleSelect() {
} }
db.getSetting("defaultAnimationStyle").then(defaultAnimationStyle => {
default_animation_style_select.value = defaultAnimationStyle || "aura";
});
default_animation_style_select.onchange = async function () {
await db.setSetting("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;
await Promise.all(enabled.map(div => (async ()=>{
await div.disable();
await div.enable();
})()));
default_animation_style_select.disabled = false;
}
};
addbtn.onclick = async () => { addbtn.onclick = async () => {
try {
var fileHandles = await showOpenFilePicker({ var fileHandles = await showOpenFilePicker({
id: "add", id: "add",
multiple: true, multiple: true,
types: [ types: [ { description: "Image", accept: { "image/*": [], } } ]
{
description: "Any image supported by browser",
accept: {
"image/*": [],
}
}
]
}); });
} catch (error) {
return;
}
if (!fileHandles.length) return; if (!fileHandles.length) return;
var files = []; var files = [];
for (let fileHandle of fileHandles) { for (let fileHandle of fileHandles) {
@ -73,13 +85,11 @@ addbtn.onclick = async () => {
await addFiles(files); await addFiles(files);
}; };
document.body.onpaste = event => { document.body.onpaste = event => {
if (event.clipboardData.files.length) { if (event.clipboardData.files.length) {
addFiles(event.clipboardData.files); addFiles(event.clipboardData.files);
} }
}; };
document.body.ondragover = event => event.preventDefault(); document.body.ondragover = event => event.preventDefault();
document.body.ondrop = event => { document.body.ondrop = event => {
event.preventDefault(); event.preventDefault();
@ -88,9 +98,7 @@ document.body.ondrop = event => {
} }
}; };
async function addFiles(files) { async function addFiles(files) {
var newEmojis = [];
var errors = []; var errors = [];
for (let file of files) { for (let file of files) {
try { try {
@ -116,15 +124,16 @@ async function addFiles(files) {
canvas.height = largestDim; canvas.height = largestDim;
let ctx = canvas.getContext("2d"); let ctx = canvas.getContext("2d");
ctx.drawImage(image, offsetX, offsetY, width, height); ctx.drawImage(image, offsetX, offsetY, width, height);
let data = canvas.toDataURL(); let data = await new Promise(r => canvas.toBlob(r, "image/png"));
URL.revokeObjectURL(image.src); URL.revokeObjectURL(image.src);
let e = { let e = {
internalId: randomId(), internalId: randomId(),
date: new Date().toISOString() date: new Date().toISOString(),
animationStyle: file.name.match(animationStyleRegex)?.[1] || undefined,
data
}; };
await chrome.storage.local.set({["data-"+e.internalId]: data}); await db.storeImage(e);
newEmojis.push(e); createEmojiSquare(e);
createEmojiSquare(e, data);
errorDiv.innerText = ""; errorDiv.innerText = "";
} catch(error) { } catch(error) {
console.error(error); console.error(error);
@ -134,10 +143,7 @@ async function addFiles(files) {
if (errors.length) { if (errors.length) {
displayError(`Errors occured adding ${errors.length} files: ${errors.join(', ')}`); displayError(`Errors occured adding ${errors.length} files: ${errors.join(', ')}`);
} }
var {emojis} = await chrome.storage.local.get("emojis"); loadToggleState();
emojis ||= [];
emojis = emojis.concat(newEmojis);
await chrome.storage.local.set({emojis});
} }
@ -149,7 +155,7 @@ deletebtn.onclick = () => {
if (deleteMode) { if (deleteMode) {
deleteMode = false; deleteMode = false;
document.body.classList.remove("deletemode"); document.body.classList.remove("deletemode");
deletebtn.innerText = "Delete Emojis"; deletebtn.innerText = "Delete";
return; return;
} }
deleteMode = true; deleteMode = true;
@ -157,71 +163,35 @@ deletebtn.onclick = () => {
deletebtn.innerText = "Stop Delete"; 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 () => { importbtn.onclick = async () => {
try {
try { try {
var [fileHandle] = await showOpenFilePicker({ var [fileHandle] = await showOpenFilePicker({
id: "import", id: "import",
types: [ types: [ { description: "ZIP or vrcem json", accept: { "application/zip": ".zip", "application/json": ".json" } } ]
{
description: "vrcem export file",
accept: {
"application/json": ".json",
}
}
]
}); });
} catch (error) {
return;
}
if (!fileHandle) return; if (!fileHandle) return;
var file = await fileHandle.getFile(); var file = await fileHandle.getFile();
var text = await file.text(); if (file.name.toLowerCase().endsWith(".json")) {
var store = JSON.parse(text); let text = await file.text();
var set = {}; let store = JSON.parse(text);
for (let key in store) { var pngs = await Promise.all(store.emojis.map(async emoji => {
if (key.startsWith("data-")) { var blob = await fetch(store[`data-${emoji.internalId}`]).then(res => res.blob());
set[key] = store[key]; return new File([blob], `${emoji.animationStyle}.png`);
}));
} else {
var zip = await JSZip.loadAsync(file);
var pngs = zip.file(/^[^\/]*\.png$/i);
pngs = await Promise.all(pngs.map(async png => {
var blob = await png.async("blob");
return new File([blob], png.name);
}));
} }
} await addFiles(pngs);
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) { } catch(error) {
displayError(error); displayError(error);
} }
@ -229,13 +199,16 @@ importbtn.onclick = async () => {
exportbtn.onclick = async () => { exportbtn.onclick = async () => {
try { try {
var store = await chrome.storage.local.get(); var images = await db.getAllImages();
store = JSON.stringify(store); var zip = new JSZip();
var blob = new Blob([store],{type: "application/json"}); for (let image of images) {
zip.file(`${image.internalId}.${image.animationStyle}.png`, image.data);
}
var blob = await zip.generateAsync({type: "blob"});
var url = URL.createObjectURL(blob); var url = URL.createObjectURL(blob);
var a = document.createElement("a"); var a = document.createElement("a");
a.href = url; a.href = url;
a.download = `vrcem-${new Date().toISOString()}.json`; a.download = `vrcem-${new Date().toISOString()}.zip`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} catch (error) { } catch (error) {
@ -244,130 +217,137 @@ exportbtn.onclick = async () => {
}; };
clearbtn.onclick = async () => { clearbtn.onclick = async () => {
try { if (!confirm("Delete all images?????")) return;
if (!confirm("Remove all emojis from storage. Are you SURE?????")) return; await db.deleteAllImages();
await chrome.storage.local.clear(); emojigrid.innerHTML = '';
location.reload();
} catch (error) {
displayError(error);
}
}; };
onfocus = () => loadToggleState().catch(displayError);
(async function loadEmojis() {
loadEmojis().catch(displayError);
async function loadEmojis() {
errorDiv.innerText = ""; errorDiv.innerText = "";
var store = await chrome.storage.local.get(); var images = await db.getAllImages();
if (!store.emojis) throw new Error("No emoji."); if (!images.length) throw new Error("No emoji.");
for (let emoji of store.emojis) { for (let image of images) {
createEmojiSquare(emoji, store["data-" + emoji.internalId]); createEmojiSquare(image);
} }
await loadToggleState(); await loadToggleState();
})().catch(displayError);
async function refreshImages() {
console.debug("refreshImages");
var images = await db.getAllImages();
for (let image of images) {
if (!document.querySelector(`[data-internal-id="${image.internalId}"]`)) createEmojiSquare(image);
}
var ids = images.map(i => i.internalId);
for (let emojisquare of document.querySelectorAll(".emojisquare")) {
if (!ids.includes(emojisquare.dataset.internalId)) emojisquare.delete();
}
} }
async function loadToggleState() {
function createEmojiSquare(emoji, url) { console.debug("loadToggleState");
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 { try {
await loadToggleState(); var elements = document.querySelectorAll(".emojisquare");
if (elements.length === 0) return;
var currentFiles = await callContentScript("getFiles", mode);
var active = currentFiles?.map(e => e.id);
elements.forEach(e => {
e.dataset.state = active.includes(e.dataset["currentId_"+mode]) ? "enabled" : "disabled";
});
errorDiv.innerText = "";
} catch (error) { } catch (error) {
displayError(error); displayError(error);
this.dataset.state = selectedState; }
}
onfocus = () => refreshImages().then(loadToggleState);
function createEmojiSquare({internalId, data, animationStyle, currentId_emoji, currentId_sticker}) {
var div = document.createElement("div");
div.className = "emojisquare";
div.dataset.internalId = internalId;
div.blob = data;
if (currentId_emoji) div.dataset.currentId_emoji = currentId_emoji;
if (currentId_sticker) div.dataset.currentId_sticker = currentId_sticker;
Object.assign(div, {
async enable() {
var lastState = this.dataset.state;
this.dataset.state = "pending";
select.disabled = true;
errorDiv.innerText = "";
try {
this.dataurl ||= await blobToDataURL(this.blob);
var newEmoji = await callContentScript("createFile", this.dataurl, mode, select.value || default_animation_style_select.value);
} catch (error) {
this.dataset.state = lastState;
select.disabled = false;
displayError(error);
return; return;
} }
await db.updateImage(this.dataset.internalId, {["currentId_"+mode]: newEmoji.id});
this.dataset["currentId_"+mode] = newEmoji.id;
this.dataset.state = "enabled";
select.disabled = false;
},
async disable() {
var lastState = this.dataset.state;
this.dataset.state = "pending";
select.disabled = true;
errorDiv.innerText = "";
try {
await callContentScript("deleteFile", this.dataset["currentId_"+mode]);
await db.updateImage(this.dataset.internalId, {["currentId_"+mode]: null});
delete this.dataset["currentId_"+mode];
} catch (error) {
this.dataset.state = lastState;
select.disabled = false;
displayError(error);
return;
} }
if (selectedState == "enabled") {
// disable
callContentScript("deleteEmoji", this.dataset.currentId).then(() => {
this.dataset.state = "disabled"; this.dataset.state = "disabled";
div.querySelector("select").disabled = false; div.querySelector("select").disabled = false;
}).catch(error => { },
this.dataset.state = selectedState; async toggle() {
div.querySelector("select").disabled = false; if (this.dataset.state == "enabled") {
displayError(error); await this.disable();
}); } else if (this.dataset.state == "disabled") {
return; await this.enable();
} }
},
async delete() {
await db.deleteImage(this.dataset.internalId);
if (this.dataset["currentId_emoji"]) callContentScript("deleteFile", this.dataset["currentId_emoji"]).catch(displayError);
if (this.dataset["currentId_sticker"]) callContentScript("deleteFile", this.dataset["currentId_sticker"]).catch(displayError);
this.remove();
}
});
// enable
callContentScript("createEmoji", this.querySelector("img").src, this.querySelector("select").value || default_animation_style_select.value).then(newEmoji => { div.onclick = function onEmojiClick() {
chrome.storage.local.get("emojis").then(({emojis}) => { if (deleteMode) this.delete();
emojis.find(e => e.internalId == this.dataset.internalId).currentId = newEmoji.id; else this.toggle();
chrome.storage.local.set({emojis}); if (emojigrid.querySelector(".emojisquare:not([data-state])")) loadToggleState();
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"); var imgdiv = document.createElement("div");
imgdiv.className = "imgcontainer"; imgdiv.className = "imgcontainer";
let img = document.createElement("img"); var img = document.createElement("img");
img.src = url; img.src = URL.createObjectURL(data);
imgdiv.appendChild(img); imgdiv.appendChild(img);
div.appendChild(imgdiv); div.appendChild(imgdiv);
let select = createAnimationStyleSelect(); var select = createAnimationStyleSelect();
select.value = emoji.animationStyle || ''; select.value = animationStyle || '';
select.onclick = event => event.stopPropagation(); select.onclick = event => event.stopPropagation();
select.onchange = async event => { select.onchange = async event => {
var {emojis} = await chrome.storage.local.get("emojis"); await db.updateImage(div.dataset.internalId, {animationStyle: event.target.value});
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") { if (div.dataset.state == "enabled") {
//recreate await div.disable();
div.dataset.state = "pending"; await div.enable();
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); div.appendChild(select);
@ -377,17 +357,7 @@ function createEmojiSquare(emoji, url) {
} }
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) { function displayError(error) {
console.error(error); console.error(error);
@ -401,6 +371,8 @@ function displayError(error) {
html += ` Click "Add Emojis" to get started.`; html += ` Click "Add Emojis" to get started.`;
if (error.includes("Could not establish connection. Receiving end does not exist.")) if (error.includes("Could not establish connection. Receiving end does not exist."))
html += `<br>Please reload your VRChat tab.`; html += `<br>Please reload your VRChat tab.`;
if (error.includes("You must upload a valid imageǃ"))
html += `<br> (NSFW images are not allowed)`;
errorDiv.innerHTML = html; errorDiv.innerHTML = html;
} }
@ -412,10 +384,16 @@ async function getVRchatTab() {
async function callContentScript(method, ...args) { async function callContentScript(method, ...args) {
var tab = await getVRchatTab(); var tab = await getVRchatTab();
var response = await chrome.tabs.sendMessage(tab.id, [method, ...args]); var {error, response} = await chrome.tabs.sendMessage(tab.id, [method, ...args]);
if (error) throw error;
console.debug(response); console.debug(response);
if (response.error) throw response.error; if (response.status == 429) {
else return response; throw new Error(`VRChat API ratelimited. Please wait ${response.retryAfter} seconds.`);
}
if (response.body.error) {
throw new Error(`VRChat API said: ${response.body.error.message}`);
}
return response.body;
}; };
@ -435,10 +413,10 @@ function loadImage(src) {
}); });
} }
function fileToDataURL(file) { function blobToDataURL(blob) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let reader = new FileReader(); let reader = new FileReader();
reader.readAsDataURL(file); reader.readAsDataURL(blob);
reader.onload = () => { reader.onload = () => {
resolve(reader.result); resolve(reader.result);
}; };

View File

@ -1,8 +1,8 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "VRChat Emoji Manager", "name": "VRChat Emoji and Sticker Manager",
"version": "1.2024.10.7", "version": "1.2024.10.9",
"description": "Store more than 9 emoji, toggle them on and off and change their animation styles.", "description": "Store more than 9 emoji or stickers, toggle them on and off and change their animation styles.",
"homepage_url": "https://gitea.moe/lamp/vrchat-emoji-manager", "homepage_url": "https://gitea.moe/lamp/vrchat-emoji-manager",
"icons": { "icons": {
"128": "icon128.png" "128": "icon128.png"