// ==UserScript==
// @name Chants for NovelAI
// @namespace https://www6.notion.site/dc99953d5f04405c893fba95dace0722
// @version 9.4
// @description Chants script for NovelAI
// @author SenY
// @match https://novelai.net/image
// @icon https://www.google.com/s2/favicons?sz=64&domain=novelai.net
// @grant none
// @license MIT
// ==/UserScript==
(function () {
const colors = {
"-1": ["red", "maroon"],
"0": ["lightblue", "dodgerblue"],
"1": ["gold", "goldenrod"],
"3": ["violet", "darkorchid"],
"4": ["lightgreen", "darkgreen"],
"5": ["tomato", "darksalmon"],
"6": ["red", "maroon"],
"7": ["whitesmoke", "black"],
"8": ["seagreen", "darkseagreen"]
}
const getChantURL = function (force) {
if (force === true) {
localStorage.removeItem("chantURL");
}
let chantURL = localStorage.getItem("chantURL") || prompt("Input your chants json url.\nThe URL must be a Cors-enabled server (e.g., gist.github.com).", "https://gist.githubusercontent.com/vkff5833/989808aadebf8648831955cdf2a7b3e3/raw/yuuri.json");
if (chantURL) {
localStorage.setItem("chantURL", chantURL);
}
return chantURL;
}
let chantURL = getChantURL(null);
let ui = document.createElement("div");
ui.style.padding = "0.5rem";
let seedButton;
const saveJpeg = function () {
let seed = Array.from(seedButton.querySelectorAll("span")).map(x => x.textContent).find(x => x.search(/^[0-9]*$/) == 0);
let filename = seed + ".jpg";
let url = document.querySelector('img[src]').getAttribute("src");
let blob = fetch(url).then(r => r.blob()).then(blob => {
let fileReader = new FileReader();
fileReader.onload = function () {
let dataURI = this.result;
let canvas = document.createElement("canvas");
let context = canvas.getContext("2d");
let image = new Image();
image.src = dataURI;
image.onload = function () {
canvas.width = image.width;
canvas.height = image.height;
context.drawImage(image, 0, 0);
let JPEG = canvas.toDataURL("image/jpeg", document.getElementById("jpegQuality").value - 0);
let link = document.createElement('a');
link.href = JPEG;
link.download = filename;
link.click();
};
}
fileReader.readAsDataURL(blob);
});
}
let jpegButton = document.createElement("div");
let jpegQuality = document.createElement("div");
jpegButton.textContent = "JPEG";
jpegButton.addEventListener("click", () => {
saveJpeg();
});
jpegQuality.textContent = "";
let input = document.createElement("input");
input.value = 0.85;
input.setAttribute("id", "jpegQuality");
input.setAttribute("type", "number");
input.setAttribute("step", "0.01");
input.setAttribute("max", "1");
input.setAttribute("min", "0");
input.style.maxWidth = "3rem";
jpegQuality.appendChild(input);
setInterval(() => {
document.querySelectorAll("textarea[placeholder]").forEach(el => {
if (el.offsetParent && !ui.offsetParent) {
el.parentElement.appendChild(ui);
}
});
seedButton = Array.from(document.querySelectorAll('span[style]')).find(x => x.textContent.trim().search(/^[0-9]*(seed|シード)/i) == 0);
if ( seedButton ) {
seedButton = seedButton.closest("button");
seedButton.classList.forEach(x => {
jpegButton.classList.add(x);
jpegQuality.classList.add(x);
});
seedButton.parentNode.insertBefore(jpegButton, seedButton);
seedButton.parentNode.insertBefore(jpegQuality, seedButton);
}
}, 500);
let allTags = [];
fetch("https://gist.githubusercontent.com/vkff5833/275ccf8fa51c2c4ba767e2fb9c653f9a/raw/danbooru.json", {
method: "GET",
}).then((response) => response.json())
.then((data) => {
allTags = data;
fetch("https://gist.githubusercontent.com/vkff5833/275ccf8fa51c2c4ba767e2fb9c653f9a/raw/danbooru_wiki.slim.json", {
method: "GET",
}).then((response) => response.json())
.then((wikiPages) => {
allTags = allTags.map(x => {
let wikiPage = wikiPages.find(y => y.name == x.name);
if (wikiPage) {
x.terms = x.terms.concat(wikiPage.otherNames);
}
return x;
});
});
});
let chants = [];
const getTargetTag = function () {
let textarea = document.querySelector("textarea[placeholder]");
if (textarea) {
let oldTags = textarea.value.split(",").map(x => x.trim());
let beforeTags = textarea.value.slice(0, textarea.selectionEnd).split(",").map(x => x.trim());
let targetTag = beforeTags[beforeTags.length - 2];
targetTag = oldTags[oldTags.indexOf(targetTag) + 1];
return targetTag.trim();
} else {
return null;
}
};
const Crease = function (value) {
const getTagPower = function (tag) {
let tagPower = 0;
tagPower += tag.split("").filter(x => x == "{").length;
tagPower -= tag.split("").filter(x => x == "[").length;
return tagPower;
}
let textarea = document.querySelector("textarea[placeholder]");
let selectionEnd = textarea.selectionEnd;
let oldTags = textarea.value.split(",").map(x => x.trim());
let targetTag = getTargetTag();
let tags = [];
if (targetTag) {
oldTags.forEach(tag => {
if (tag == targetTag) {
let tagPower = getTagPower(targetTag) + value;
tag = tag.replace(/[\{\}\[\]]/g, "", tag);
for (let i = 0; i < Math.abs(tagPower); i++) {
if (tagPower > 0) {
tag = '{' + tag + '}';
}
if (tagPower < 0) {
tag = '[' + tag + ']';
}
}
}
tags.push(tag);
});
tags = tags.filter(x => x);
textarea.textContent = tags.join(", ").replace(/^, /g, "");
textarea.value = tags.join(", ").replace(/^, /g, "") + ", ";
textarea._valueTracker = '';
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
textarea.selectionEnd = selectionEnd;
}
}
const Append = function (key, value) {
let targetTag = getTargetTag();
let text;
let newTags;
let textarea = document.querySelector("textarea[placeholder]");
let selectionEnd = textarea.selectionEnd;
let oldTags = textarea.value.split(",").map(x => x.trim());
if (key) {
let chant = chants.find(x => x.name == key.trim());
if (chant) {
newTags = chant.content.split(",").map(x => x.trim());
}
}
if (value) {
newTags = [value.trim()];
}
if (newTags.length) {
//let tags = oldTags.concat(newTags).filter(x => x);
let tags = [];
oldTags.forEach(tag => {
if (tag == targetTag) {
if (key) {
tags.push(tag);
}
newTags.forEach(newTag => {
tags.push(newTag);
});
} else {
tags.push(tag);
}
});
if (!targetTag) {
tags = tags.concat(newTags);
}
tags = Array.from(new Set(tags.filter(x => x)));
textarea.textContent = tags.join(", ").replace(/^, /g, "");
textarea.value = tags.join(", ").replace(/^, /g, "") + ", ";
// Special Thanks to https://fate.5ch.net/test/read.cgi/liveuranus/1702763225/730
textarea._valueTracker = '';
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
textarea.selectionEnd = selectionEnd;
}
}
const Suggest = function () {
let textarea = document.querySelector("textarea[placeholder]");
if (textarea) {
let tags = textarea.value.split(",").map(x => x.trim());
if (tags.length) {
let targetTag = getTargetTag().replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
document.getElementById("suggestionField").textContent = "";
let suggestions = [];
if (targetTag) {
suggestions = allTags.filter(x => x.name.search(targetTag.replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")) >= 0).slice(0, 10);
suggestions = suggestions.concat(allTags.filter(x => {
return x.terms.map(y => y.search(targetTag.replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")) >= 0).includes(true);
}).slice(0, 20));
}
let done = new Set();
suggestions.forEach(tag => {
if (!done.has(tag.name)) {
let button = document.createElement("button");
let count = tag.coumt;
if (count > 1000) {
count /= 1000;
count = Math.round(count * 10) / 10;
count += "k";
}
button.textContent = tag.name + " (" + count + ")";
button.style.color = colors[tag.category][1];
button.setAttribute("title", tag.terms.map(x => x.trim()).filter(x => x).join(", "));
button.addEventListener("click", function () {
Append(null, tag.name);
});
document.getElementById("suggestionField").appendChild(button);
if (textarea.value.split(",").map(y => y.trim().replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")).includes(tag.name.replace(/_/g, " "))) {
button.style.opacity = 0.5;
}
}
done.add(tag.name);
});
}
}
}
const Build = function () {
ui.textContent = "";
let hr = document.createElement("hr");
hr.style.color = "grey";
hr.style.borderWidth = "2px";
let optionField = document.createElement("div");
let chantsField = document.createElement("div");
let suggestionField = document.createElement("div");
suggestionField.setAttribute("id", "suggestionField");
ui.appendChild(optionField);
ui.appendChild(hr.cloneNode());
ui.appendChild(chantsField);
ui.appendChild(hr.cloneNode());
ui.appendChild(suggestionField);
let resetButton = document.createElement("button");
resetButton.textContent = "Reset Chants URL";
resetButton.style.color = "red";
resetButton.addEventListener("click", function () {
chantURL = getChantURL(true);
Build();
});
optionField.appendChild(resetButton);
let clearButton = document.createElement("button");
clearButton.textContent = "Clear Suggestion";
clearButton.style.color = "red";
clearButton.addEventListener("click", function () {
document.getElementById("suggestionField").textContent = "";
});
optionField.appendChild(clearButton);
fetch(chantURL, {
method: "GET",
}).then((response) => response.json())
.then((data) => {
chants = data;
chants.forEach(chant => {
let button = document.createElement("button");
button.textContent = chant.name;
button.style.color = colors[chant.color][1];
button.addEventListener("click", function () {
Append(chant.name, null);
});
chantsField.appendChild(button);
});
});
}
let init = false;
document.addEventListener("DOMContentLoaded", function () {
if (init === false) {
Build();
init = true;
}
});
document.addEventListener("keydown", function (e) {
if (e.ctrlKey == true && e.code == "ArrowUp") {
e.preventDefault();
Crease(1);
}
if (e.ctrlKey == true && e.code == "ArrowDown") {
e.preventDefault();
Crease(-1);
}
});
document.addEventListener("keyup", function (e) {
if (e.target.tagName == "TEXTAREA") {
Suggest();
}
});
document.addEventListener("click", function (e) {
if (e.target.tagName == "TEXTAREA") {
Suggest();
}
});
})();