// ==UserScript==
// @name CC98 Tools - Image Collections - GM
// @version 1.0.3
// @description 为CC98网页版添加收藏图片功能
// @icon https://www.cc98.org/static/98icon.ico
// @author ml98
// @namespace https://www.cc98.org/user/name/ml98
// @license MIT
// @match https://www.cc98.org/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
// Store
const db = GMStore();
// GM Store
function GMStore() {
async function add(image) {
const images = GM_getValue('images', []);
images.push(image);
GM_setValue('images', images);
}
async function get(tags) {
const images = GM_getValue('images', []);
return images.filter(image => tags.some(tag => image.tags.includes(tag)));
}
async function del(urls) {
const images = GM_getValue('images', []);
const new_images = images.filter(image => !urls.includes(image.url));
GM_setValue('images', new_images);
}
return { add, get, del };
}
// Components
const imagePicker = ImagePicker({
onSearch: async function (text) {
const images = await db.get(text.split(" "));
const result = images.map((image) => ({
src: image.url,
text: image.tags
.filter((tag) => tag !== "default_tag")
.join(" "),
}));
console.log("result", result);
return result;
},
onDelete: async function (urls) {
console.log("delete", urls);
await db.del(urls);
},
onOK: async function (urls) {
console.log("ok", urls);
putText(urls.map((url) => `[img]${url}[/img]\n`).join(""));
},
});
document.body.appendChild(imagePicker);
const tagsInput = TagsInput({
onSubmit: async function (text) {
const tags = ["default_tag", ...text.split(" ").filter(Boolean)];
console.log("save", tagsInput.imgSrc, "with tags", tags);
await db.add({ url: tagsInput.imgSrc, tags: tags });
},
});
document.body.appendChild(tagsInput);
function putText(text) {
const textarea = document.querySelector(".ubb-editor > textarea");
if (!textarea) return;
const setter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value"
).set;
setter.call(textarea, textarea.value + text);
textarea.dispatchEvent(new Event("input", { bubbles: true }));
}
function Modal() {
const modal = element(`<div tabindex="0"><div class="ant-modal-mask"></div><div class="ant-modal-wrap"><div class="ant-modal" style="width:60%;"><div class="ant-modal-content">Modal</div></div></div></div>`);
modal.show = () => { modal.style.display = "block"; modal.focus({ preventScroll: true }); };
modal.hide = () => { modal.style.display = "none"; };
on(modal.querySelector(".ant-modal-wrap"), "click", function (e) {
e.target === this && modal.hide();
});
on(document.body, "keyup", function (e) {
e.keyCode === 27 && modal.hide();
});
return modal;
}
function Input(i) {
const input = element(`<span class="ant-input-group ant-input-group-compact" style="display:flex;"><input type="text" class="ant-input"/><button type="button" class="ant-btn ant-btn-primary" style="box-sizing:border-box;"><span>Submit</span></button></span>`);
const $ = (s) => input.querySelector(s);
const inputElement = $("input");
inputElement.placeholder = i.placeholder || "input text";
on($("button"), "click", async () => await i.onSubmit(inputElement.value));
on(inputElement, "keyup", async function (e) {
e.keyCode === 13 && (await i.onSubmit(inputElement.value));
});
return input;
}
function Item(i) {
const item = element(`<div class="search-result-item"><img src="${i.src}" loading="lazy"/><p>${i.text}</p></div>`);
item.select = () => item.classList.add("selected");
on(item, "click", () => item.classList.toggle("selected"));
return item;
}
function ImagePicker(i) {
const modal = Modal();
const $ = (s) => modal.querySelector(s);
const $$ = (s) => [...modal.querySelectorAll(s)];
$(".ant-modal-content").innerHTML = `<button class="ant-modal-close"><span class="ant-modal-close-x"></span></button><div class="ant-modal-header"><div class="ant-modal-title">Search</div></div><div class="ant-modal-body"><div class="ant-list" tabindex="0" style="height:20rem;margin-top:1em;overflow-y:auto;"></div></div><div class="ant-modal-footer"><div><button type="button" class="ant-btn ant-btn-danger"><span>删 除</span></button><button type="button" class="ant-btn ant-btn-primary"><span>确 定</span></button></div></div>`;
on($(".ant-modal-close"), "click", () => modal.hide());
on($(".ant-btn-danger"), "click", async function () {
await i.onDelete(
$$(".search-result-item.selected>img").map((img) => img.src)
);
});
on($(".ant-btn-primary"), "click", async function () {
await i.onOK(
$$(".search-result-item.selected>img").map((img) => img.src)
);
modal.hide();
});
const list = $(".ant-list");
on(list, "keydown", function (e) {
if (e.ctrlKey && e.code === "KeyA") {
e.preventDefault();
$$(".search-result-item").forEach((item) => item.select());
}
});
const search = Input({
placeholder: "Search by tags (default_tag)",
onSubmit: async (text) => {
const result = await i.onSearch(text);
list.innerHTML = "";
list.append(...result.map((item) => Item(item)));
},
});
const body = $(".ant-modal-body");
body.insertBefore(search, body.firstChild);
modal.hide();
return modal;
}
function TagsInput(i) {
const modal = Modal();
const $ = (s) => modal.querySelector(s);
$(".ant-modal-content").innerHTML = `<div class="ant-modal-body"></div>`;
const input = Input({
placeholder: "Enter tags, separated by spaces",
onSubmit: async (text) => {
await i.onSubmit(text);
modal.hide();
},
});
const body = $(".ant-modal-body");
body.insertBefore(input, body.firstChild);
modal.hide();
return modal;
}
GM_addStyle(`
.search-result-item { border-radius:4px; display:inline-block; margin:4px; outline:solid 1px lightgray; padding:2px; }
.search-result-item.selected { outline:solid 2px deepskyblue; }
.search-result-item>img { border-radius:4px; max-height:150px; overflow:hidden; }
`);
// Observer to add or remove button
Observe(document.body, callback);
function Observe(targetNode, callback, config) {
config = config || {
attributes: false,
childList: true,
subtree: true,
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
return observer;
}
function callback(mutationsList) {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
for (const node of mutation.addedNodes) {
if (node.classList?.contains("ubb-image-toolbox")) {
addSaveButton(node);
} else if (
node.classList?.contains("ubb-editor") ||
node.classList?.contains("fa-smile-o") ||
node.id === "sendTopicInfo"
) {
addImagePickerButton();
}
}
for (const node of mutation.removedNodes) {
if (node.classList?.contains("fa-smile-o")) {
removeImagePickerButton();
}
}
}
}
}
function addSaveButton(toolbox) {
// console.log('addSaveButton');
const saveButton = element(
`<button><i class="fa fa-bookmark"></i></button>`
);
on(saveButton, "click", () => {
tagsInput.imgSrc = toolbox.nextSibling.src;
tagsInput.show();
});
toolbox.insertBefore(saveButton, toolbox.firstChild);
}
function addImagePickerButton() {
const referenceNode = document.querySelector(".fa-smile-o.ubb-button");
if (!referenceNode) return;
// console.log('addImagePickerButton');
const imagePickerButton = element(
`<button type="button" class="fa fa-bookmark ubb-button" title="收藏"></button>`
);
on(imagePickerButton, "click", () => {
imagePicker.show();
});
referenceNode.parentNode.insertBefore(
imagePickerButton,
referenceNode.nextSibling
);
}
function removeImagePickerButton() {
const imagePickerButton = document.querySelector(
".fa-bookmark.ubb-button"
);
if (!imagePickerButton) return;
// console.log('removeImagePickerButton');
imagePickerButton.remove();
}
function on(elem, event, func) {
return elem.addEventListener(event, func, false);
}
function element(html) {
var t = document.createElement("template");
t.innerHTML = html.trim();
return t.content.firstChild;
}
})();