iconfont一键复制SVG

适用于 iconfont 快速复制 SVG 代码。支持图标库文件复制,矢量插画库文件复制。默认复制图标为20px,方便调整。

  1. // ==UserScript==
  2. // @name iconfont一键复制SVG
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2.0
  5. // @description 适用于 iconfont 快速复制 SVG 代码。支持图标库文件复制,矢量插画库文件复制。默认复制图标为20px,方便调整。
  6. // @author 2690874578@qq.com
  7. // @match https://www.iconfont.cn/*
  8. // @icon http://img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg
  9. // @require https://cdn.jsdelivr.net/npm/sweetalert2@11.7.3/dist/sweetalert2.all.min.js
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @run-at document-idle
  14. // @license GPL-3.0-only
  15. // ==/UserScript==
  16.  
  17.  
  18. (async function () {
  19. "use strict";
  20.  
  21. // 脚本级全局常量
  22. // minify js and css
  23. // https://www.minifier.org/
  24. // minify html
  25. // https://www.atatus.com/tools/html-minify
  26. const SCRIPT_SETTINGS_POPUP = `
  27. <dialog id="script-popup"><form method="dialog"><h1 class="set-tit1">插件设置</h1><fieldset><legend class="set-tit2">复制大小</legend><input name="size" placeholder="" type="number" max="5000" min="4" class="set-size" value="20"></fieldset><fieldset class="set-radio"><legend class="set-tit2">复制格式</legend><input id="radio-svg" type="radio" name="format" value="svg" class="svg-ra" checked="checked"><label class="s-txt" for="radio-svg">SVG</label><input id="radio-png" type="radio" name="format" value="png" class="svg-ra"><label class="s-txt" for="radio-png">PNG</label></fieldset><fieldset class="btn-outdiv"><button id="dialog-cancel" class="btb-cancel" onclick="document.querySelector('#radio-cancel').click()">取消</button><button id="dialog-confirm" class="btn-ok" onclick="document.querySelector('#radio-confirm').click()">确定</button><input id="radio-cancel" type="radio" name="action" value="cancel" class="hidden"> <input id="radio-confirm" type="radio" name="action" value="confirm" class="hidden"></fieldset></form><style>
  28. dialog#script-popup{*{all:revert;margin:0;padding:0;border-top-width:0;padding-right:0;padding-left:0;border-left-width:0;border-bottom-width:0;padding-bottom:0;margin:0;padding-top:0;border-right-width:0}.hidden{display:none}ul{list-style:none}img{border:none}a{text-decoration:none}.fl{float:left}.fr{float:right}.clear{clear:both;overflow:hidden;height:0}.center{width:1300px;margin:0 auto}.lv{color:#379e3e!important}.hong{color:#f00!important}a:hover img{filter:alpha(opacity=80);opacity:.8;-webkit-transition:all 0.2s linear;-moz-transition:all 0.2s linear;-ms-transition:all 0.2s linear;transition:all 0.2s linear}&{position:fixed;width:340px;padding:40px 30px;background-color:#fff;border:none;border-radius:4px;box-sizing:border-box;box-shadow:rgb(0 0 0 / .2) 0 12px 28px 0,rgb(0 0 0 / .1) 0 2px 4px 0,rgb(255 255 255 / .05) 0 0 0 1px inset}legend{font-weight:700}.set-tit1{max-width:100%;text-align:center;text-transform:none;word-wrap:break-word;color:#333;font-size:30px;font-style:normal;font-weight:700;margin-bottom:32px}.set-tit2{font-size:18px;color:#333;margin-bottom:8px}.set-size{border:2px solid #e8ebf3;box-sizing:border-box;padding:0 16px;width:100%;margin-bottom:32px;height:46px;border-radius:8px;font-size:14px}.set-size:focus{border:2px solid #4569ff;outline:none;box-shadow:inset 0 1px 1px rgb(0 0 0 / .06),0 0 0 3px rgb(62 157 255 / 30%)}.svg-ra{width:16px;height:16px;vertical-align:middle;margin:0 0 4px 0;padding:0}.set-radio label{margin-left:.5em;margin-right:32px}.s-txt{color:#666;text-align:center;font-size:14px;font-style:normal;font-weight:400;line-height:27px}.btn-outdiv{display:flex;align-items:flex-start;gap:8px;align-self:stretch;margin-top:32px}.btb-cancel{display:flex;width:73px;height:39px;padding:10px;justify-content:center;align-items:center;gap:10px;background-color:#fff;border:1px solid #dcdee2;outline:0;color:#999;transition:all 0.3s ease;border-radius:8px}.btb-cancel:hover{background-color:#e2e8f0;transition:all 0.3s ease}input[type='radio']:checked{border-color:red}.btn-ok{display:flex;height:39px;padding:10px;justify-content:center;align-items:center;gap:10px;flex:1 0 0%;background-color:#4569ff;transition:all 0.3s ease;color:#fff;border:0;outline:0;border-radius:8px}.btn-ok:hover{background:linear-gradient(0deg,rgb(0 0 0 / .1) 0%,rgb(0 0 0 / .1) 100%),#3f5dfa;transition:all 0.3s ease}}
  29. </style></dialog>
  30. `;
  31. const SMALL_DELAY = 200,
  32. MEDIUM_DELAY = 500,
  33. LARGE_DELAY = 1000,
  34. XML = new XMLSerializer(),
  35. // 支持的图像导出格式
  36. OUT_FMTS = ["svg", "png"],
  37. // 设置表: [设置的名称, 默认值]
  38. MENU = {
  39. SVG_SIZE: ["svg_size", 20],
  40. OUT_FMT: ["out_fmt", "svg"],
  41. };
  42.  
  43. /**
  44. * 脚本配置初始化 -------------------------------------------------------------------------
  45. */
  46.  
  47. const CFG = (() => {
  48. /**
  49. * 判断 size 是否为 1 - 1000 内的整数
  50. * @param {string | number} size
  51. * @returns {boolean}
  52. */
  53. function _is_size_valid(size) {
  54. const _size = parseFloat(size);
  55. if (_size === NaN) return false;
  56. if (_size - parseInt(size) !== 0) return false;
  57. if (_size < 4 || _size > 5000) return false;
  58. return true;
  59. }
  60.  
  61. /**
  62. * 生成 x 合规函数生成器
  63. * @param {(x: string) => T} parse
  64. * @param {(x: string) => boolean} is_valid
  65. * @param {() => T} getter
  66. * @returns {(alter_on_invalid: (string) => void) => (size: string) => number}
  67. */
  68. function _gen_validator(parse, is_valid, getter) {
  69. return (alter_on_invalid) => (x) => {
  70. const _x = parse(x);
  71. if (!is_valid(x)) {
  72. console.warn(`无效的插件设置项:`, x);
  73. alter_on_invalid(x);
  74. return getter();
  75. }
  76. return _x;
  77. };
  78. }
  79.  
  80. /**
  81. * 从对话框关闭事件提取表单数据为字典
  82. * @param {CloseEvent} event
  83. * @returns {Map<string, string>}
  84. */
  85. function _extract_form_data(event) {
  86. const form = event.target.querySelector("form");
  87. const data = new FormData(form);
  88. return new Map(data.entries());
  89. }
  90.  
  91. function _is_format_valid(format) {
  92. return OUT_FMTS.includes(format);
  93. }
  94.  
  95. /**
  96. * 生成提示函数
  97. * @param {string} text 应该有 {x} 占位符
  98. * @param {string} title
  99. * @returns {(x: string) => void}
  100. */
  101. function _alert_invalid_x(text, title) {
  102. return (x) => alert_error(text.replace("{x}", x), title);
  103. }
  104.  
  105. /**
  106. * 解包表单数据,合法化,对非法的弹窗提示
  107. * @param {Map<string, string>} data
  108. * @returns {{ size: number, format: string }}
  109. */
  110. function _unpack(data) {
  111. const alert_size = _alert_invalid_x(
  112. "尺寸 {x} 不是有效数字!",
  113. "无效尺寸!"
  114. );
  115. const alert_format = _alert_invalid_x(
  116. "图像格式 {x} 不受支持!",
  117. "无效格式!"
  118. );
  119. const get_stored_x = (x) => () => GM_getValue(...x);
  120. const validate_size = _gen_validator(
  121. parseInt,
  122. _is_size_valid,
  123. get_stored_x(MENU.SVG_SIZE)
  124. );
  125. const pass = (x) => x;
  126. const validate_format = _gen_validator(
  127. pass,
  128. _is_format_valid,
  129. get_stored_x(MENU.OUT_FMT)
  130. );
  131.  
  132. const size = validate_size(alert_size)(data.get("size"));
  133. const format = validate_format(alert_format)(data.get("format"));
  134. return { size, format };
  135. }
  136.  
  137. /**
  138. * 弹出脚本配置弹窗以获取配置
  139. * @param {(data: { size: number, format: string }, ...tasks: (data: { size: number, format: string }) => Promise<void>) => Promise<void>} on_success
  140. * @param {...(data: { size: number, format: string }) => Promise<void>} tasks
  141. * @returns {Promise<{ valid: boolean, size: number, format: string }>}
  142. */
  143. async function ask_for_config(on_success, ...tasks) {
  144. const event = await show_settings();
  145. const data = _extract_form_data(event);
  146. console.info(`从对话框中提取的表单数据:`, data);
  147.  
  148. if (data.get("action") === "cancel") {
  149. show_msg("取消设置", "warning");
  150. return { valid: false };
  151. }
  152.  
  153. const { size, format } = _unpack(data);
  154. on_success({ size, format }, ...tasks);
  155. return {
  156. valid: true,
  157. size,
  158. format,
  159. };
  160. }
  161.  
  162. // 在 GM_registerMenuCommand 函数中注册(不可用)
  163. /**
  164. * 显式插件设置成功
  165. * @param {{ size: number, format: string }} _
  166. * @returns {Promise<void>}
  167. */
  168. async function _show_config_ok(_) {
  169. show_msg("插件设置成功~");
  170. console.info("插件设置成功弹窗已经触发");
  171. }
  172.  
  173. // 在 GM_registerMenuCommand 函数中注册(不可用)
  174. /**
  175. * 设置复制图标的提示文本
  176. * @param {{ size: number, format: string }} config
  177. * @returns {Promise<void>}
  178. */
  179. async function _set_icon_title(config) {
  180. const { format } = config;
  181. const _format = format.toUpperCase();
  182. const icons = $("span.svg-copy");
  183.  
  184. for (const span of icons) {
  185. span.title = "复制" + _format;
  186. }
  187. console.info(`全部图标 title 已经更新完成 -> ${_format}`);
  188. }
  189.  
  190. /**
  191. * 当插件设置成功时批量执行任务
  192. * @param {{ size: number, format: string }} config
  193. * @param {...(data: { size: number, format: string }) => Promise<void>} tasks
  194. */
  195. async function on_config_success(config, ...tasks) {
  196. console.info("插件设置成功,正在批量执行后置任务,使用最新配置:", config);
  197. for (const task of tasks) {
  198. task(config);
  199. }
  200. }
  201.  
  202. /**
  203. * 获取存储的值(失败时使用默认值)
  204. * @returns {{ size: number, format: string }}
  205. */
  206. function get_stored_config() {
  207. return {
  208. size: GM_getValue(...MENU.SVG_SIZE),
  209. format: GM_getValue(...MENU.OUT_FMT),
  210. };
  211. }
  212.  
  213. /**
  214. * 为弹窗对话框插入存储的配置
  215. * @param {HTMLDialogElement} popup
  216. * @param {{ size: number, format: string }} config
  217. */
  218. function insert_config(popup, config) {
  219. const $ = (s) => popup.querySelector(s);
  220. $('input[name="size"]').value = config.size;
  221. $(`[id="radio-${config.format}"]`).click();
  222. }
  223.  
  224. /**
  225. * 弹出脚本设置弹窗,返回弹窗关闭事件
  226. * @returns {Promise<CloseEvent>}
  227. */
  228. function show_settings() {
  229. const popup = $("#script-popup")[0];
  230. const config = get_stored_config();
  231. insert_config(popup, config);
  232. popup.showModal();
  233.  
  234. return new Promise((resolve, _) => {
  235. popup.addEventListener("close", resolve);
  236. });
  237. }
  238.  
  239. GM_registerMenuCommand("插件设置", async () => {
  240. const config = await ask_for_config(
  241. on_config_success,
  242. _show_config_ok,
  243. _set_icon_title,
  244. );
  245. if (!config.valid) return;
  246.  
  247. GM_setValue(MENU.SVG_SIZE[0], config.size);
  248. GM_setValue(MENU.OUT_FMT[0], config.format);
  249. });
  250.  
  251. return {
  252. /**
  253. * @returns {number}
  254. */
  255. get SVG_SIZE() {
  256. return GM_getValue(...MENU.SVG_SIZE);
  257. },
  258.  
  259. /**
  260. * @returns {string}
  261. */
  262. get OUT_FMT() {
  263. return GM_getValue(...MENU.OUT_FMT);
  264. },
  265. };
  266. })();
  267.  
  268. /**
  269. * 公用函数 ----------------------------------------------------------------------------
  270. */
  271.  
  272. function fire(...args) {
  273. const Swal = window["Swal"];
  274. if (!(Swal instanceof Function)) {
  275. // debugger;
  276. const msg = "弹窗库 SweetAlert2 未加载!";
  277. alert(msg + "\n你将无法看到正常弹窗,但功能仍会执行!");
  278. console.warn("弹窗消息:", ...args);
  279. return Promise.reject(new Error(msg));
  280. }
  281. return Swal.fire(...args);
  282. }
  283.  
  284. /**
  285. * 弹出小型提示框,2秒后消失
  286. * @param {string} text 提示文本
  287. * @param {"success" | "warning" | "error" | "info" | "question"} status 状态
  288. * @returns {Promise}
  289. */
  290. function show_msg(text = "复制成功,可以粘贴咯~", status = "success") {
  291. return fire({
  292. text,
  293. toast: true,
  294. timer: 2000,
  295. showConfirmButton: false,
  296. icon: status,
  297. position: "top",
  298. customClass: {
  299. popup: "copy-popup",
  300. htmlContainer: "copy-container",
  301. icon: "copy-icon",
  302. },
  303. });
  304. }
  305.  
  306. /**
  307. * 显示警告弹窗
  308. * @param {string} text
  309. * @param {string} title
  310. */
  311. function alert_error(text, title = null) {
  312. fire({
  313. icon: "error",
  314. title,
  315. text,
  316. });
  317. }
  318.  
  319. /**
  320. * 异步的等待 delay_ms 毫秒
  321. * @param {number} delay_ms
  322. * @returns {Promise<void>}
  323. */
  324. function sleep(delay_ms) {
  325. return new Promise((resolve) => setTimeout(resolve, delay_ms));
  326. }
  327.  
  328. /**
  329. * 将 svg 元素序列化为大小为 20x20 的 svg 代码
  330. * @param {SVGElement} svg
  331. * @returns {string}
  332. */
  333. function svgToStr(svg) {
  334. // 设置大小
  335. svg.setAttribute("width", `${CFG.SVG_SIZE}`);
  336. svg.setAttribute("height", `${CFG.SVG_SIZE}`);
  337.  
  338. // 序列化
  339. return XML.serializeToString(svg);
  340. }
  341.  
  342. /**
  343. * 元素选择器
  344. * @param {string} selector 选择器
  345. * @returns {HTMLElement[]} 元素
  346. */
  347. function $(selector) {
  348. const self = this?.querySelectorAll ? this : document;
  349. return [...self.querySelectorAll(selector)];
  350. }
  351.  
  352. /**
  353. * 安全元素选择器,直到元素存在时才返回元素列表,最多等待5秒
  354. * @param {string} selector 选择器
  355. * @returns {Promise<Array<HTMLElement>} 元素列表
  356. */
  357. async function $$(selector) {
  358. const self = this?.querySelectorAll ? this : document;
  359.  
  360. for (let i = 0; i < 10; i++) {
  361. let elems = [...self.querySelectorAll(selector)];
  362. if (elems.length > 0) {
  363. return elems;
  364. }
  365. await sleep(200);
  366. }
  367.  
  368. const not_found_error = new Error(
  369. `"${selector}" not found in 2s`
  370. );
  371. console.error(not_found_error);
  372. return [];
  373. }
  374.  
  375. /**
  376. * 域名主函数 ----------------------------------------------------------------------
  377. */
  378.  
  379. /**
  380. * iconfont 主函数
  381. */
  382. async function iconfont() {
  383. console.log("进入 iconfont");
  384. init_task();
  385.  
  386. // 域名级全局常量
  387. const PATHS = ["search", "illustrations", "collections"];
  388. const STYLE_TEXT = `
  389. .force-hide{visibility:hidden!important}.block-icon-list li:hover div.icon-cover{display:grid;grid-template-columns:auto auto}.block-icon-list li .icon-cover span.cover-item-line{height:auto;line-height:50px}.svg-copy.disabled{color:#6d6d6d!important}.icon-fuzhidaima:before{font-size:24px}.copy-icon{border:none!important;margin:0 1.25em!important;margin:0 0 0 10px!important}.copy-container{margin:8px 16px!important;padding:0!important;font-size:14px!important}.copy-popup{top:60px;padding:4px 10px!important;height:44px!important;font-size:12px!important;width:fit-content!important;align-content:center;box-shadow:rgb(0 0 0 / .2) 0 12px 28px 0,rgb(0 0 0 / .1) 0 2px 4px 0,rgb(255 255 255 / .05) 0 0 0 1px inset!important}
  390. `;
  391.  
  392. /**
  393. * 阻塞直到图标区存在
  394. */
  395. while (true) {
  396. if ($(".block-icon-list > li")[0]) break;
  397. await sleep(SMALL_DELAY);
  398. }
  399. console.log("图标区出现了,开始执行任务");
  400.  
  401. function addStyle() {
  402. const id = "iconfont-svg-copy-style";
  403. if ($(`#${id}`)[0]) return;
  404.  
  405. const style = document.createElement("style");
  406. style.id = id;
  407. style.innerHTML = STYLE_TEXT;
  408. document.head.append(style);
  409. }
  410.  
  411. /**
  412. * 初始化 writeText 钩子
  413. * @returns {Function}
  414. */
  415. function initHookOnWriteText() {
  416. const writeText = navigator.clipboard.writeText;
  417. const boundedWriteText = (text) =>
  418. writeText.call(navigator.clipboard, text);
  419.  
  420. /**
  421. *
  422. * @param {(value) => any} resolve
  423. * @returns {(text: string) => string}
  424. */
  425. function wrapHookedWriteText(resolve) {
  426. /**
  427. * @param {string} text
  428. * @returns {string}
  429. */
  430. return async function (text) {
  431. console.log("进入 hooked 的 writeText 函数");
  432.  
  433. // 无论成功与否,writeText 都要改回去
  434. Object.defineProperty(navigator.clipboard, "writeText", {
  435. value: writeText,
  436. writable: false,
  437. enumerable: true,
  438. configurable: true,
  439. });
  440.  
  441. // 没有取得 svg 字符串,解决为空字符串
  442. if (!`${text}`.includes("<svg")) {
  443. resolve("");
  444. return;
  445. }
  446.  
  447. // 成功取得 svg 字符串,解决为SVG代码
  448. try {
  449. // await boundedWriteText(text);
  450. resolve(text);
  451. } catch (e) {
  452. return e;
  453. }
  454. };
  455. }
  456.  
  457. function hookWriteText() {
  458. return new Promise((resolve) => {
  459. // 劫持 writeText 函数,直到一次调用后失效
  460. Object.defineProperty(navigator.clipboard, "writeText", {
  461. value: wrapHookedWriteText(resolve),
  462. writable: false,
  463. enumerable: true,
  464. configurable: true,
  465. });
  466. });
  467. }
  468. return hookWriteText;
  469. }
  470.  
  471. /**
  472. * 返回期约,直到 writeText 被调用且复制内容包含 "<svg" 时才解决为 svg_str。
  473. * 如果调用 writeText 的内容不包含 "<svg",则解决为 ""。
  474. */
  475. const hookWriteText = initHookOnWriteText();
  476.  
  477. /**
  478. * 在弹窗中获取 svg
  479. * @param {HTMLElement} card
  480. */
  481. async function copyInPopup(card) {
  482. // 禁用复制按钮
  483. const icon = $.call(card, ".svg-copy")[0];
  484. icon.classList.add("disabled");
  485. icon.removeEventListener("click", on_copy_icon_clicked, true);
  486.  
  487. // 触发弹窗
  488. const download = $.call(card, "[title='下载'], [title='Download']")[0];
  489. download.click();
  490.  
  491. // 等待弹窗加载完毕
  492. while (true) {
  493. if ($("[id*='dlg_'], [id*='mask_dlg_']").length) {
  494. break;
  495. }
  496. await sleep(SMALL_DELAY);
  497. }
  498.  
  499. // 隐藏弹窗
  500. const dialogs = await $$("[id*='dlg_']");
  501. dialogs.forEach((elem) => elem.classList.add("force-hide"));
  502.  
  503. let popup;
  504. for (let elem of dialogs) {
  505. if (elem.id.startsWith("dlg_")) {
  506. popup = elem;
  507. break;
  508. }
  509. }
  510. if (!popup) throw new Error("#dlg_ not found");
  511.  
  512. // 取得复制SVG按钮
  513. const copy_btn = (await $$("#btn-copy-svg"))[0];
  514. if (!copy_btn) {
  515. alert_error("此插画无法复制!可能是版权受限!", "复制失败");
  516. return;
  517. }
  518.  
  519. let svg_str = "";
  520. let i = 1;
  521. do {
  522. const copy_task = hookWriteText();
  523. copy_btn.click();
  524. await sleep(SMALL_DELAY);
  525. svg_str = await copy_task;
  526. console.info(`try copy svg in popup: ${i}`);
  527. i += 1;
  528. // debugger;
  529. } while (svg_str === "" && i < 10);
  530.  
  531. if (svg_str === "") {
  532. alert_error("复制失败!可能是网络异常!请稍后再试!", "复制失败");
  533. return;
  534. }
  535.  
  536. // debugger;
  537. await copy_svg_to_aim_fmt(svg_str);
  538.  
  539. // 关闭弹窗
  540. $(".mp-e2e-dialog-close")[0].click();
  541. // 重新启用复制按钮
  542. icon.classList.remove("disabled");
  543. icon.addEventListener("click", on_copy_icon_clicked, true);
  544. }
  545.  
  546. /**
  547. * 复制 blobs 为一个剪贴板对象
  548. * @param {Blob[]} blobs
  549. */
  550. async function copy_blobs(blobs) {
  551. console.log("blob to be written:", blobs);
  552. const bundle = {};
  553. blobs.forEach((blob) => {
  554. bundle[blob.type] = blob;
  555. });
  556. const item = new ClipboardItem(bundle);
  557. // 复制到剪贴板
  558. try {
  559. await navigator.clipboard.write([item]);
  560. // 提示复制成功
  561. show_msg();
  562. }
  563. catch (err) {
  564. console.error(err);
  565. show_msg("复制到剪贴板失败!", "error");
  566. }
  567. }
  568.  
  569. /**
  570. * svg str 转 png blob
  571. * @param {string} svg
  572. * @returns {Promise<Blob[]>}
  573. */
  574. async function svg_to_png(svg) {
  575. // prepare an image for format convertion
  576. const img = new Image();
  577. const svg_blob = new Blob([svg], { type: "image/svg+xml" });
  578. const svg_url = URL.createObjectURL(svg_blob);
  579. img.src = svg_url;
  580.  
  581. // draw white background
  582. const canvas = document.createElement("canvas");
  583. const size = CFG.SVG_SIZE;
  584. const sizes = [size, size];
  585. [canvas.width, canvas.height] = sizes;
  586. const ctx = canvas.getContext("2d");
  587. ctx.fillStyle = "transparent";
  588. ctx.fillRect(0, 0, ...sizes);
  589.  
  590. // draw image
  591. await new Promise((resolve) => {
  592. img.onload = resolve;
  593. });
  594. ctx.drawImage(img, 0, 0);
  595. // release svg resource
  596. URL.revokeObjectURL(svg_url);
  597.  
  598. // convert to png
  599. return new Promise((resolve) =>
  600. canvas.toBlob(resolve)
  601. );
  602. }
  603.  
  604. /**
  605. * 复制 svg 代码为目标格式到剪贴板
  606. * @param {string} svg_str
  607. */
  608. async function copy_svg_to_aim_fmt(svg_str) {
  609. const fmt_to_blobs = new Map([
  610. ["svg", async () => [
  611. new Blob([svg_str], { type: "text/plain" }),
  612. new Blob([svg_str], { type: "image/svg+xml" }),
  613. ]],
  614. ["png", async () => [await svg_to_png(svg_str)]],
  615. ]);
  616.  
  617. const blob_for_exception = new Blob([`未知输出格式: ${CFG.OUT_FMT}`], { type: "text/plain" });
  618. const gen_ex_blobs = async () => [blob_for_exception];
  619. const gen_blobs = fmt_to_blobs.get(CFG.OUT_FMT) ?? gen_ex_blobs;
  620. const blobs = await gen_blobs();
  621.  
  622. await copy_blobs(blobs);
  623. }
  624.  
  625. /**
  626. * 当点击复制图标时复制 svg 到剪贴板
  627. * @param {PointerEvent} event
  628. */
  629. function on_copy_icon_clicked(event) {
  630. // 取得svg
  631. const card = event.target.closest("li");
  632. const svg = $.call(card, "svg")[0];
  633.  
  634. // 如果是在 iframe 中的,那就要通过模拟点击下载的方式来获取
  635. if (!svg) {
  636. copyInPopup(card);
  637. return;
  638. }
  639.  
  640. // 序列化
  641. const svg_str = svgToStr(svg);
  642. copy_svg_to_aim_fmt(svg_str);
  643. }
  644.  
  645. function addCopyIcon() {
  646. // 获取卡片
  647. const cards = $(".block-icon-list > li");
  648. if (!cards[0]) throw new Error("无法选中图标块");
  649.  
  650. // 制作按钮元素模板
  651. const template = document.createElement("span");
  652. template.title = "复制" + CFG.OUT_FMT.toUpperCase();
  653. template.classList.add(
  654. "cover-item",
  655. "iconfont",
  656. "cover-item-line",
  657. "icon-fuzhidaima",
  658. "svg-copy"
  659. );
  660.  
  661. cards.forEach((card) => {
  662. // 添加复制图标
  663. const icon_copy = template.cloneNode();
  664. // 增加复制功能
  665. icon_copy.addEventListener("click", on_copy_icon_clicked, true);
  666. card.querySelector(".icon-cover").append(icon_copy);
  667. });
  668. }
  669.  
  670. function add_script_settings_popup(popup_html) {
  671. // 弹窗已存在,退出
  672. if ($("#script-popup")[0]) return;
  673. // 弹窗不存在,创建并插入
  674. document.body.insertAdjacentHTML("beforeend", popup_html);
  675. }
  676.  
  677. async function mainTask() {
  678. console.log("mainTask entered");
  679.  
  680. const first_path = location.pathname.split("/")[1];
  681. console.log("当前一级路径:" + first_path);
  682.  
  683. // 无关路径
  684. if (!first_path || !PATHS.includes(first_path)) return;
  685.  
  686. // 等待直到图标块出现
  687. while (true) {
  688. if ($(".block-icon-list > li")[0]) break;
  689. await sleep(SMALL_DELAY);
  690. }
  691.  
  692. // 如果已经存在按钮,退出主函数
  693. if ($(".icon-cover span.svg-copy")[0]) return;
  694. console.log("正在建造 [复制SVG] 图标...");
  695.  
  696. addStyle();
  697. addCopyIcon();
  698.  
  699. console.log("[复制SVG] 图标 建造完成");
  700. }
  701.  
  702. function delayedTask() {
  703. setTimeout(mainTask, 0);
  704. }
  705.  
  706. function getIconsBox(block = true) {
  707. const s = ".block-icon-list";
  708. if (block) return $(`${s} li`)[0].closest(s);
  709. return $$(`${s} li`).then((elems) => elems[0].closest(s));
  710. }
  711.  
  712. function monitorIconsChanging() {
  713. const observer = new MutationObserver(delayedTask);
  714. observer.observe(getIconsBox(), { childList: true });
  715. }
  716.  
  717. const onMainChanged = (function () {
  718. let icons_box = getIconsBox();
  719.  
  720. async function inner() {
  721. const new_box = await getIconsBox(false);
  722. if (icons_box === new_box) return;
  723.  
  724. icons_box = new_box;
  725. mainTask();
  726. monitorIconsChanging();
  727. }
  728.  
  729. function delayed() {
  730. setTimeout(inner, LARGE_DELAY);
  731. }
  732.  
  733. return delayed;
  734. })();
  735.  
  736. async function monitorMainChanging() {
  737. const elem = (await $$("#magix_vf_main"))[0],
  738. observer = new MutationObserver(onMainChanged);
  739. observer.observe(elem, { attributes: true });
  740. }
  741.  
  742. function init_task() {
  743. console.info("执行初始化任务");
  744. add_script_settings_popup(SCRIPT_SETTINGS_POPUP);
  745. }
  746.  
  747. function main() {
  748. console.log("进入 iconfont.main");
  749. mainTask();
  750. monitorMainChanging();
  751. monitorIconsChanging();
  752. addEventListener("popstate", mainTask, true);
  753. }
  754.  
  755. main();
  756. }
  757.  
  758. function execute_route(route, host) {
  759. const action = route[host];
  760. if (!action) {
  761. console.log(`未知域名,不能处理:${host}`);
  762. return;
  763. }
  764. action();
  765. }
  766.  
  767. /**
  768. * 路由,主函数入口
  769. */
  770. (() => {
  771. console.log("进入 route");
  772. const host = location.hostname;
  773. const route = {
  774. "www.iconfont.cn": iconfont,
  775. };
  776. execute_route(route, host);
  777. })();
  778.  
  779. /**
  780. * [更新日志]
  781. *
  782. * 更新日期:2023/4/23
  783. * 更新版本:1.1.2
  784. * - 美化SVG尺寸设置弹窗
  785. *
  786. * 更新日期:2023/7/24
  787. * 更新版本:1.1.3
  788. * - 新增复制为PNG选项
  789. *
  790. * 更新日期:2024/6/15
  791. * 更新版本:1.1.4
  792. * - 重写配置弹窗,合并尺寸和图像类型到一个弹窗中
  793. *
  794. * 更新日期:2024/6/15
  795. * 更新版本:1.1.5
  796. * - 修复非图标页面不能打开设置弹窗的 BUG
  797. *
  798. * 更新日期:2024/6/16
  799. * 更新版本:1.1.6
  800. * - 修复复制图标 title 不随复制格式变化而变化的 BUG
  801. *
  802. * 更新日期:2024/7/11
  803. * 更新版本:1.1.7
  804. * - 修复复制部分插画失败的问题
  805. * - 当无法复制时弹窗提示(copyInPopup情形下)
  806. *
  807. * 更新日期:2024/7/11
  808. * 更新版本:1.1.8
  809. * - 调整 sweetalert2 依赖
  810. *
  811. * 更新日期:2024/7/11
  812. * 更新版本:1.1.9
  813. * - 优化部分代码
  814. */
  815. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址