CC98 Tools - Image Collections - dev

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

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