Compare commits
15 Commits
321cc93cf5
...
a74163b29a
Author | SHA1 | Date | |
---|---|---|---|
a74163b29a | |||
349c7bd551 | |||
779624a722 | |||
a31550e205 | |||
d8167a3a3d | |||
4ed59e5b66 | |||
0a6a6df29b | |||
2f76dd3df2 | |||
15198ca301 | |||
f19ce27f2b | |||
9f020332cc | |||
5f1b0db47c | |||
ebed204c9d | |||
9ec6b8d9cd | |||
c5c47c111b |
@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
@ -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
139
db.js
Normal 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
13
dep/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
manage.html
14
manage.html
@ -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
428
manage.js
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user