CC98 Tools - Image Collections - GM

为CC98网页版添加收藏图片功能

  1. // ==UserScript==
  2. // @name CC98 Tools - Image Collections - GM
  3. // @version 1.0.3
  4. // @description 为CC98网页版添加收藏图片功能
  5. // @icon https://www.cc98.org/static/98icon.ico
  6.  
  7. // @author ml98
  8. // @namespace https://www.cc98.org/user/name/ml98
  9. // @license MIT
  10.  
  11. // @match https://www.cc98.org/*
  12. // @grant GM_addStyle
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @run-at document-idle
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. "use strict";
  20. // Store
  21. const db = GMStore();
  22.  
  23. // GM Store
  24. function GMStore() {
  25. async function add(image) {
  26. const images = GM_getValue('images', []);
  27. images.push(image);
  28. GM_setValue('images', images);
  29. }
  30. async function get(tags) {
  31. const images = GM_getValue('images', []);
  32. return images.filter(image => tags.some(tag => image.tags.includes(tag)));
  33. }
  34. async function del(urls) {
  35. const images = GM_getValue('images', []);
  36. const new_images = images.filter(image => !urls.includes(image.url));
  37. GM_setValue('images', new_images);
  38. }
  39. return { add, get, del };
  40. }
  41.  
  42. // Components
  43. const imagePicker = ImagePicker({
  44. onSearch: async function (text) {
  45. const images = await db.get(text.split(" "));
  46. const result = images.map((image) => ({
  47. src: image.url,
  48. text: image.tags
  49. .filter((tag) => tag !== "default_tag")
  50. .join(" "),
  51. }));
  52. console.log("result", result);
  53. return result;
  54. },
  55. onDelete: async function (urls) {
  56. console.log("delete", urls);
  57. await db.del(urls);
  58. },
  59. onOK: async function (urls) {
  60. console.log("ok", urls);
  61. putText(urls.map((url) => `[img]${url}[/img]\n`).join(""));
  62. },
  63. });
  64. document.body.appendChild(imagePicker);
  65.  
  66. const tagsInput = TagsInput({
  67. onSubmit: async function (text) {
  68. const tags = ["default_tag", ...text.split(" ").filter(Boolean)];
  69. console.log("save", tagsInput.imgSrc, "with tags", tags);
  70. await db.add({ url: tagsInput.imgSrc, tags: tags });
  71. },
  72. });
  73. document.body.appendChild(tagsInput);
  74.  
  75. function putText(text) {
  76. const textarea = document.querySelector(".ubb-editor > textarea");
  77. if (!textarea) return;
  78. const setter = Object.getOwnPropertyDescriptor(
  79. window.HTMLTextAreaElement.prototype,
  80. "value"
  81. ).set;
  82. setter.call(textarea, textarea.value + text);
  83. textarea.dispatchEvent(new Event("input", { bubbles: true }));
  84. }
  85.  
  86. function Modal() {
  87. 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>`);
  88. modal.show = () => { modal.style.display = "block"; modal.focus({ preventScroll: true }); };
  89. modal.hide = () => { modal.style.display = "none"; };
  90. on(modal.querySelector(".ant-modal-wrap"), "click", function (e) {
  91. e.target === this && modal.hide();
  92. });
  93. on(document.body, "keyup", function (e) {
  94. e.keyCode === 27 && modal.hide();
  95. });
  96. return modal;
  97. }
  98.  
  99. function Input(i) {
  100. 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>`);
  101. const $ = (s) => input.querySelector(s);
  102. const inputElement = $("input");
  103. inputElement.placeholder = i.placeholder || "input text";
  104. on($("button"), "click", async () => await i.onSubmit(inputElement.value));
  105. on(inputElement, "keyup", async function (e) {
  106. e.keyCode === 13 && (await i.onSubmit(inputElement.value));
  107. });
  108. return input;
  109. }
  110.  
  111. function Item(i) {
  112. const item = element(`<div class="search-result-item"><img src="${i.src}" loading="lazy"/><p>${i.text}</p></div>`);
  113. item.select = () => item.classList.add("selected");
  114. on(item, "click", () => item.classList.toggle("selected"));
  115. return item;
  116. }
  117.  
  118. function ImagePicker(i) {
  119. const modal = Modal();
  120. const $ = (s) => modal.querySelector(s);
  121. const $$ = (s) => [...modal.querySelectorAll(s)];
  122. $(".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>`;
  123. on($(".ant-modal-close"), "click", () => modal.hide());
  124. on($(".ant-btn-danger"), "click", async function () {
  125. await i.onDelete(
  126. $$(".search-result-item.selected>img").map((img) => img.src)
  127. );
  128. });
  129. on($(".ant-btn-primary"), "click", async function () {
  130. await i.onOK(
  131. $$(".search-result-item.selected>img").map((img) => img.src)
  132. );
  133. modal.hide();
  134. });
  135. const list = $(".ant-list");
  136. on(list, "keydown", function (e) {
  137. if (e.ctrlKey && e.code === "KeyA") {
  138. e.preventDefault();
  139. $$(".search-result-item").forEach((item) => item.select());
  140. }
  141. });
  142. const search = Input({
  143. placeholder: "Search by tags (default_tag)",
  144. onSubmit: async (text) => {
  145. const result = await i.onSearch(text);
  146. list.innerHTML = "";
  147. list.append(...result.map((item) => Item(item)));
  148. },
  149. });
  150. const body = $(".ant-modal-body");
  151. body.insertBefore(search, body.firstChild);
  152. modal.hide();
  153. return modal;
  154. }
  155.  
  156. function TagsInput(i) {
  157. const modal = Modal();
  158. const $ = (s) => modal.querySelector(s);
  159. $(".ant-modal-content").innerHTML = `<div class="ant-modal-body"></div>`;
  160. const input = Input({
  161. placeholder: "Enter tags, separated by spaces",
  162. onSubmit: async (text) => {
  163. await i.onSubmit(text);
  164. modal.hide();
  165. },
  166. });
  167. const body = $(".ant-modal-body");
  168. body.insertBefore(input, body.firstChild);
  169. modal.hide();
  170. return modal;
  171. }
  172.  
  173. GM_addStyle(`
  174. .search-result-item { border-radius:4px; display:inline-block; margin:4px; outline:solid 1px lightgray; padding:2px; }
  175. .search-result-item.selected { outline:solid 2px deepskyblue; }
  176. .search-result-item>img { border-radius:4px; max-height:150px; overflow:hidden; }
  177. `);
  178.  
  179. // Observer to add or remove button
  180. Observe(document.body, callback);
  181.  
  182. function Observe(targetNode, callback, config) {
  183. config = config || {
  184. attributes: false,
  185. childList: true,
  186. subtree: true,
  187. };
  188. const observer = new MutationObserver(callback);
  189. observer.observe(targetNode, config);
  190. return observer;
  191. }
  192.  
  193. function callback(mutationsList) {
  194. for (const mutation of mutationsList) {
  195. if (mutation.type === "childList") {
  196. for (const node of mutation.addedNodes) {
  197. if (node.classList?.contains("ubb-image-toolbox")) {
  198. addSaveButton(node);
  199. } else if (
  200. node.classList?.contains("ubb-editor") ||
  201. node.classList?.contains("fa-smile-o") ||
  202. node.id === "sendTopicInfo"
  203. ) {
  204. addImagePickerButton();
  205. }
  206. }
  207. for (const node of mutation.removedNodes) {
  208. if (node.classList?.contains("fa-smile-o")) {
  209. removeImagePickerButton();
  210. }
  211. }
  212. }
  213. }
  214. }
  215.  
  216. function addSaveButton(toolbox) {
  217. // console.log('addSaveButton');
  218. const saveButton = element(
  219. `<button><i class="fa fa-bookmark"></i></button>`
  220. );
  221. on(saveButton, "click", () => {
  222. tagsInput.imgSrc = toolbox.nextSibling.src;
  223. tagsInput.show();
  224. });
  225. toolbox.insertBefore(saveButton, toolbox.firstChild);
  226. }
  227.  
  228. function addImagePickerButton() {
  229. const referenceNode = document.querySelector(".fa-smile-o.ubb-button");
  230. if (!referenceNode) return;
  231. // console.log('addImagePickerButton');
  232. const imagePickerButton = element(
  233. `<button type="button" class="fa fa-bookmark ubb-button" title="收藏"></button>`
  234. );
  235. on(imagePickerButton, "click", () => {
  236. imagePicker.show();
  237. });
  238. referenceNode.parentNode.insertBefore(
  239. imagePickerButton,
  240. referenceNode.nextSibling
  241. );
  242. }
  243.  
  244. function removeImagePickerButton() {
  245. const imagePickerButton = document.querySelector(
  246. ".fa-bookmark.ubb-button"
  247. );
  248. if (!imagePickerButton) return;
  249. // console.log('removeImagePickerButton');
  250. imagePickerButton.remove();
  251. }
  252.  
  253. function on(elem, event, func) {
  254. return elem.addEventListener(event, func, false);
  255. }
  256.  
  257. function element(html) {
  258. var t = document.createElement("template");
  259. t.innerHTML = html.trim();
  260. return t.content.firstChild;
  261. }
  262. })();