GreasyFork优化

自动登录账号、快捷寻找自己库被其他脚本引用、更新自己的脚本列表、库、优化图片浏览、美化页面、Markdown复制按钮

当前为 2024-03-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GreasyFork优化
  3. // @namespace https://greasyfork.org/zh-CN/scripts/475722
  4. // @supportURL https://github.com/WhiteSevs/TamperMonkeyScript/issues
  5. // @version 2024.3.15.14.59
  6. // @description 自动登录账号、快捷寻找自己库被其他脚本引用、更新自己的脚本列表、库、优化图片浏览、美化页面、Markdown复制按钮
  7. // @author WhiteSevs
  8. // @license MIT
  9. // @icon data:image/png;base64,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
  10. // @match *://greasyfork.org/*
  11. // @run-at document-start
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @grant GM_deleteValue
  15. // @grant GM_xmlhttpRequest
  16. // @grant GM_addStyle
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_unregisterMenuCommand
  19. // @grant GM_info
  20. // @grant unsafeWindow
  21. // @connect greasyfork.org
  22. // @require https://update.greasyfork.org/scripts/449471/1305484/Viewer.js
  23. // @require https://update.greasyfork.org/scripts/462234/1322684/Message.js
  24. // @require https://update.greasyfork.org/scripts/456485/1343167/pops.js
  25. // @require https://update.greasyfork.org/scripts/455186/1343166/WhiteSevsUtils.js
  26. // @require https://update.greasyfork.org/scripts/465772/1343165/DOMUtils.js
  27. // ==/UserScript==
  28.  
  29. (function () {
  30. if (typeof unsafeWindow === "undefined") {
  31. unsafeWindow = globalThis;
  32. }
  33. /* -----------------↓公共配置↓----------------- */
  34. /**
  35. * @type {import("../库/Qmsg")}
  36. */
  37. const Qmsg = window.Qmsg;
  38. /**
  39. * @type {import("../库/pops")}
  40. */
  41. const pops = window.pops;
  42. /**
  43. * @type {import("../库/Utils")}
  44. */
  45. const utils = window.Utils.noConflict();
  46. /**
  47. * @type {import("../库/DOMUtils")}
  48. */
  49. const DOMUtils = window.DOMUtils.noConflict();
  50. Qmsg.config({
  51. position: "top",
  52. html: true,
  53. maxNums: 4,
  54. autoClose: true,
  55. showClose: false,
  56. showReverse: false,
  57. });
  58. const log = new utils.Log(GM_info, unsafeWindow.console || console);
  59. log.config({
  60. debug: false,
  61. });
  62. const httpx = new utils.Httpx(GM_xmlhttpRequest);
  63. httpx.config({
  64. onabort() {
  65. Qmsg.error("请求被取消");
  66. },
  67. ontimeout() {
  68. Qmsg.error("请求超时");
  69. },
  70. onerror(response) {
  71. Qmsg.error("请求异常");
  72. log.error(["httpx-onerror", response]);
  73. },
  74. });
  75. /* -----------------↑公共配置↑----------------- */
  76.  
  77. /* -----------------↓函数区域↓----------------- */
  78. /**
  79. * GreasyFork的api
  80. */
  81. const GreasyforkApi = {
  82. /**
  83. * 获取代码搜索地址
  84. * @param {string} url
  85. * @returns {string}
  86. */
  87. getCodeSearchUrl(url) {
  88. return "https://greasyfork.org/zh-CN/scripts/code-search?c=" + url;
  89. },
  90. /**
  91. * 获取管理地址
  92. * @param {string} url
  93. * @returns {string}
  94. */
  95. getAdminUrl(url) {
  96. return url + "/admin";
  97. },
  98. /**
  99. * 从字符串中提取Id
  100. * @param {?string} text
  101. * @returns {string}
  102. */
  103. getScriptId(text) {
  104. return (text || window.location.pathname).match(/\/scripts\/([\d]+)/i)[1];
  105. },
  106. /**
  107. * 从字符串中提取用户id
  108. * @param {?string} text
  109. * @returns {string}
  110. */
  111. getUserId(text) {
  112. return (text || window.location.pathname).match(/\/users\/([\d]+)/i)[1];
  113. },
  114. /**
  115. * 从字符串中提取脚本名
  116. * @param {?string} text
  117. * @returns {?string}
  118. */
  119. getScriptName(text) {
  120. let pathname = window.location.pathname;
  121. if (text != null) {
  122. pathname = new URL(text).pathname;
  123. }
  124. pathname = decodeURIComponent(pathname);
  125. pathname = pathname.split("/");
  126. for (const name of pathname) {
  127. if (name.match(/[\d]+/)) {
  128. return name.match(/[\d]+-(.+)/)[1];
  129. }
  130. }
  131. },
  132. /**
  133. * 获取需要切换语言的Url
  134. */
  135. getSwitchLanguageUrl(localeLanguage = "zh-CN") {
  136. let url = window.location.origin;
  137. let urlSplit = window.location.pathname.split("/");
  138. urlSplit[1] = localeLanguage;
  139. url = url + urlSplit.join("/");
  140. url += window.location.search;
  141. if (window.location.search === "") {
  142. url += "?locale_override=1";
  143. } else if (!window.location.search.includes("locale_override=1")) {
  144. url += "&locale_override=1";
  145. }
  146. return url;
  147. },
  148. /**
  149. * 获取脚本统计数据
  150. * @param {string} scriptId
  151. */
  152. async getScriptStats(scriptId) {
  153. return new Promise(async (resolve) => {
  154. let scriptStatsRequest = await httpx.get({
  155. url: `https://greasyfork.org/scripts/${scriptId}/stats.json`,
  156. fetch: true,
  157. onerror() {},
  158. ontimeout() {},
  159. });
  160. if (!scriptStatsRequest.status) {
  161. resolve(false);
  162. return;
  163. }
  164. let scriptStatsJSON = scriptStatsRequest.data;
  165. resolve(scriptStatsJSON);
  166. });
  167. },
  168. /**
  169. * 解析并获取admin内的源代码同步的配置表单
  170. * @param {string} scriptId
  171. * @returns {Promise<?FormData>}
  172. */
  173. async getSourceCodeSyncFormData(scriptId) {
  174. let getResp = await httpx.get(
  175. `https://greasyfork.org/zh-CN/scripts/${scriptId}/admin`,
  176. {
  177. fetch: true,
  178. }
  179. );
  180. log.success(getResp);
  181. if (!getResp.status) {
  182. Qmsg.error("请求admin内容失败");
  183. return;
  184. }
  185. let adminHTML = getResp.data.responseText;
  186. let adminHTMLElement = DOMUtils.parseHTML(adminHTML, false, true);
  187. let formElement = adminHTMLElement.querySelector("form.edit_script");
  188. if (!formElement) {
  189. Qmsg.error("解析admin的源代码同步表单失败");
  190. return;
  191. }
  192. let formData = new FormData(formElement);
  193. return formData;
  194. },
  195. /**
  196. * 进行源代码同步,要求先getSourceCodeSyncFormData
  197. * @param {string} scriptId
  198. * @param {FormData} data
  199. * @returns {Promise<?Response>}
  200. */
  201. async sourceCodeSync(scriptId, data) {
  202. let postResp = await httpx.post(
  203. `https://greasyfork.org/zh-CN/scripts/${scriptId}/sync_update`,
  204. {
  205. fetch: true,
  206. data: data,
  207. }
  208. );
  209. log.success(postResp);
  210. if (!postResp.status) {
  211. Qmsg.error("源代码同步失败");
  212. return;
  213. }
  214. return postResp;
  215. },
  216. /**
  217. * 获取用户的信息,包括脚本列表、未上架的脚本、库
  218. * @returns {Promise<?{
  219. * id: number,
  220. * name: string,
  221. * scripts: GreasyForkScriptInfo[],
  222. * scriptList: GreasyForkScriptInfo[],
  223. * scriptLibraryList: GreasyForkScriptInfo[],
  224. * url: string,
  225. * }>}
  226. */
  227. async getUserInfo(userId) {
  228. let getResp = await httpx.get(
  229. `https://greasyfork.org/zh-CN/users/${userId}.json`,
  230. {
  231. fetch: true,
  232. }
  233. );
  234. log.success(getResp);
  235. if (!getResp.status) {
  236. Qmsg.error("获取用户信息失败");
  237. return;
  238. }
  239. let data = utils.toJSON(getResp.data.responseText);
  240. data["scriptList"] = [];
  241. data["scriptLibraryList"] = [];
  242. data["scripts"].forEach((scriptInfo) => {
  243. if (scriptInfo["code_url"].endsWith(".user.js")) {
  244. data["scriptList"].push(scriptInfo);
  245. } else {
  246. data["scriptLibraryList"].push(scriptInfo);
  247. }
  248. });
  249. return data;
  250. },
  251. /**
  252. * 获取用户的收藏集
  253. * @param {string} userId
  254. * @returns {Promise<?{
  255. * id: string,
  256. * name: string,
  257. * }[]>}
  258. */
  259. async getUserCollection(userId) {
  260. let getResp = await httpx.get(
  261. `https://greasyfork.org/zh-CN/users/${userId}`,
  262. {
  263. fetch: true,
  264. }
  265. );
  266. log.info(["获取用户的收藏集", getResp]);
  267. if (!getResp.status) {
  268. Qmsg.error("获取用户的收藏集失败");
  269. return;
  270. }
  271. let respText = getResp.data.responseText;
  272. let respDocument = DOMUtils.parseHTML(respText, true, true);
  273. let userScriptSets = respDocument.querySelector("#user-script-sets");
  274. if (!userScriptSets) {
  275. log.error("解析Script Sets失败");
  276. return;
  277. }
  278. let scriptSetsIdList = [];
  279. userScriptSets.querySelectorAll("li").forEach((liElement) => {
  280. let setsUrl = liElement.querySelector("a:last-child").href;
  281. if (setsUrl.includes("?fav=1")) {
  282. /* 自带的收藏夹 */
  283. return;
  284. }
  285. let setsName = liElement.querySelector("a").innerText;
  286. let setsId = setsUrl.match(/\/sets\/([\d]+)\//)[1];
  287. scriptSetsIdList.push({
  288. id: setsId,
  289. name: setsName,
  290. });
  291. });
  292.  
  293. return scriptSetsIdList;
  294. },
  295. /**
  296. * 获取某个收藏集的信息
  297. * @param {string} userId 用户id
  298. * @param {string} setsId 收藏集id
  299. * @returns {Promise<?FormData>}
  300. */
  301. async getUserCollectionInfo(userId, setsId) {
  302. let getResp = await httpx.get(
  303. `https://greasyfork.org/zh-CN/users/${userId}/sets/${setsId}/edit`,
  304. {
  305. fetch: true,
  306. }
  307. );
  308. if (!getResp.status) {
  309. Qmsg.error(`获取收藏集${setsId}失败`);
  310. return;
  311. }
  312. let respText = getResp.data.responseText;
  313. let respDocument = DOMUtils.parseHTML(respText, true, true);
  314. let edit_script_set_form = respDocument.querySelector(
  315. 'form[id^="edit_script_set"]'
  316. );
  317. if (!edit_script_set_form) {
  318. Qmsg.error("获取表单元素#edit_script_set失败");
  319. return;
  320. }
  321. let formData = new FormData(edit_script_set_form);
  322. let csrfToken = respDocument.querySelector('meta[name="csrf-token"]');
  323. if (csrfToken.hasAttribute("content")) {
  324. let authenticity_token = csrfToken.getAttribute("content");
  325. formData.set("authenticity_token", authenticity_token);
  326. }
  327. return formData;
  328. },
  329. /**
  330. * 更新用户的某个收藏集的表单信息
  331. * @param {string} userId 用户id
  332. * @param {string} setsId 收藏集id
  333. * @param {string} data
  334. * @param {Promise<?Document>}
  335. */
  336. async updateUserSetsInfo(userId, setsId, data) {
  337. let postResp = await httpx.post(
  338. `https://greasyfork.org/zh-CN/users/${userId}/sets/${setsId}`,
  339. {
  340. fetch: true,
  341. headers: {
  342. accept:
  343. "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
  344. "accept-language":
  345. "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
  346. "cache-control": "no-cache",
  347. "content-type": "application/x-www-form-urlencoded",
  348. pragma: "no-cache",
  349. },
  350. fetchInit: {
  351. referrerPolicy: "strict-origin-when-cross-origin",
  352. },
  353. data: data,
  354. }
  355. );
  356. if (!postResp.status) {
  357. Qmsg.error("更新收藏集表单请求失败");
  358. return;
  359. }
  360. let respText = postResp.data.responseText;
  361. let respDocument = DOMUtils.parseHTML(respText, true, true);
  362. return respDocument;
  363. },
  364. /**
  365. * 切换语言
  366. * @param {string} url
  367. */
  368. async switchLanguage(url) {
  369. let getResp = await httpx.get(url, {
  370. fetch: true,
  371. headers: {
  372. "Upgrade-Insecure-Requests": 1,
  373. },
  374. });
  375. if (!getResp.status) {
  376. return;
  377. }
  378. log.info(getResp);
  379. },
  380. };
  381.  
  382. /**
  383. * GreasyFork的css
  384. */
  385. const GreasyforkCSS = {
  386. UIScriptListCSS: `
  387. .w-script-list-item {
  388. padding: 10px 0;
  389. border-bottom: 1px solid #e5e5e5;
  390. font-size: 16px;
  391. text-align: left;
  392. }
  393. .w-script-version,
  394. .w-script-fan-score,
  395. .w-script-create-time,
  396. .w-script-update-time,
  397. .w-script-locale,
  398. .w-script-sync-type{
  399. font-size: 14px;
  400. color: #7c7c7c;
  401. }
  402. .w-script-fan-score {
  403. margin-left: unset !important;
  404. text-align: unset !important;
  405. max-width: unset !important;
  406. }
  407. .w-script-deleted{
  408. text-decoration: line-through;
  409. font-style: italic;
  410. color: red;
  411. }
  412. .w-script-deleted .w-script-name::before {
  413. content: "【删除】";
  414. }
  415. `,
  416. OwnCSS: `
  417. .whitesev-hide{
  418. display: none;
  419. }
  420. .whitesev-hide-important{
  421. display: none !important;
  422. }
  423. `,
  424. /**
  425. * 初始化
  426. */
  427. init() {
  428. GM_addStyle(this.OwnCSS);
  429. },
  430. };
  431.  
  432. /**
  433. * 配置面板
  434. */
  435. const PopsPanel = {
  436. /**
  437. * 本地存储的总键名
  438. */
  439. key: "GM_Panel",
  440. /**
  441. * 属性attributes的data-key
  442. */
  443. attributeDataKey_Name: "data-key",
  444. /**
  445. * 属性attributes的data-default-value
  446. */
  447. attributeDataDefaultValue_Name: "data-default-value",
  448. /**
  449. * 初始化菜单
  450. */
  451. initMenu() {
  452. this.initLocalDefaultValue();
  453. if (unsafeWindow.top !== unsafeWindow.self) {
  454. return;
  455. }
  456. GreasyforkMenu.menu.add([
  457. {
  458. key: "show_pops_panel_setting",
  459. text: "⚙ 设置",
  460. autoReload: false,
  461. isStoreValue: false,
  462. showText(text) {
  463. return text;
  464. },
  465. callback: () => {
  466. this.showPanel();
  467. },
  468. },
  469. {
  470. key: "transfer_old_data",
  471. text: "🔧 迁移旧数据",
  472. autoReload: false,
  473. isStoreValue: false,
  474. showText(text) {
  475. return text;
  476. },
  477. callback: () => {
  478. this.transferOldData();
  479. },
  480. },
  481. ]);
  482. },
  483. /**
  484. * 初始化本地设置默认的值
  485. */
  486. initLocalDefaultValue() {
  487. let content = this.getContent();
  488. content.forEach((item) => {
  489. if (!item["forms"]) {
  490. return;
  491. }
  492. item.forms.forEach((__item__) => {
  493. if (__item__.forms) {
  494. __item__.forms.forEach((containerItem) => {
  495. if (!containerItem.attributes) {
  496. return;
  497. }
  498. let key = containerItem.attributes[this.attributeDataKey_Name];
  499. let defaultValue =
  500. containerItem.attributes[this.attributeDataDefaultValue_Name];
  501. if (this.getValue(key) == null) {
  502. this.setValue(key, defaultValue);
  503. }
  504. });
  505. } else {
  506. }
  507. });
  508. });
  509. },
  510. /**
  511. * 设置值
  512. * @param {string} key 键
  513. * @param {any} value 值
  514. */
  515. setValue(key, value) {
  516. let localValue = GM_getValue(this.key, {});
  517. localValue[key] = value;
  518. GM_setValue(this.key, localValue);
  519. },
  520. /**
  521. * 获取值
  522. * @param {string} key 键
  523. * @param {any} defaultValue 默认值
  524. * @returns {any}
  525. */
  526. getValue(key, defaultValue) {
  527. let localValue = GM_getValue(this.key, {});
  528. return localValue[key] ?? defaultValue;
  529. },
  530. /**
  531. * 删除值
  532. * @param {string} key 键
  533. */
  534. deleteValue(key) {
  535. let localValue = GM_getValue(this.key, {});
  536. delete localValue[key];
  537. GM_setValue(this.key, localValue);
  538. },
  539. /**
  540. * 显示设置面板
  541. */
  542. showPanel() {
  543. pops.panel({
  544. title: {
  545. text: `${GM_info?.script?.name || "GreasyFork优化"}-设置`,
  546. position: "center",
  547. },
  548. content: this.getContent(),
  549. mask: {
  550. enable: true,
  551. clickEvent: {
  552. toClose: true,
  553. },
  554. },
  555. style: GreasyforkCSS.UIScriptListCSS,
  556. width: pops.isPhone() ? "92vw" : "800px",
  557. height: pops.isPhone() ? "80vh" : "600px",
  558. only: true,
  559. drag: true,
  560. });
  561. },
  562. /**
  563. * 获取按钮配置
  564. * @param {string} text 文字
  565. * @param {string|undefined} description 描述
  566. * @param {string} key 键
  567. * @param {boolean} defaultValue 默认值
  568. * @param {?(event:Event,value: boolean)=>boolean} _callback_ 点击回调
  569. */
  570. getSwtichDetail(text, description, key, defaultValue, _callback_) {
  571. let result = {
  572. text: text,
  573. description: description,
  574. type: "switch",
  575. attributes: {},
  576. getValue() {
  577. if (PopsPanel.getValue(key) == null) {
  578. PopsPanel.setValue(key, Boolean(defaultValue));
  579. }
  580. return Boolean(PopsPanel.getValue(key, defaultValue));
  581. },
  582. callback(event, value) {
  583. log.success(`${value ? "开启" : "关闭"} ${text}`);
  584. if (typeof _callback_ === "function") {
  585. if (_callback_(event, value)) {
  586. return;
  587. }
  588. }
  589. PopsPanel.setValue(key, Boolean(value));
  590. },
  591. };
  592. result.attributes[this.attributeDataKey_Name] = key;
  593. result.attributes[this.attributeDataDefaultValue_Name] =
  594. Boolean(defaultValue);
  595. return result;
  596. },
  597. /**
  598. * 获取配置内容
  599. * @returns {PopsPanelContentConfig[]}
  600. */
  601. getContent() {
  602. return [
  603. {
  604. id: "greasy-fork-panel-config-account",
  605. title: "账号",
  606. forms: [
  607. {
  608. text: "账号/密码",
  609. type: "forms",
  610. forms: [
  611. {
  612. text: "账号",
  613. type: "input",
  614. attributes: {
  615. "data-key": "user",
  616. "data-default-value": "",
  617. },
  618. getValue() {
  619. return PopsPanel.getValue(
  620. this.attributes["data-key"],
  621. this.attributes["data-default-value"]
  622. );
  623. },
  624. callback(event, value) {
  625. PopsPanel.setValue(this.attributes["data-key"], value);
  626. },
  627. placeholder: "请输入账号",
  628. },
  629. {
  630. text: "密码",
  631. type: "input",
  632. attributes: {
  633. "data-key": "pwd",
  634. "data-default-value": "",
  635. },
  636. getValue() {
  637. return PopsPanel.getValue(
  638. this.attributes["data-key"],
  639. this.attributes["data-default-value"]
  640. );
  641. },
  642. callback(event, value) {
  643. PopsPanel.setValue(this.attributes["data-key"], value);
  644. },
  645. isPassword: true,
  646. placeholder: "请输入密码",
  647. },
  648. ],
  649. },
  650. {
  651. text: "功能",
  652. type: "forms",
  653. forms: [
  654. PopsPanel.getSwtichDetail(
  655. "自动登录",
  656. "自动登录当前保存的账号",
  657. "autoLogin",
  658. true
  659. ),
  660. {
  661. text: "清空账号/密码",
  662. type: "button",
  663. buttonIconIsLoading: false,
  664. buttonType: "default",
  665. buttonText: "点击清空",
  666. callback(event) {
  667. if (confirm("确定清空账号和密码?")) {
  668. PopsPanel.deleteValue("user");
  669. PopsPanel.deleteValue("pwd");
  670. Qmsg.success("已清空账号/密码");
  671. let $shadowRoot = event.target.getRootNode();
  672. $shadowRoot.querySelector(
  673. `li[data-key="user"] .pops-panel-input input`
  674. ).value = "";
  675. $shadowRoot.querySelector(
  676. `li[data-key="pwd"] .pops-panel-input input`
  677. ).value = "";
  678. }
  679. },
  680. },
  681. {
  682. text: "源代码同步【脚本列表】",
  683. type: "button",
  684. buttonIconIsLoading: false,
  685. buttonType: "primary",
  686. buttonText: "一键同步",
  687. callback(event) {
  688. if (!window.location.pathname.match(/\/.+\/users\/.+/gi)) {
  689. PopsPanel.setValue(
  690. "goto_updateSettingsAndSynchronize_scriptList",
  691. true
  692. );
  693. if (GreasyforkMenu.getUserLinkElement()) {
  694. Qmsg.success("前往用户主页");
  695. window.location.href =
  696. GreasyforkMenu.getUserLinkElement().href;
  697. } else {
  698. Qmsg.error("获取当前已登录的用户主页失败");
  699. }
  700. return;
  701. }
  702. let scriptUrlList = [];
  703. document
  704. .querySelectorAll(
  705. "#user-script-list-section li a.script-link"
  706. )
  707. .forEach((item) => {
  708. scriptUrlList = scriptUrlList.concat(
  709. GreasyforkApi.getAdminUrl(item.href)
  710. );
  711. });
  712. GreasyforkMenu.updateScript(scriptUrlList);
  713. },
  714. },
  715. {
  716. text: "源代码同步【未上架的脚本】",
  717. type: "button",
  718. buttonIconIsLoading: false,
  719. buttonType: "primary",
  720. buttonText: "一键同步",
  721. callback(event) {
  722. if (!window.location.pathname.match(/\/.+\/users\/.+/gi)) {
  723. PopsPanel.setValue(
  724. "goto_updateSettingsAndSynchronize_unlistedScriptList",
  725. true
  726. );
  727. if (GreasyforkMenu.getUserLinkElement()) {
  728. Qmsg.success("前往用户主页");
  729. window.location.href =
  730. GreasyforkMenu.getUserLinkElement().href;
  731. } else {
  732. Qmsg.error("获取当前已登录的用户主页失败");
  733. }
  734. return;
  735. }
  736. let scriptUrlList = [];
  737. document
  738. .querySelectorAll(
  739. "#user-unlisted-script-list li a.script-link"
  740. )
  741. .forEach((item) => {
  742. scriptUrlList = scriptUrlList.concat(
  743. GreasyforkApi.getAdminUrl(item.href)
  744. );
  745. });
  746. GreasyforkMenu.updateScript(scriptUrlList);
  747. },
  748. },
  749. {
  750. text: "源代码同步【库】",
  751. type: "button",
  752. buttonIconIsLoading: false,
  753. buttonType: "primary",
  754. buttonText: "一键同步",
  755. callback(event) {
  756. if (!window.location.pathname.match(/\/.+\/users\/.+/gi)) {
  757. PopsPanel.setValue(
  758. "goto_updateSettingsAndSynchronize_libraryScriptList",
  759. true
  760. );
  761. if (GreasyforkMenu.getUserLinkElement()) {
  762. Qmsg.success("前往用户主页");
  763. window.location.href =
  764. GreasyforkMenu.getUserLinkElement().href;
  765. } else {
  766. Qmsg.error("获取当前已登录的用户主页失败");
  767. }
  768. return;
  769. }
  770. let scriptUrlList = [];
  771. document
  772. .querySelectorAll(
  773. "#user-library-script-list li a.script-link"
  774. )
  775. .forEach((item) => {
  776. scriptUrlList = scriptUrlList.concat(
  777. GreasyforkApi.getAdminUrl(item.href)
  778. );
  779. });
  780. GreasyforkMenu.updateScript(scriptUrlList);
  781. },
  782. },
  783. ],
  784. },
  785. ],
  786. },
  787. {
  788. id: "greasy-fork-panel-config-optimization",
  789. title: "优化",
  790. forms: [
  791. {
  792. text: "功能",
  793. type: "forms",
  794. forms: [
  795. {
  796. text: "固定当前语言",
  797. type: "select",
  798. attributes: {
  799. "data-key": "language-selector-locale",
  800. "data-default-value": "zh-CN",
  801. },
  802. getValue() {
  803. return PopsPanel.getValue(
  804. this.attributes["data-key"],
  805. this.attributes["data-default-value"]
  806. );
  807. },
  808. callback(event, isSelectedValue, isSelectedText) {
  809. PopsPanel.setValue(
  810. this.attributes["data-key"],
  811. isSelectedValue
  812. );
  813. },
  814. data: (function () {
  815. let result = [
  816. {
  817. value: "",
  818. text: "无",
  819. },
  820. ];
  821. document
  822. .querySelectorAll(
  823. "select#language-selector-locale option"
  824. )
  825. .forEach((element) => {
  826. let value = element.getAttribute("value");
  827. if (value === "help") {
  828. return;
  829. }
  830. let text = (
  831. element.innerText || element.textContent
  832. ).trim();
  833. result.push({
  834. value: value,
  835. text: text,
  836. });
  837. });
  838. return result;
  839. })(),
  840. },
  841. PopsPanel.getSwtichDetail(
  842. "美化页面元素",
  843. "如button、input、textarea",
  844. "beautifyPage",
  845. true
  846. ),
  847. PopsPanel.getSwtichDetail(
  848. "美化历史版本页面",
  849. "更直观的查看版本迭代",
  850. "beautifyHistoryVersionPage",
  851. true
  852. ),
  853. PopsPanel.getSwtichDetail(
  854. "美化上传图片按钮",
  855. "放大上传区域",
  856. "beautifyUploadImage",
  857. true
  858. ),
  859. PopsPanel.getSwtichDetail(
  860. "【代码】页面添加复制代码按钮",
  861. "更优雅的复制",
  862. "addCopyCodeButton",
  863. true
  864. ),
  865. PopsPanel.getSwtichDetail(
  866. "【代码】页面快捷键",
  867. "【F】键全屏、【Alt+Shift+F】键宽屏",
  868. "fullScreenOptimization",
  869. true
  870. ),
  871. PopsPanel.getSwtichDetail(
  872. "优化图片浏览",
  873. "使用Viewer浏览图片",
  874. "optimizeImageBrowsing",
  875. true
  876. ),
  877. PopsPanel.getSwtichDetail(
  878. "覆盖图床图片跳转",
  879. "配合上面的【优化图片浏览】更优雅浏览图片",
  880. "overlayBedImageClickEvent",
  881. true
  882. ),
  883. PopsPanel.getSwtichDetail(
  884. "美化Greasyfork Beautify脚本",
  885. '需安装Greasyfork Beautify脚本,<a href="https://greasyfork.org/zh-CN/scripts/446849-greasyfork-beautify" target="_blank">🖐点我安装</a>',
  886. "beautifyGreasyforkBeautify",
  887. true
  888. ),
  889. ],
  890. },
  891. ],
  892. },
  893. {
  894. id: "greasy-fork-panel-config-discussions",
  895. title: "论坛",
  896. forms: [
  897. {
  898. text: "功能",
  899. type: "forms",
  900. forms: [
  901. this.getSwtichDetail(
  902. "过滤重复的评论",
  903. "过滤掉重复的评论数量(≥2)",
  904. "greasyfork-discussions-filter-duplicate-comments",
  905. false,
  906. undefined
  907. ),
  908. ],
  909. },
  910. {
  911. text: "过滤脚本(id)",
  912. type: "forms",
  913. forms: [
  914. {
  915. type: "own",
  916. getLiElementCallBack(liElement) {
  917. let textareaDiv = DOMUtils.createElement(
  918. "div",
  919. {
  920. className: "pops-panel-textarea",
  921. innerHTML: `<textarea placeholder="请输入脚本id,每行一个"></textarea>`,
  922. },
  923. {
  924. style: "width: 100%;",
  925. }
  926. );
  927. let textarea = textareaDiv.querySelector("textarea");
  928. const KEY = "greasyfork-discussions-filter-script";
  929. textarea.value = PopsPanel.getValue(KEY, "");
  930. DOMUtils.on(textarea, "input", undefined, function (event) {
  931. PopsPanel.setValue(KEY, event.target.value);
  932. });
  933. liElement.appendChild(textareaDiv);
  934. return liElement;
  935. },
  936. },
  937. ],
  938. },
  939. {
  940. text: "过滤发布的用户(id)",
  941. type: "forms",
  942. forms: [
  943. {
  944. type: "own",
  945. getLiElementCallBack(liElement) {
  946. let textareaDiv = DOMUtils.createElement(
  947. "div",
  948. {
  949. className: "pops-panel-textarea",
  950. innerHTML: `<textarea placeholder="请输入用户id,每行一个"></textarea>`,
  951. },
  952. {
  953. style: "width: 100%;",
  954. }
  955. );
  956. let textarea = textareaDiv.querySelector("textarea");
  957. const KEY = "greasyfork-discussions-filter-post-user";
  958. textarea.value = PopsPanel.getValue(KEY, "");
  959. DOMUtils.on(textarea, "input", undefined, function (event) {
  960. PopsPanel.setValue(KEY, event.target.value);
  961. });
  962. liElement.appendChild(textareaDiv);
  963. return liElement;
  964. },
  965. },
  966. ],
  967. },
  968. {
  969. text: "过滤回复的用户(id)",
  970. type: "forms",
  971. forms: [
  972. {
  973. type: "own",
  974. getLiElementCallBack(liElement) {
  975. let textareaDiv = DOMUtils.createElement(
  976. "div",
  977. {
  978. className: "pops-panel-textarea",
  979. innerHTML: `<textarea placeholder="请输入用户id,每行一个"></textarea>`,
  980. },
  981. {
  982. style: "width: 100%;",
  983. }
  984. );
  985. let textarea = textareaDiv.querySelector("textarea");
  986. const KEY = "greasyfork-discussions-filter-reply-user";
  987. textarea.value = PopsPanel.getValue(KEY, "");
  988. DOMUtils.on(textarea, "input", undefined, function (event) {
  989. PopsPanel.setValue(KEY, event.target.value);
  990. });
  991. liElement.appendChild(textareaDiv);
  992. return liElement;
  993. },
  994. },
  995. ],
  996. },
  997. ],
  998. },
  999. {
  1000. id: "greasy-fork-panel-config-script-list",
  1001. title: "脚本列表",
  1002. callback(event, rightHeaderElement, rightContainerElement) {
  1003. Greasyfork.UIScriptList(
  1004. "script-list",
  1005. event,
  1006. rightHeaderElement,
  1007. rightContainerElement
  1008. );
  1009. },
  1010. forms: [],
  1011. },
  1012. {
  1013. id: "greasy-fork-panel-config-library",
  1014. title: "库",
  1015. callback(event, rightHeaderElement, rightContainerElement) {
  1016. Greasyfork.UIScriptList(
  1017. "script-library",
  1018. event,
  1019. rightHeaderElement,
  1020. rightContainerElement
  1021. );
  1022. },
  1023. forms: [],
  1024. },
  1025. ];
  1026. },
  1027. /**
  1028. * 迁移旧数据
  1029. */
  1030. transferOldData() {
  1031. let oldData = GM_getValue("GM_Menu_Local_Map");
  1032. let currentData = GM_getValue(this.key, {});
  1033. if (oldData) {
  1034. Object.assign(currentData, oldData);
  1035. GM_setValue(this.key, currentData);
  1036. GM_deleteValue("GM_Menu_Local_Map");
  1037. Qmsg.success("共迁移数据量:" + Object.keys(oldData).length);
  1038. } else {
  1039. Qmsg.info("不存在旧数据");
  1040. }
  1041. },
  1042. };
  1043.  
  1044. /**
  1045. * GreasyFork的菜单
  1046. */
  1047. const GreasyforkMenu = {
  1048. /**
  1049. * @class
  1050. */
  1051. menu: new utils.GM_Menu({
  1052. GM_getValue,
  1053. GM_setValue,
  1054. GM_registerMenuCommand,
  1055. GM_unregisterMenuCommand,
  1056. }),
  1057. /**
  1058. * 当前是否已登录
  1059. */
  1060. isLogin: false,
  1061. /**
  1062. * 初始化环境变量
  1063. */
  1064. initEnv() {
  1065. let userLinkElement = this.getUserLinkElement();
  1066. this.isLogin = Boolean(userLinkElement);
  1067. },
  1068. /**
  1069. * 获取当前登录用户的a标签元素
  1070. * @returns {?HTMLAnchorElement}
  1071. */
  1072. getUserLinkElement() {
  1073. return document.querySelector("#nav-user-info span.user-profile-link a");
  1074. },
  1075. /**
  1076. * 更新脚本
  1077. * @param {string[]} scriptUrlList
  1078. */
  1079. async updateScript(scriptUrlList) {
  1080. let getLoadingHTML = function (scriptName, progress = 1) {
  1081. return `
  1082. <div style="display: flex;flex-direction: column;align-items: flex-start;">
  1083. <div style="height: 30px;line-height: 30px;">名称:${scriptName}</div>
  1084. <div style="height: 30px;line-height: 30px;">进度:${progress}/${scriptUrlList.length}</div>
  1085. </div>`;
  1086. };
  1087. if (utils.isNull(scriptUrlList)) {
  1088. Qmsg.error("未获取到【脚本列表】");
  1089. } else {
  1090. let loading = Qmsg.loading(
  1091. getLoadingHTML(GreasyforkApi.getScriptName(scriptUrlList[0])),
  1092. {
  1093. html: true,
  1094. }
  1095. );
  1096. let successNums = 0;
  1097. let failedNums = 0;
  1098. for (let index = 0; index < scriptUrlList.length; index++) {
  1099. let scriptUrl = scriptUrlList[index];
  1100. let scriptId = GreasyforkApi.getScriptId(scriptUrl);
  1101. log.success("更新:" + scriptUrl);
  1102. let scriptName = GreasyforkApi.getScriptName(scriptUrl);
  1103. loading.setHTML(getLoadingHTML(scriptName, index + 1));
  1104. let codeSyncFormData = await GreasyforkApi.getSourceCodeSyncFormData(
  1105. scriptId
  1106. );
  1107. if (codeSyncFormData) {
  1108. let syncUpdateStatus = await GreasyforkApi.sourceCodeSync(
  1109. scriptId,
  1110. codeSyncFormData
  1111. );
  1112. if (syncUpdateStatus) {
  1113. Qmsg.success("源代码同步成功,3秒后更新下一个");
  1114. await utils.sleep(3000);
  1115. successNums++;
  1116. } else {
  1117. Qmsg.error("源代码同步失败");
  1118. failedNums++;
  1119. }
  1120. } else {
  1121. Qmsg.error("源代码同步失败");
  1122. failedNums++;
  1123. }
  1124. }
  1125. loading.close();
  1126. if (successNums === 0) {
  1127. Qmsg.error("全部更新失败");
  1128. } else {
  1129. Qmsg.success(
  1130. `全部更新完毕<br >
  1131. 成功:${successNums}<br >
  1132. 失败:${failedNums}<br >
  1133. 总计:${scriptUrlList.length}`,
  1134. {
  1135. html: true,
  1136. }
  1137. );
  1138. }
  1139. }
  1140. },
  1141. /**
  1142. * 处理本地的goto事件
  1143. */
  1144. handleLocalGotoCallBack() {
  1145. if (PopsPanel.getValue("goto_updateSettingsAndSynchronize_scriptList")) {
  1146. PopsPanel.deleteValue("goto_updateSettingsAndSynchronize_scriptList");
  1147. if (!window.location.pathname.match(/\/.+\/users\/.+/gi)) {
  1148. PopsPanel.setValue(
  1149. "goto_updateSettingsAndSynchronize_scriptList",
  1150. true
  1151. );
  1152. if (GreasyforkMenu.getUserLinkElement()) {
  1153. Qmsg.success("前往用户主页");
  1154. window.location.href = GreasyforkMenu.getUserLinkElement().href;
  1155. } else {
  1156. Qmsg.error("获取当前已登录的用户主页失败");
  1157. }
  1158. return;
  1159. }
  1160. let scriptUrlList = [];
  1161. document
  1162. .querySelectorAll("#user-script-list-section li a.script-link")
  1163. .forEach((item) => {
  1164. scriptUrlList = scriptUrlList.concat(
  1165. GreasyforkApi.getAdminUrl(item.href)
  1166. );
  1167. });
  1168. GreasyforkMenu.updateScript(scriptUrlList);
  1169. } else if (
  1170. PopsPanel.getValue(
  1171. "goto_updateSettingsAndSynchronize_unlistedScriptList"
  1172. )
  1173. ) {
  1174. PopsPanel.deleteValue(
  1175. "goto_updateSettingsAndSynchronize_unlistedScriptList"
  1176. );
  1177. if (!window.location.pathname.match(/\/.+\/users\/.+/gi)) {
  1178. PopsPanel.setValue(
  1179. "goto_updateSettingsAndSynchronize_unlistedScriptList",
  1180. true
  1181. );
  1182. if (GreasyforkMenu.getUserLinkElement()) {
  1183. Qmsg.success("前往用户主页");
  1184. window.location.href = GreasyforkMenu.getUserLinkElement().href;
  1185. } else {
  1186. Qmsg.error("获取当前已登录的用户主页失败");
  1187. }
  1188. return;
  1189. }
  1190. let scriptUrlList = [];
  1191. document
  1192. .querySelectorAll("#user-unlisted-script-list li a.script-link")
  1193. .forEach((item) => {
  1194. scriptUrlList = scriptUrlList.concat(
  1195. GreasyforkApi.getAdminUrl(item.href)
  1196. );
  1197. });
  1198. GreasyforkMenu.updateScript(scriptUrlList);
  1199. } else if (
  1200. PopsPanel.getValue(
  1201. "goto_updateSettingsAndSynchronize_libraryScriptList"
  1202. )
  1203. ) {
  1204. PopsPanel.deleteValue(
  1205. "goto_updateSettingsAndSynchronize_libraryScriptList"
  1206. );
  1207. if (!window.location.pathname.match(/\/.+\/users\/.+/gi)) {
  1208. PopsPanel.setValue(
  1209. "goto_updateSettingsAndSynchronize_libraryScriptList",
  1210. true
  1211. );
  1212. if (GreasyforkMenu.getUserLinkElement()) {
  1213. Qmsg.success("前往用户主页");
  1214. window.location.href = GreasyforkMenu.getUserLinkElement().href;
  1215. } else {
  1216. Qmsg.error("获取当前已登录的用户主页失败");
  1217. }
  1218. return;
  1219. }
  1220. let scriptUrlList = [];
  1221. document
  1222. .querySelectorAll("#user-library-script-list li a.script-link")
  1223. .forEach((item) => {
  1224. scriptUrlList = scriptUrlList.concat(
  1225. GreasyforkApi.getAdminUrl(item.href)
  1226. );
  1227. });
  1228. GreasyforkMenu.updateScript(scriptUrlList);
  1229. }
  1230. },
  1231. };
  1232.  
  1233. /**
  1234. * GreasyFork的业务功能
  1235. */
  1236. const Greasyfork = {
  1237. /**
  1238. * 自动登录
  1239. */
  1240. autoLogin() {
  1241. utils.waitNode("span.sign-in-link a[rel=nofollow]").then(async () => {
  1242. let user = PopsPanel.getValue("user");
  1243. let pwd = PopsPanel.getValue("pwd");
  1244. if (utils.isNull(user)) {
  1245. Qmsg.error("请先在菜单中录入账号");
  1246. return;
  1247. }
  1248. if (utils.isNull(pwd)) {
  1249. Qmsg.error("请先在菜单中录入密码");
  1250. return;
  1251. }
  1252. let csrfToken = document.querySelector("meta[name='csrf-token']");
  1253. if (!csrfToken) {
  1254. Qmsg.error("获取csrf-token失败");
  1255. return;
  1256. }
  1257. let loginTip = Qmsg.loading("正在登录中...");
  1258. let postResp = await httpx.post(
  1259. "https://greasyfork.org/zh-CN/users/sign_in",
  1260. {
  1261. fetch: true,
  1262. data: encodeURI(
  1263. `authenticity_token=${csrfToken.getAttribute(
  1264. "content"
  1265. )}&user[email]=${user}&user[password]=${pwd}&user[remember_me]=1&commit=登录`
  1266. ),
  1267. headers: {
  1268. "Content-Type": "application/x-www-form-urlencoded",
  1269. },
  1270. }
  1271. );
  1272. loginTip.destroy();
  1273. if (!postResp.status) {
  1274. log.error(postResp);
  1275. Qmsg.error("登录失败,请在控制台查看原因");
  1276. return;
  1277. }
  1278. let respText = postResp.data.responseText;
  1279. let parseLoginHTMLNode = DOMUtils.parseHTML(respText, true, true);
  1280. if (
  1281. parseLoginHTMLNode.querySelectorAll(
  1282. ".sign-out-link a[rel=nofollow][data-method='delete']"
  1283. ).length
  1284. ) {
  1285. Qmsg.success("登录成功,1s后自动跳转");
  1286. setTimeout(() => {
  1287. window.location.reload();
  1288. }, 1000);
  1289. } else {
  1290. log.error(postResp);
  1291. log.error(`当前账号:${user}`);
  1292. log.error(`当前密码:${pwd}`);
  1293. Qmsg.error("登录失败,可能是账号/密码错误,请在控制台查看原因");
  1294. }
  1295. });
  1296. },
  1297. /**
  1298. * 设置代码搜索按钮(对于库)
  1299. */
  1300. setFindCodeSearchBtn() {
  1301. utils.waitNode("ul#script-links li.current span").then(() => {
  1302. let searchBtn = DOMUtils.createElement("li", {
  1303. innerHTML: `<a href="javascript:;"><span>寻找引用</span></a>`,
  1304. });
  1305. DOMUtils.append(document.querySelector("ul#script-links"), searchBtn);
  1306. DOMUtils.on(searchBtn, "click", async function () {
  1307. let scriptId = window.location.pathname.match(/scripts\/([\d]+)/i);
  1308. if (!scriptId) {
  1309. log.error([scriptId, window.location.pathname]);
  1310. Qmsg.error("获取脚本id失败");
  1311. return;
  1312. }
  1313. scriptId = scriptId[scriptId.length - 1];
  1314. window.location.href = GreasyforkApi.getCodeSearchUrl(
  1315. `greasyfork.org/scripts/${scriptId}`
  1316. );
  1317. });
  1318. });
  1319. },
  1320. /**
  1321. * 添加收藏按钮
  1322. */
  1323. setCollectScriptBtn() {
  1324. utils.waitNode("ul#script-links li.current span").then(() => {
  1325. let collectBtn = DOMUtils.createElement("li", {
  1326. innerHTML: `<a href="javascript:;"><span>收藏</span></a>`,
  1327. });
  1328. DOMUtils.append(document.querySelector("ul#script-links"), collectBtn);
  1329. DOMUtils.on(collectBtn, "click", async function () {
  1330. let scriptId = window.location.pathname.match(/scripts\/([\d]+)/i);
  1331. if (!scriptId) {
  1332. log.error([scriptId, window.location.pathname]);
  1333. Qmsg.error("获取脚本id失败");
  1334. return;
  1335. }
  1336. scriptId = scriptId[scriptId.length - 1];
  1337. if (!GreasyforkMenu.isLogin) {
  1338. Qmsg.error("请先登录账号");
  1339. log.error("请先登录账号");
  1340. return;
  1341. }
  1342. let userId = GreasyforkApi.getUserId(
  1343. GreasyforkMenu.getUserLinkElement().href
  1344. );
  1345. if (userId == null) {
  1346. Qmsg.error("获取用户id失败");
  1347. log.error("获取用户id失败");
  1348. return;
  1349. }
  1350. let loading = Qmsg.loading("获取收藏夹中...");
  1351. let userCollection = await GreasyforkApi.getUserCollection(userId);
  1352. loading.close();
  1353. if (!userCollection) {
  1354. return;
  1355. }
  1356. let alertHTML = "";
  1357. userCollection.forEach((userCollectInfo) => {
  1358. alertHTML += `
  1359. <li class="user-collect-item" data-id="${userCollectInfo.id}" data-name="${userCollectInfo.name}">
  1360. <div class="user-collect-name">${userCollectInfo.name}</div>
  1361. <div class="user-collect-btn-container">
  1362. <div class="pops-panel-button collect-add-script-id">
  1363. <button type="primary" data-icon="" data-righticon="">
  1364. <span>添加</span>
  1365. </button>
  1366. </div>
  1367. <div class="pops-panel-button collect-delete-script-id">
  1368. <button type="danger" data-icon="" data-righticon="">
  1369. <span>删除</span>
  1370. </button>
  1371. </div>
  1372. </div>
  1373. </li>
  1374. `;
  1375. });
  1376. let collectionDialog = pops.alert({
  1377. title: {
  1378. text: "收藏集",
  1379. position: "center",
  1380. },
  1381. content: {
  1382. html: true,
  1383. text: `<ul>${alertHTML}</ul>`,
  1384. },
  1385. mask: {
  1386. enable: true,
  1387. clickEvent: {
  1388. toClose: true,
  1389. },
  1390. },
  1391. btn: {
  1392. ok: {
  1393. enable: false,
  1394. },
  1395. },
  1396. width: pops.isPhone() ? "92dvw" : "500px",
  1397. height: "auto",
  1398. drag: true,
  1399. only: true,
  1400. style: `
  1401. .pops{
  1402. --content-max-height: 400px;
  1403. max-height: var(--content-max-height);
  1404. }
  1405. .pops[type-value=alert] .pops-alert-content {
  1406. max-height: calc(var(--content-max-height) - var(--container-title-height) - var(--container-bottom-btn-height));
  1407. }
  1408. .user-collect-item{
  1409. -webkit-user-select: none;
  1410. user-select: none;
  1411. padding: 5px 10px;
  1412. display: flex;
  1413. align-items: center;
  1414. justify-content: space-between;
  1415. border-bottom: 1px dotted #c9c9c9;
  1416. }
  1417. .user-collect-name{
  1418.  
  1419. }
  1420. .user-collect-item:hover{
  1421. }
  1422. .user-collect-btn-container{
  1423. margin-left: 10px;
  1424. display: flex;
  1425. }
  1426. `,
  1427. });
  1428. /* 添加事件 */
  1429. DOMUtils.on(
  1430. collectionDialog.$shadowRoot,
  1431. "click",
  1432. ".collect-add-script-id",
  1433. async function (event) {
  1434. /** @type {HTMLLIElement} */
  1435. let currentSelectCollectInfo =
  1436. event.target.closest(".user-collect-item");
  1437. let setsId = currentSelectCollectInfo.dataset.id;
  1438. let setsName = currentSelectCollectInfo.dataset.name;
  1439. let loading = Qmsg.loading("添加中...");
  1440. let formData = await GreasyforkApi.getUserCollectionInfo(
  1441. userId,
  1442. setsId
  1443. );
  1444. let addFormData = utils.cloneFormData(formData);
  1445. let saveFormData = utils.cloneFormData(formData);
  1446. addFormData.set("add-script", scriptId);
  1447. addFormData.set("script-action", "i");
  1448. saveFormData.append("scripts-included[]", scriptId);
  1449. saveFormData.set("save", 1);
  1450. let addData = Array.from(new URLSearchParams(addFormData))
  1451. .map(
  1452. ([key, value]) =>
  1453. `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
  1454. )
  1455. .join("&");
  1456. let saveData = Array.from(new URLSearchParams(saveFormData))
  1457. .map(
  1458. ([key, value]) =>
  1459. `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
  1460. )
  1461. .join("&");
  1462. log.info(["添加的数据", addData]);
  1463. log.info(["保存的数据", saveData]);
  1464. let addResult = await GreasyforkApi.updateUserSetsInfo(
  1465. userId,
  1466. setsId,
  1467. addData
  1468. );
  1469. if (!addResult) {
  1470. loading.close();
  1471. return;
  1472. }
  1473. let changeScriptSet =
  1474. addResult.querySelector(".change-script-set");
  1475. let section = changeScriptSet.querySelector("section");
  1476. let alertElement = section.querySelector(".alert");
  1477. if (alertElement) {
  1478. pops.alert({
  1479. title: {
  1480. text: "添加失败",
  1481. position: "center",
  1482. },
  1483. content: {
  1484. text: alertElement.innerHTML,
  1485. html: true,
  1486. },
  1487. mask: {
  1488. enable: true,
  1489. clickEvent: {
  1490. toClose: true,
  1491. },
  1492. },
  1493. style: `
  1494. .pops-alert-content{
  1495. font-style: italic;
  1496. background-color: #ffc;
  1497. border: none;
  1498. border-left: 6px solid #FFEB3B;
  1499. padding: .5em;
  1500. }
  1501. `,
  1502. drag: true,
  1503. dragLimit: true,
  1504. width: pops.isPhone() ? "88vw" : "400px",
  1505. height: pops.isPhone() ? "50vh" : "300px",
  1506. });
  1507. } else {
  1508. await GreasyforkApi.updateUserSetsInfo(
  1509. userId,
  1510. setsId,
  1511. saveData
  1512. );
  1513. Qmsg.success("添加成功");
  1514. }
  1515. loading.close();
  1516. }
  1517. );
  1518. /* 删除事件 */
  1519. DOMUtils.on(
  1520. collectionDialog.$shadowRoot,
  1521. "click",
  1522. ".collect-delete-script-id",
  1523. async function (event) {
  1524. /** @type {HTMLLIElement} */
  1525. let currentSelectCollectInfo =
  1526. event.target.closest(".user-collect-item");
  1527. let setsId = currentSelectCollectInfo.dataset.id;
  1528. let setsName = currentSelectCollectInfo.dataset.name;
  1529. let loading = Qmsg.loading("删除中...");
  1530. let formData = await GreasyforkApi.getUserCollectionInfo(
  1531. userId,
  1532. setsId
  1533. );
  1534. let deleteFormData = new FormData();
  1535. let saveFormData = new FormData();
  1536. for (const [key, value] of formData.entries()) {
  1537. deleteFormData.append(key, value);
  1538. if (
  1539. key === "scripts-included[]" &&
  1540. value.toString() === scriptId.toString()
  1541. ) {
  1542. continue;
  1543. }
  1544. saveFormData.append(key, value);
  1545. }
  1546. deleteFormData.set("remove-scripts-included[]", scriptId);
  1547. deleteFormData.set("remove-selected-scripts", "i");
  1548. deleteFormData.delete("script-action");
  1549. saveFormData.set("save", 1);
  1550. let removeData = Array.from(new URLSearchParams(deleteFormData))
  1551. .map(
  1552. ([key, value]) =>
  1553. `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
  1554. )
  1555. .join("&");
  1556. let saveData = Array.from(new URLSearchParams(saveFormData))
  1557. .map(
  1558. ([key, value]) =>
  1559. `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
  1560. )
  1561. .join("&");
  1562. log.info(["删除的数据", removeData]);
  1563. log.info(["保存的数据", saveData]);
  1564. let removeResult = await GreasyforkApi.updateUserSetsInfo(
  1565. userId,
  1566. setsId,
  1567. removeData
  1568. );
  1569. if (!removeResult) {
  1570. loading.close();
  1571. return;
  1572. }
  1573. await GreasyforkApi.updateUserSetsInfo(userId, setsId, saveData);
  1574. Qmsg.success("删除成功");
  1575. loading.close();
  1576. }
  1577. );
  1578. });
  1579. });
  1580. },
  1581. /**
  1582. * 修复图片显示问题
  1583. */
  1584. repairImgShow() {
  1585. if (window.innerWidth < window.innerHeight) {
  1586. GM_addStyle(`
  1587. img.lum-img{
  1588. width: 100% !important;
  1589. height: 100% !important;
  1590. }
  1591. `);
  1592. }
  1593. },
  1594. /**
  1595. * 修复代码的行号显示不够问题
  1596. * 超过1w行不会高亮代码
  1597. */
  1598. repairCodeLineNumber() {
  1599. if (!window.location.pathname.split("/")?.includes("code")) {
  1600. return;
  1601. }
  1602. utils
  1603. .waitNode("#script-content div.code-container pre.prettyprint ol")
  1604. .then((element) => {
  1605. if (element.childElementCount >= 1000) {
  1606. log.success(
  1607. `当前代码行数${element.childElementCount}行,超过1000行,优化行号显示问题`
  1608. );
  1609. GM_addStyle(`
  1610. pre.prettyprint{
  1611. padding-left: 10px;
  1612. font-family: Monaco,Consolas,'Lucida Console','Courier New',serif;
  1613. font-size: 12px;
  1614. }
  1615. `);
  1616. }
  1617. });
  1618. },
  1619. /**
  1620. * 优化图片浏览
  1621. */
  1622. optimizeImageBrowsing() {
  1623. GM_addStyle(`
  1624. @media (max-width: 460px) {
  1625. .lum-lightbox-image-wrapper {
  1626. display:flex;
  1627. overflow: auto;
  1628. -webkit-overflow-scrolling: touch
  1629. }
  1630. .lum-lightbox-caption {
  1631. width: 100%;
  1632. position: absolute;
  1633. bottom: 0
  1634. }
  1635. .lum-lightbox-position-helper {
  1636. margin: auto
  1637. }
  1638. .lum-lightbox-inner img {
  1639. max-width:100%;
  1640. max-height:100%;
  1641. }
  1642. }
  1643. `);
  1644. /**
  1645. * 查看图片
  1646. * @param {Array} imgList
  1647. * @param {Number} _index_
  1648. */
  1649. function viewIMG(imgList = [], _index_ = 0) {
  1650. let viewerULNodeHTML = "";
  1651. imgList.forEach((item) => {
  1652. viewerULNodeHTML += `<li><img data-src="${item}" loading="lazy"></li>`;
  1653. });
  1654. let viewerULNode = DOMUtils.createElement("ul", {
  1655. innerHTML: viewerULNodeHTML,
  1656. });
  1657. /**
  1658. * @type {import("../库/Viewer")}
  1659. */
  1660. let viewer = new Viewer(viewerULNode, {
  1661. inline: false,
  1662. url: "data-src",
  1663. zIndex: utils.getMaxZIndex() + 100,
  1664. hidden: () => {
  1665. viewer.destroy();
  1666. },
  1667. });
  1668. _index_ = _index_ < 0 ? 0 : _index_;
  1669. viewer.view(_index_);
  1670. viewer.zoomTo(1);
  1671. viewer.show();
  1672. }
  1673. /**
  1674. * 获取<img>标签上的src属性
  1675. * @param {HTMLElement} element
  1676. * @returns {?string}
  1677. */
  1678. function getImgElementSrc(element) {
  1679. return (
  1680. element.getAttribute("data-src") ||
  1681. element.getAttribute("src") ||
  1682. element.getAttribute("alt")
  1683. );
  1684. }
  1685. DOMUtils.on(document, "click", "img", function (event) {
  1686. /**
  1687. * @type {HTMLElement}
  1688. */
  1689. let imgElement = event.target;
  1690. /* 在超链接标签里 */
  1691. if (
  1692. imgElement.parentElement?.localName === "a" &&
  1693. imgElement.hasAttribute("data-screenshots")
  1694. ) {
  1695. return;
  1696. }
  1697. /* Viewer的图片浏览 */
  1698. if (imgElement.closest(".viewer-container")) {
  1699. return;
  1700. }
  1701. /* GreasFork自带的图片浏览 */
  1702. if (imgElement.closest(".lum-lightbox-position-helper")) {
  1703. return;
  1704. }
  1705. /* 判断是否是user-content内的,如果是,多图片模式 */
  1706. let userContentElement = imgElement.closest(".user-content");
  1707. /* 图片链接数组 */
  1708. let imgList = [];
  1709. /* 当前图片的下标 */
  1710. let imgIndex = 0;
  1711. /* 图片元素数组 */
  1712. let imgElementList = [];
  1713. /* 当前的图片的链接 */
  1714. let currentImgSrc = getImgElementSrc(imgElement);
  1715. if (currentImgSrc.startsWith("https://img.shields.io")) {
  1716. /** shields.io的图标 */
  1717. return;
  1718. }
  1719. if (userContentElement) {
  1720. userContentElement
  1721. .querySelectorAll("img")
  1722. .forEach((childImgElement) => {
  1723. imgElementList.push(childImgElement);
  1724. let imgSrc = getImgElementSrc(childImgElement);
  1725. if (childImgElement.parentElement?.localName === "a") {
  1726. imgSrc =
  1727. childImgElement.parentElement.getAttribute("data-href") ||
  1728. childImgElement.parentElement.href;
  1729. }
  1730. imgList.push(imgSrc);
  1731. });
  1732. imgIndex = imgElementList.indexOf(imgElement);
  1733. if (imgIndex === -1) {
  1734. imgIndex = 0;
  1735. }
  1736. } else {
  1737. imgList.push(currentImgSrc);
  1738. imgIndex = 0;
  1739. }
  1740.  
  1741. log.success(["点击浏览图片👉", imgList, imgIndex]);
  1742. viewIMG(imgList, imgIndex);
  1743. });
  1744. /* 把上传的图片使用自定义图片预览 */
  1745. document.querySelectorAll(".user-screenshots").forEach((element) => {
  1746. let linkElement = element.querySelector("a");
  1747. let imgSrc =
  1748. linkElement.getAttribute("data-href") ||
  1749. linkElement.getAttribute("href");
  1750. let imgElement = element.querySelector("img");
  1751. imgElement.setAttribute("data-screenshots", true);
  1752. imgElement.setAttribute("data-src", imgSrc);
  1753. linkElement.setAttribute("href", "javascript:;");
  1754. /* img标签添加a标签后面 */
  1755. DOMUtils.after(linkElement, imgElement);
  1756. /* a标签删除 */
  1757. linkElement.remove();
  1758. });
  1759. },
  1760. /**
  1761. * 覆盖图床图片的parentElement的a标签
  1762. */
  1763. overlayBedImageClickEvent() {
  1764. document.querySelectorAll(".user-content a>img").forEach((imgElement) => {
  1765. let linkElement = imgElement.parentElement;
  1766. let url = linkElement.getAttribute("href");
  1767. linkElement.setAttribute("data-href", url);
  1768. linkElement.removeAttribute("href");
  1769. DOMUtils.on(linkElement, "click", undefined, function (event) {
  1770. Qmsg.warning(
  1771. `<div style="overflow-wrap: anywhere;">拦截跳转:<a href="${url}" target="_blank">${url}</a></div>`,
  1772. {
  1773. html: true,
  1774. timeout: 5000,
  1775. zIndex: utils.getMaxZIndex(),
  1776. }
  1777. );
  1778. });
  1779. });
  1780. },
  1781. /**
  1782. * 脚本首页新增今日更新
  1783. */
  1784. async scriptHomepageAddedTodaySUpdate() {
  1785. if (
  1786. !window.location.pathname.includes("/scripts/") ||
  1787. !document.querySelector("#install-area")
  1788. ) {
  1789. return;
  1790. }
  1791. let scriptStatsJSON = await GreasyforkApi.getScriptStats(
  1792. GreasyforkApi.getScriptId()
  1793. );
  1794. if (!scriptStatsJSON) {
  1795. return;
  1796. }
  1797. scriptStatsJSON = utils.toJSON(scriptStatsJSON.responseText);
  1798. log.info(["统计信息", scriptStatsJSON]);
  1799. let todayStatsJSON =
  1800. scriptStatsJSON[utils.formatTime(undefined, "yyyy-MM-dd")];
  1801. if (!todayStatsJSON) {
  1802. log.error("今日份的统计信息不存在");
  1803. return;
  1804. }
  1805. let update_checks = todayStatsJSON["update_checks"];
  1806. log.info(["今日统计信息", todayStatsJSON]);
  1807. DOMUtils.after(
  1808. "dd.script-show-daily-installs",
  1809. DOMUtils.createElement("dt", {
  1810. className: "script-show-daily-update_checks",
  1811. innerHTML: "<span>今日检查</span>",
  1812. })
  1813. );
  1814. DOMUtils.after(
  1815. "dt.script-show-daily-update_checks",
  1816. DOMUtils.createElement("dd", {
  1817. className: "script-show-daily-update_checks",
  1818. innerHTML: "<span>" + update_checks + "</span>",
  1819. })
  1820. );
  1821. },
  1822. /**
  1823. * 美化页面元素
  1824. */
  1825. beautifyPageElement() {
  1826. let beautifyMarkdownCSS = `
  1827. code{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:.85em;color:#000;background-color:#f0f0f0;border-radius:3px;padding:.2em 0}
  1828. table{text-indent:initial}
  1829. table{margin:10px 0 15px 0;border-collapse:collapse;border-spacing:0;display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}
  1830. code,pre{color:#333;background:0 0;font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.4;-moz-tab-size:8;-o-tab-size:8;tab-size:8;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}
  1831. pre{padding:.8em;overflow:auto;border-radius:3px;background:#f5f5f5}
  1832. :not(pre)>code{padding:.1em;border-radius:.3em;white-space:normal;background:#f5f5f5}
  1833. html body{font-family:"Helvetica Neue",Helvetica,"Segoe UI",Arial,freesans,sans-serif;font-size:16px;line-height:1.6;color:#333;background-color:#fff;overflow:initial;box-sizing:border-box;word-wrap:break-word}
  1834. html body>:first-child{margin-top:0}
  1835. html body h1,html body h2,html body h3,html body h4,html body h5,html body h6{line-height:1.2;margin-top:1em;margin-bottom:16px;color:#000}
  1836. html body h1{font-size:2.25em;font-weight:300;padding-bottom:.3em}
  1837. html body h2{font-size:1.75em;font-weight:400;padding-bottom:.3em}
  1838. html body h3{font-size:1.5em;font-weight:500}
  1839. html body h4{font-size:1.25em;font-weight:600}
  1840. html body h5{font-size:1.1em;font-weight:600}
  1841. html body h6{font-size:1em;font-weight:600}
  1842. html body h1,html body h2,html body h3,html body h4,html body h5{font-weight:600}
  1843. html body h5{font-size:1em}
  1844. html body h6{color:#5c5c5c}
  1845. html body strong{color:#000}
  1846. html body del{color:#5c5c5c}
  1847. html body a:not([href]){color:inherit;}
  1848. html body a{text-decoration:underline;text-underline-offset: .2rem;}
  1849. html body a:hover{color:#00a3f5;}
  1850. html body img{max-width:100%}
  1851. html body>p{margin-top:0;margin-bottom:16px;word-wrap:break-word}
  1852. html body>ol,html body>ul{margin-bottom:16px}
  1853. html body ol,html body ul{padding-left:2em}
  1854. html body ol.no-list,html body ul.no-list{padding:0;list-style-type:none}
  1855. html body ol ol,html body ol ul,html body ul ol,html body ul ul{margin-top:0;margin-bottom:0}
  1856. html body li{margin-bottom:0}
  1857. html body li.task-list-item{list-style:none}
  1858. html body li>p{margin-top:0;margin-bottom:0}
  1859. html body .task-list-item-checkbox{margin:0 .2em .25em -1.8em;vertical-align:middle}
  1860. html body .task-list-item-checkbox:hover{cursor:pointer}
  1861. html body blockquote{margin:16px 0;font-size:inherit;padding:0 15px;color:#5c5c5c;background-color:#f0f0f0;border-left:4px solid #d6d6d6 !important;}
  1862. html body blockquote>:first-child{margin-top:0}
  1863. html body blockquote>:last-child{margin-bottom:0}
  1864. html body hr{height:4px;margin:32px 0;background-color:#d6d6d6;border:0 none}
  1865. html body table{margin:10px 0 15px 0;border-collapse:collapse;border-spacing:0;display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}
  1866. html body table th{font-weight:700;color:#000}
  1867. html body table td,html body table th{border:1px solid #d6d6d6;padding:6px 13px}
  1868. html body dl{padding:0}
  1869. html body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}
  1870. html body dl dd{padding:0 16px;margin-bottom:16px}
  1871. html body code{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:.85em;color:#000;background-color:#f0f0f0;border-radius:3px;padding:.2em 0}
  1872. html body code::after,html body code::before{letter-spacing:-.2em;content:"\\00a0"}
  1873. html body pre>code{padding:0;margin:0;word-break:normal;white-space:pre;background:0 0;border:0}
  1874. html body .highlight{margin-bottom:16px}
  1875. html body .highlight pre,html body pre{padding:1em;overflow:auto;line-height:1.45;border:#d6d6d6;border-radius:3px}
  1876. html body .highlight pre{margin-bottom:0;word-break:normal}
  1877. html body pre code,html body pre tt{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}
  1878. html body pre code:after,html body pre code:before,html body pre tt:after,html body pre tt:before{content:normal}
  1879. html body blockquote,html body dl,html body ol,html body p,html body pre,html body ul{margin-top:0;margin-bottom:16px}
  1880. html body kbd{color:#000;border:1px solid #d6d6d6;border-bottom:2px solid #c7c7c7;padding:2px 4px;background-color:#f0f0f0;border-radius:3px}
  1881. @media print{html body{background-color:#fff}
  1882. html body h1,html body h2,html body h3,html body h4,html body h5,html body h6{color:#000;page-break-after:avoid}
  1883. html body blockquote{color:#5c5c5c}
  1884. html body pre{page-break-inside:avoid}
  1885. html body table{display:table}
  1886. html body img{display:block;max-width:100%;max-height:100%}
  1887. html body code,html body pre{word-wrap:break-word;white-space:pre}
  1888. }
  1889. .scrollbar-style::-webkit-scrollbar{width:8px}
  1890. .scrollbar-style::-webkit-scrollbar-track{border-radius:10px;background-color:transparent}
  1891. .scrollbar-style::-webkit-scrollbar-thumb{border-radius:5px;background-color:rgba(150,150,150,.66);border:4px solid rgba(150,150,150,.66);background-clip:content-box}
  1892. `;
  1893. let beautifyButtonCSS = `
  1894. /* 美化按钮 */
  1895. input[type="submit"],
  1896. button {
  1897. display: inline-flex;
  1898. justify-content: center;
  1899. align-items: center;
  1900. line-height: 1;
  1901. height: 32px;
  1902. white-space: nowrap;
  1903. cursor: pointer;
  1904. /* color: #606266; */
  1905. text-align: center;
  1906. box-sizing: border-box;
  1907. outline: none;
  1908. transition: .1s;
  1909. font-weight: 500;
  1910. user-select: none;
  1911. vertical-align: middle;
  1912. -webkit-appearance: none;
  1913. background-color: #ffffff;
  1914. border: 1px solid #dcdfe6;
  1915. border-color: #dcdfe6;
  1916. padding: 8px 15px;
  1917. font-size: 14px;
  1918. border-radius: 4px;
  1919. }
  1920. input[type="submit"]:hover,
  1921. input[type="submit"]:focus,
  1922. button:hover,
  1923. button:focus {
  1924. color: #409eff;
  1925. border-color: #c6e2ff;
  1926. background-color: #ecf5ff;
  1927. outline: none;
  1928. }
  1929.  
  1930. input[type="url"] {
  1931. position: relative;
  1932. font-size: 14px;
  1933. display: inline-flex;
  1934. line-height: 32px;
  1935. box-sizing: border-box;
  1936. vertical-align: middle;
  1937. -webkit-appearance: none;
  1938. /* color: #606266; */
  1939. padding: 0;
  1940. outline: none;
  1941. border: none;
  1942. background: none;
  1943. flex-grow: 1;
  1944. align-items: center;
  1945. justify-content: center;
  1946. padding: 1px 11px;
  1947. background-color: #ffffff;
  1948. background-image: none;
  1949. border-radius: 4px;
  1950. cursor: text;
  1951. transition: box-shadow .2s cubic-bezier(.645, .045, .355, 1);
  1952. transform: translateZ(0);
  1953. box-shadow: 0 0 0 1px #dcdfe6 inset;
  1954.  
  1955. width: 100%;
  1956. width: -moz-available;
  1957. width: -webkit-fill-available;
  1958. width: fill-available;
  1959.  
  1960. }
  1961. input[type="url"]::placeholder {
  1962. color: #a8abb2;
  1963. }
  1964. input[type="url"]:hover {
  1965. box-shadow: 0 0 0 1px #c0c4cc inset;
  1966. }
  1967. input[type="url"]:focus {
  1968. box-shadow: 0 0 0 1px #409eff inset;
  1969. }
  1970. `;
  1971. let beautifyRadioCSS = `
  1972. label.radio-label {
  1973. font-weight: 500;
  1974. position: relative;
  1975. cursor: pointer;
  1976. display: inline-flex;
  1977. align-items: center;
  1978. white-space: normal;
  1979. outline: none;
  1980. font-size: 14px;
  1981. user-select: none;
  1982. margin-right: 32px;
  1983. height: 32px;
  1984. padding: 4px;
  1985. border-radius: 4px;
  1986. box-sizing: border-box;
  1987. }
  1988. label:has(input[type=radio]:checked),
  1989. label:has(input[type=radio]:checked) a{
  1990. color: #409eff;
  1991. }
  1992. label.radio-label input[type="radio"]{
  1993. margin-right: 4px;
  1994. width: 14px;
  1995. height: 14px;
  1996. }
  1997. label.radio-label input[type="radio"]:checked{
  1998. -webkit-appearance: none;
  1999. -moz-appearance: none;
  2000. appearance: none;
  2001. border-radius: 50%;
  2002. width: 14px;
  2003. height: 14px;
  2004. outline: none;
  2005. border: 4px solid #409eff;
  2006. cursor: pointer;
  2007. }
  2008. label.radio-label input[type="radio"]:checked + span{
  2009. color: #409eff;
  2010. }
  2011. `;
  2012. let beautifyTextAreaCSS = `
  2013. textarea {
  2014. position: relative;
  2015. display: inline-block;
  2016. width: 100%;
  2017. vertical-align: bottom;
  2018. font-size: 14px;
  2019. position: relative;
  2020. display: block;
  2021. resize: vertical;
  2022. padding: 5px 11px;
  2023. line-height: 1.5;
  2024. box-sizing: border-box;
  2025. width: 100%;
  2026. font-size: inherit;
  2027. font-family: inherit;
  2028. /* color: #606266; */
  2029. background-color: #ffffff;
  2030. background-image: none;
  2031. -webkit-appearance: none;
  2032. box-shadow: 0 0 0 1px #dcdfe6 inset;
  2033. border-radius: 4px;
  2034. transition: box-shadow .2s cubic-bezier(.645, .045, .355, 1);
  2035. border: none;
  2036. }
  2037. textarea:focus{
  2038. outline: none;
  2039. box-shadow: 0 0 0 1px #409eff inset;
  2040. }
  2041. `;
  2042. /**
  2043. * 未派上用场的CSS
  2044. */
  2045. let notUseBeautifyCSS = `
  2046. .token.blockquote,.token.comment{color:#969896}
  2047. .token.cdata{color:#183691}
  2048. .token.doctype,.token.macro.property,.token.punctuation,.token.variable{color:#333}
  2049. .token.builtin,.token.important,.token.keyword,.token.operator,.token.rule{color:#a71d5d}
  2050. .token.attr-value,.token.regex,.token.string,.token.url{color:#183691}
  2051. .token.atrule,.token.boolean,.token.code,.token.command,.token.constant,.token.entity,.token.number,.token.property,.token.symbol{color:#0086b3}
  2052. .token.prolog,.token.selector,.token.tag{color:#63a35c}
  2053. .token.attr-name,.token.class,.token.class-name,.token.function,.token.id,.token.namespace,.token.pseudo-class,.token.pseudo-element,.token.url-reference .token.variable{color:#795da3}
  2054. .token.entity{cursor:help}
  2055. .token.title,.token.title .token.punctuation{font-weight:700;color:#1d3e81}
  2056. .token.list{color:#ed6a43}
  2057. .token.inserted{background-color:#eaffea;color:#55a532}
  2058. .token.deleted{background-color:#ffecec;color:#bd2c00}
  2059. .token.bold{font-weight:700}
  2060. .token.italic{font-style:italic}
  2061. .language-json .token.property{color:#183691}
  2062. .language-markup .token.tag .token.punctuation{color:#333}
  2063. .language-css .token.function,code.language-css{color:#0086b3}
  2064. .language-yaml .token.atrule{color:#63a35c}
  2065. code.language-yaml{color:#183691}
  2066. .language-ruby .token.function{color:#333}
  2067. .language-markdown .token.url{color:#795da3}
  2068. .language-makefile .token.symbol{color:#795da3}
  2069. .language-makefile .token.variable{color:#183691}
  2070. .language-makefile .token.builtin{color:#0086b3}
  2071. .language-bash .token.keyword{color:#0086b3}
  2072. pre[data-line]{position:relative;padding:1em 0 1em 3em}
  2073. pre[data-line] .line-highlight-wrapper{position:absolute;top:0;left:0;background-color:transparent;display:block;width:100%}
  2074. pre[data-line] .line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}
  2075. pre[data-line] .line-highlight:before,pre[data-line] .line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}
  2076. pre[data-line] .line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}
  2077. `;
  2078. GM_addStyle(beautifyMarkdownCSS);
  2079. GM_addStyle(beautifyButtonCSS);
  2080. GM_addStyle(beautifyRadioCSS);
  2081. GM_addStyle(beautifyTextAreaCSS);
  2082. GM_addStyle(`
  2083. p:has(input[type="submit"][name="update-and-sync"]){
  2084. margin-top: 10px;
  2085. }
  2086. `);
  2087. DOMUtils.ready(function () {
  2088. let markupChoiceELement = document.querySelector(
  2089. 'a[target="markup_choice"][href*="daringfireball.net"]'
  2090. );
  2091. if (markupChoiceELement) {
  2092. markupChoiceELement.parentElement.replaceChild(
  2093. DOMUtils.createElement("span", {
  2094. textContent: "Markdown",
  2095. }),
  2096. markupChoiceELement
  2097. );
  2098. }
  2099.  
  2100. if (
  2101. globalThis.location.pathname.endsWith("/admin") &&
  2102. !document.querySelector('input[type="submit"][name="update-only"]')
  2103. ) {
  2104. GM_addStyle(`
  2105. .indented{
  2106. padding-left: unset;
  2107. }
  2108. `);
  2109. }
  2110. });
  2111. },
  2112. /**
  2113. * 美化 历史版本 页面
  2114. */
  2115. beautifyHistoryVersionPage() {
  2116. if (!globalThis.location.pathname.endsWith("/versions")) {
  2117. return;
  2118. }
  2119. let displayCSS = `
  2120. .version-number,
  2121. .version-date,
  2122. .version-changelog{
  2123. display: none;
  2124. }
  2125. `;
  2126. /* 美化version页面 */
  2127. let beautifyVersionsPageCSS = `
  2128. ul.history_versions,
  2129. ul.history_versions li{
  2130. width: 100%;
  2131. }
  2132. ul.history_versions li{
  2133. display: flex;
  2134. flex-direction: column;
  2135. margin: 25px 0px;
  2136. }
  2137. .diff-controls input[type="radio"]:nth-child(2){
  2138. margin-left: 5px;
  2139. }
  2140. .flex-align-item-center{
  2141. display: flex;
  2142. align-items: center;
  2143. }
  2144. .script-tag{
  2145. margin-bottom: 8px;
  2146. }
  2147. .script-tag-version a{
  2148. color: #656d76;
  2149. fill: #656d76;
  2150. text-decoration: none;
  2151. width: fit-content;
  2152. width: -moz-fit-content;
  2153. }
  2154. .script-tag-version a:hover svg{
  2155. color: #00a3f5;
  2156. fill: #00a3f5;
  2157. }
  2158. .script-tag-version a > span{
  2159. margin-left: 0.25rem;
  2160. }
  2161. .script-note-box-body{
  2162. border-radius: 0.375rem;
  2163. border-style: solid;
  2164. border-width: max(1px, 0.0625rem);
  2165. border-color: #d0d7de;
  2166. color: #1f2328;
  2167. padding: 16px;
  2168. overflow-wrap: anywhere;
  2169. }
  2170. .script-note-box-body p{
  2171. margin-bottom: unset;
  2172. }
  2173. `;
  2174. GM_addStyle(beautifyVersionsPageCSS);
  2175. GM_addStyle(displayCSS);
  2176. DOMUtils.ready(function () {
  2177. let historyVersionsULElement = document.querySelector(
  2178. "ul.history_versions"
  2179. );
  2180. if (!historyVersionsULElement) {
  2181. Qmsg.error("未找到history_versions元素列表");
  2182. return;
  2183. }
  2184. /* 遍历每一个版本块 */
  2185. Array.from(historyVersionsULElement.children).forEach((liElement) => {
  2186. /* 版本链接 */
  2187. let versionUrl = liElement.querySelector(".version-number a").href;
  2188. /* 版本号 */
  2189. let versionNumber =
  2190. liElement.querySelector(".version-number a").innerText;
  2191. /* 更新日期 */
  2192. let versionDate = liElement
  2193. .querySelector(".version-date")
  2194. .getAttribute("datetime");
  2195. /* 更新日志 */
  2196. let updateNote =
  2197. liElement.querySelector(".version-changelog")?.innerHTML || "";
  2198.  
  2199. let versionDateElement = DOMUtils.createElement("span", {
  2200. className: "script-version-date",
  2201. innerHTML: utils.formatTime(versionDate, "yyyy年MM月dd日 hh:mm:ss"),
  2202. });
  2203. let tagElement = DOMUtils.createElement("div", {
  2204. className: "script-tag",
  2205. innerHTML: `
  2206. <div class="script-tag-version">
  2207. <a href="${versionUrl}}" class="flex-align-item-center">
  2208. <svg aria-label="Tag" role="img" height="16" viewBox="0 0 16 16" version="1.1" width="16">
  2209. <path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"></path>
  2210. </svg>
  2211. <span>${versionNumber}</span>
  2212. </a>
  2213. </div>
  2214. `,
  2215. });
  2216. let boxBodyElement = DOMUtils.createElement("div", {
  2217. className: "script-note-box-body",
  2218. innerHTML: updateNote,
  2219. });
  2220. liElement.appendChild(versionDateElement);
  2221. liElement.appendChild(tagElement);
  2222. liElement.appendChild(boxBodyElement);
  2223. });
  2224. });
  2225. },
  2226. /**
  2227. * 美化 Greasyfork Beautify脚本
  2228. */
  2229. beautifyGreasyforkBeautify() {
  2230. let compatibleBeautifyCSS = `
  2231. #main-header{
  2232. background-color: #670000 !important;
  2233. background-image: linear-gradient(#670000,#990000) !important;
  2234. }
  2235. #site-nav-vue{
  2236. flex-wrap: wrap;
  2237. justify-content: flex-end;
  2238. }
  2239. .open-sidebar{
  2240. border-width: 1px;
  2241. border-radius: 3px;
  2242. margin-right: 0;
  2243. }
  2244. input.search-submit{
  2245. transform: translateY(-5%) !important;
  2246. margin-left: 10px;
  2247. }
  2248. #script-content code{
  2249. word-wrap: break-word;
  2250. }
  2251. .code-container ::selection {
  2252. background-color: #3D4556 !important;
  2253. }
  2254. `;
  2255.  
  2256. GM_addStyle(compatibleBeautifyCSS);
  2257. if (utils.isPhone()) {
  2258. GM_addStyle(`
  2259. section#script-info,
  2260. section.text-content,
  2261. div.width-constraint table.text-content.log-table{
  2262. margin-top: 80px;
  2263. }
  2264. div.width-constraint div.sidebarred{
  2265. padding-top: 80px;
  2266. }
  2267. div.width-constraint div.sidebarred .sidebar{
  2268. top: 80px;
  2269. }`);
  2270. } else {
  2271. GM_addStyle(`
  2272. section#script-info{
  2273. margin-top: 10px;
  2274. }`);
  2275. }
  2276. },
  2277. /**
  2278. * 美化上传图片
  2279. */
  2280. beautifyUploadImage() {
  2281. let beautifyCSS = `
  2282. /* 隐藏 添加: */
  2283. label[for="discussion_comments_attributes_0_attachments"],
  2284. label[for="comment_attachments"]{
  2285. display: none;
  2286. }
  2287. input[type="file"]{
  2288. width: 100%;
  2289. font-size: 20px;
  2290. background: #e2e2e2;
  2291. padding: 40px 0px;
  2292. border-radius: 10px;
  2293. text-align-last: center;
  2294. }
  2295. `;
  2296. GM_addStyle(beautifyCSS);
  2297. DOMUtils.ready(function () {
  2298. /**
  2299. * 清空错误的提示
  2300. * @param {HTMLElement} element
  2301. */
  2302. function clearErrorTip(element) {
  2303. while (element.nextElementSibling) {
  2304. element.parentElement.removeChild(element.nextElementSibling);
  2305. }
  2306. }
  2307. let fileElementList = document.querySelectorAll('input[type="file"]');
  2308. fileElementList.forEach((fileElement) => {
  2309. if (fileElement.getAttribute("name") === "code_upload") {
  2310. return;
  2311. }
  2312. if (
  2313. fileElement.hasAttribute("accept") &&
  2314. fileElement.getAttribute("accept").includes("javascript")
  2315. ) {
  2316. return;
  2317. }
  2318. DOMUtils.on(fileElement, "change", function (event) {
  2319. clearErrorTip(event.target);
  2320. /**
  2321. * @type {File[]}
  2322. */
  2323. let chooseImageFiles = event.currentTarget.files;
  2324. if (chooseImageFiles.length === 0) {
  2325. return;
  2326. }
  2327. log.info(["选择的图片", chooseImageFiles]);
  2328. if (chooseImageFiles.length > 5) {
  2329. DOMUtils.after(
  2330. fileElement,
  2331. DOMUtils.createElement("p", {
  2332. textContent: `❌ 最多同时长传5张图片`,
  2333. })
  2334. );
  2335. }
  2336. /**
  2337. * @type {File[]}
  2338. */
  2339. let notAllowImage = [];
  2340. Array.from(chooseImageFiles).forEach((imageFile) => {
  2341. if (
  2342. imageFile.size > 204800 ||
  2343. !imageFile.type.match(/png|gif|jpeg|webp/i)
  2344. ) {
  2345. notAllowImage.push(imageFile);
  2346. }
  2347. });
  2348. if (notAllowImage.length === 0) {
  2349. return;
  2350. }
  2351. notAllowImage.forEach((imageFile) => {
  2352. DOMUtils.after(
  2353. fileElement,
  2354. DOMUtils.createElement("p", {
  2355. textContent: `❌ 图片:${
  2356. imageFile.name
  2357. } 大小:${utils.formatByteToSize(imageFile.size)}`,
  2358. })
  2359. );
  2360. });
  2361. });
  2362. });
  2363. });
  2364. },
  2365. /**
  2366. * 添加复制代码按钮
  2367. */
  2368. addCopyCodeButton() {
  2369. if (!window.location.pathname.endsWith("/code")) {
  2370. return;
  2371. }
  2372. utils
  2373. .waitNode("div#script-content div.code-container")
  2374. .then((element) => {
  2375. let copyButton = DOMUtils.createElement(
  2376. "button",
  2377. {
  2378. textContent: "复制代码",
  2379. },
  2380. {
  2381. style: "margin-bottom: 1em;",
  2382. }
  2383. );
  2384. DOMUtils.on(copyButton, "click", async function () {
  2385. let loading = Qmsg.loading("加载文件中...");
  2386. let getResp = await httpx.get(
  2387. `https://greasyfork.org/zh-CN/scripts/${GreasyforkApi.getScriptId()}.json`,
  2388. {
  2389. fetch: true,
  2390. responseType: "json",
  2391. }
  2392. );
  2393. if (!getResp.status) {
  2394. loading.close();
  2395. return;
  2396. }
  2397. let respJSON = utils.toJSON(getResp.data.responseText);
  2398. let code_url = respJSON["code_url"];
  2399. log.success(["代码地址:", code_url]);
  2400. let scriptJS = await httpx.get(code_url);
  2401. if (!scriptJS.status) {
  2402. loading.close();
  2403. return;
  2404. }
  2405. loading.close();
  2406. utils.setClip(scriptJS.data.responseText);
  2407. Qmsg.success("复制成功");
  2408. });
  2409. DOMUtils.before(element, copyButton);
  2410. });
  2411. },
  2412. /**
  2413. * F11全屏,代码全屏
  2414. */
  2415. fullScreenOptimization() {
  2416. if (!window.location.pathname.endsWith("/code")) {
  2417. return;
  2418. }
  2419. GM_addStyle(`
  2420. .code-wide-screen{
  2421. position: absolute;
  2422. top: 0;
  2423. left: 0;
  2424. right: 0;
  2425. bottom: 0;
  2426. margin: 0;
  2427. padding: 0;
  2428. width: 100%;
  2429. height: 100%;
  2430. min-width: 100%;
  2431. min-height: 100%;
  2432. max-width: 100%;
  2433. max-height: 100%;
  2434. z-index: 10000;
  2435. }
  2436. `);
  2437. let isFullScreen = false;
  2438. DOMUtils.keydown(
  2439. window,
  2440. function (...args) {
  2441. /**
  2442. * @type {KeyboardEvent}
  2443. */
  2444. let event = args[0];
  2445. if (event.key.toLowerCase() === "f") {
  2446. let codeElement = document.querySelector(
  2447. "#script-content div.code-container code"
  2448. );
  2449. if (event.altKey && event.shiftKey) {
  2450. /* 宽屏 */
  2451. utils.preventEvent(event);
  2452. if (codeElement.classList.contains("code-wide-screen")) {
  2453. /* 当前处于宽屏状态,退出宽屏 */
  2454. codeElement.classList.remove("code-wide-screen");
  2455. } else {
  2456. /* 进入宽屏 */
  2457. codeElement.classList.add("code-wide-screen");
  2458. }
  2459. } else if (
  2460. !event.altKey &&
  2461. !event.ctrlKey &&
  2462. !event.shiftKey &&
  2463. !event.metaKey
  2464. ) {
  2465. /* 全屏 */
  2466. utils.preventEvent(event);
  2467. if (isFullScreen) {
  2468. /* 退出全屏 */
  2469. utils.exitFullScreen(codeElement);
  2470. isFullScreen = false;
  2471. } else {
  2472. /* 进入全屏 */
  2473. utils.enterFullScreen(codeElement);
  2474. isFullScreen = true;
  2475. }
  2476. }
  2477. }
  2478. },
  2479. {
  2480. capture: true,
  2481. }
  2482. );
  2483. },
  2484. /**
  2485. * 在Markdown右上角添加复制按钮
  2486. */
  2487. addMarkdownCopyButton() {
  2488. /* 不在/code页面添加Markdown复制按钮 */
  2489. if (window.location.href.endsWith("/code")) {
  2490. return;
  2491. }
  2492. GM_addStyle(`
  2493. pre{
  2494. position: relative;
  2495. margin-bottom: 0px !important;
  2496. width: 100%;
  2497. }
  2498. `);
  2499. GM_addStyle(`
  2500. .snippet-clipboard-content{
  2501. display: flex;
  2502. justify-content: space-between;
  2503. background: rgb(246, 248, 250);
  2504. margin-bottom: 16px;
  2505. }
  2506. .zeroclipboard-container {
  2507. /* right: 0;
  2508. top: 0;
  2509. position: absolute; */
  2510. box-sizing: border-box;
  2511. display: flex;
  2512. font-size: 16px;
  2513. line-height: 24px;
  2514. text-size-adjust: 100%;
  2515. overflow-wrap: break-word;
  2516. width: fit-content;
  2517. height: fit-content;
  2518. }
  2519. .zeroclipboard-container svg{
  2520. vertical-align: text-bottom;
  2521. display: inline-block;
  2522. overflow: visible;
  2523. fill: currentColor;
  2524. margin: 8px;
  2525. }
  2526. .zeroclipboard-container svg[aria-hidden="true"]{
  2527. display: none;
  2528. }
  2529. clipboard-copy.js-clipboard-copy {
  2530. position: relative;
  2531. padding: 0px;
  2532. color: rgb(36, 41, 47);
  2533. background-color: rgb(246, 248, 250);
  2534. transition: 80ms cubic-bezier(0.33, 1, 0.68, 1);
  2535. transition-property: color,background-color,box-shadow,border-color;
  2536. display: inline-block;
  2537. font-size: 14px;
  2538. line-height: 20px;
  2539. white-space: nowrap;
  2540. vertical-align: middle;
  2541. cursor: pointer;
  2542. -webkit-user-select: none;
  2543. user-select: none;
  2544. border: 1px solid rgba(31, 35, 40, 0.15);
  2545. -webkit-appearance: none;
  2546. appearance: none;
  2547. box-shadow: rgba(31, 35, 40, 0.04) 0px 1px 0px 0px, rgba(255, 255, 255, 0.25) 0px 1px 0px 0px inset;
  2548. margin: 8px;
  2549. overflow-wrap: break-word;
  2550. text-wrap: nowrap;
  2551. border-radius: 6px;
  2552. }
  2553. clipboard-copy.js-clipboard-copy[success]{
  2554. border-color: rgb(31, 136, 61);
  2555. box-shadow: 0 0 0 0.2em rgba(52,208,88,.4);
  2556. }
  2557. clipboard-copy.js-clipboard-copy:hover{
  2558. background-color: rgb(243, 244, 246);
  2559. border-color: rgba(31, 35, 40, 0.15);
  2560. transition-duration: .1s;
  2561. }
  2562. clipboard-copy.js-clipboard-copy:active{
  2563. background-color: rgb(235, 236, 240);
  2564. border-color: rgba(31, 35, 40, 0.15);
  2565. transition: none;
  2566. }
  2567. `);
  2568. GM_addStyle(`
  2569. .pops-tip.github-tooltip {
  2570. border-radius: 6px;
  2571. padding: 6px 8px;
  2572. }
  2573. .pops-tip.github-tooltip, .pops-tip.github-tooltip .pops-tip-arrow::after {
  2574. background: rgb(36, 41, 47);
  2575. color: #fff;
  2576. }
  2577. .pops-tip.github-tooltip .pops-tip-arrow::after {
  2578. width: 8px;
  2579. height: 8px;
  2580. }
  2581. `);
  2582. /**
  2583. * 获取复制按钮元素
  2584. * @returns {HTMLElement}
  2585. */
  2586. function getCopyElement() {
  2587. let copyElement = DOMUtils.createElement("div", {
  2588. className: "zeroclipboard-container",
  2589. innerHTML: `
  2590. <clipboard-copy class="js-clipboard-copy">
  2591. <svg height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon-copy">
  2592. <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
  2593. </svg>
  2594. <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon-check-copy">
  2595. <path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path>
  2596. </svg>
  2597. </clipboard-copy>
  2598. `,
  2599. });
  2600. let clipboardCopyElement =
  2601. copyElement.querySelector(".js-clipboard-copy");
  2602. let octiconCopyElement = copyElement.querySelector(".octicon-copy");
  2603. let octiconCheckCopyElement = copyElement.querySelector(
  2604. ".octicon-check-copy"
  2605. );
  2606. DOMUtils.on(copyElement, "click", function () {
  2607. let codeElement = copyElement.parentElement.querySelector("code");
  2608. if (
  2609. !codeElement &&
  2610. copyElement.parentElement.className.includes("prettyprinted")
  2611. ) {
  2612. /* 在gf的/code的复制 */
  2613. codeElement = copyElement.parentElement;
  2614. }
  2615. if (!codeElement) {
  2616. Qmsg.error("未找到code元素");
  2617. return;
  2618. }
  2619. utils.setClip(codeElement.innerText || codeElement.textContent);
  2620. clipboardCopyElement.setAttribute("success", "true");
  2621. octiconCopyElement.setAttribute("aria-hidden", true);
  2622. octiconCheckCopyElement.removeAttribute("aria-hidden");
  2623. let tooltip = pops.tooltip({
  2624. target: clipboardCopyElement,
  2625. content: "✅ 复制成功!",
  2626. position: "left",
  2627. className: "github-tooltip",
  2628. alwaysShow: true,
  2629. });
  2630. setTimeout(() => {
  2631. clipboardCopyElement.removeAttribute("success");
  2632. octiconCheckCopyElement.setAttribute("aria-hidden", true);
  2633. octiconCopyElement.removeAttribute("aria-hidden");
  2634. tooltip.close();
  2635. }, 2000);
  2636. });
  2637. return copyElement;
  2638. }
  2639.  
  2640. document.querySelectorAll("pre").forEach((preElement) => {
  2641. let zeroclipboardElement = preElement.querySelector(
  2642. "div.zeroclipboard-container"
  2643. );
  2644. if (zeroclipboardElement) {
  2645. return;
  2646. }
  2647. let copyElement = getCopyElement(preElement);
  2648. let snippetClipboardContentElement = DOMUtils.createElement("div", {
  2649. className: "snippet-clipboard-content",
  2650. });
  2651. DOMUtils.before(preElement, snippetClipboardContentElement);
  2652. snippetClipboardContentElement.appendChild(preElement);
  2653. snippetClipboardContentElement.appendChild(copyElement);
  2654. });
  2655. },
  2656. /**
  2657. * 固定当前语言
  2658. */
  2659. languageSelectorLocale() {
  2660. let localeLanguage = PopsPanel.getValue("language-selector-locale");
  2661. let currentLocaleLanguage = window.location.pathname
  2662. .split("/")
  2663. .filter((item) => Boolean(item))[0];
  2664. log.success("选择语言:" + localeLanguage);
  2665. log.success("当前语言:" + currentLocaleLanguage);
  2666. if (utils.isNull(localeLanguage)) {
  2667. return;
  2668. }
  2669. if (localeLanguage === currentLocaleLanguage) {
  2670. return;
  2671. } else {
  2672. let timer = null;
  2673. let url = GreasyforkApi.getSwitchLanguageUrl(localeLanguage);
  2674. GreasyforkApi.switchLanguage(url);
  2675. log.success("新Url:" + url);
  2676. Qmsg.loading(
  2677. `当前语言:${currentLocaleLanguage},3秒后切换至:${localeLanguage}`,
  2678. {
  2679. timeout: 3000,
  2680. showClose: true,
  2681. onClose() {
  2682. clearTimeout(timer);
  2683. },
  2684. }
  2685. );
  2686. Qmsg.info("导航至:" + url, {
  2687. timeout: 3000,
  2688. });
  2689. timer = setTimeout(() => {
  2690. window.location.href = url;
  2691. }, 3000);
  2692. }
  2693. },
  2694. /**
  2695. * 面板-脚本列表|库
  2696. * @param {"script-list"|"script-library"} type
  2697. * @param {Event} event
  2698. * @param {HTMLLIElement} rightHeaderElement
  2699. * @param {HTMLLIElement} rightContainerElement
  2700. * @returns
  2701. */
  2702. async UIScriptList(type, event, rightHeaderElement, rightContainerElement) {
  2703. if (!GreasyforkMenu.isLogin) {
  2704. Qmsg.error("请先登录账号!");
  2705. return;
  2706. }
  2707. let userLinkElement = GreasyforkMenu.getUserLinkElement();
  2708. let userId = userLinkElement.href
  2709. .split("/")
  2710. .pop()
  2711. .match(/([0-9]+)/)[0];
  2712. let loading = pops.loading({
  2713. mask: {
  2714. enable: true,
  2715. },
  2716. parent: rightContainerElement,
  2717. content: {
  2718. text: "获取信息中,请稍后...",
  2719. },
  2720. addIndexCSS: false,
  2721. });
  2722. let userInfo = await GreasyforkApi.getUserInfo(userId);
  2723. loading.close();
  2724. if (!userInfo) {
  2725. return;
  2726. }
  2727. log.info(userInfo);
  2728. let scriptList =
  2729. type === "script-list"
  2730. ? userInfo["scriptList"]
  2731. : userInfo["scriptLibraryList"];
  2732. Qmsg.success(`获取成功,共 ${scriptList.length} 个`);
  2733. for (const scriptInfo of scriptList) {
  2734. let liElement = DOMUtils.createElement("li", {
  2735. className: "w-script-list-item",
  2736. innerHTML: `
  2737. <div class="w-script-info">
  2738. <div class="w-script-name">
  2739. <a href="${scriptInfo["url"]}" target="_blank">${
  2740. scriptInfo["name"]
  2741. }</a>
  2742. </div>
  2743. <div class="w-script-fan-score">
  2744. <p>评分:${scriptInfo["fan_score"]}</p>
  2745. </div>
  2746. <div class="w-script-locale">
  2747. <p>语言:${scriptInfo["locale"]}</p>
  2748. </div>
  2749. <div class="w-script-version">
  2750. <p>版本:${scriptInfo["version"]}</p>
  2751. </div>
  2752. <div class="w-script-update-time">
  2753. <p>更新:${utils.getDaysDifference(
  2754. new Date(scriptInfo["code_updated_at"]).getTime(),
  2755. undefined,
  2756. "auto"
  2757. )}前</p>
  2758. </div>
  2759. </div>
  2760. `,
  2761. });
  2762. let scriptInfoElement = liElement.querySelector(".w-script-info");
  2763. let buttonElement = DOMUtils.createElement("div", {
  2764. className: "pops-panel-button",
  2765. innerHTML: `
  2766. <button type="primary" data-icon="" data-righticon="false">
  2767. <span>同步代码</span>
  2768. </button>
  2769. `,
  2770. });
  2771. if (scriptInfo["deleted"]) {
  2772. /* 该脚本已给删除 */
  2773. liElement.classList.add("w-script-deleted");
  2774. buttonElement.querySelector("button").setAttribute("disabled", true);
  2775. }
  2776.  
  2777. DOMUtils.on(buttonElement, "click", undefined, async function () {
  2778. log.success(["同步", scriptInfo]);
  2779. let btn = buttonElement.querySelector("button");
  2780. let span = buttonElement.querySelector("button span");
  2781. let iconElement = DOMUtils.createElement(
  2782. "i",
  2783. {
  2784. className: "pops-bottom-icon",
  2785. innerHTML: pops.config.iconSVG.loading,
  2786. },
  2787. {
  2788. "is-loading": true,
  2789. }
  2790. );
  2791. btn.setAttribute("disabled", true);
  2792. btn.setAttribute("data-icon", true);
  2793. span.innerText = "同步中...";
  2794. DOMUtils.before(span, iconElement);
  2795. let codeSyncFormData = await GreasyforkApi.getSourceCodeSyncFormData(
  2796. scriptInfo["id"]
  2797. );
  2798. if (codeSyncFormData) {
  2799. const SCRIPT_SYNC_TYPE_ID_FORMDATA_KEY =
  2800. "script[script_sync_type_id]";
  2801. if (codeSyncFormData.has(SCRIPT_SYNC_TYPE_ID_FORMDATA_KEY)) {
  2802. /* 1是手动同步、2是自动同步、3是webhook同步 */
  2803. let syncTypeId = codeSyncFormData.get(
  2804. SCRIPT_SYNC_TYPE_ID_FORMDATA_KEY
  2805. );
  2806. let syncMode = "";
  2807. if (syncTypeId.toString() === "1") {
  2808. syncMode = "手动";
  2809. } else if (syncTypeId.toString() === "2") {
  2810. syncMode = "自动";
  2811. } else if (syncTypeId.toString() === "3") {
  2812. syncMode = "webhook";
  2813. }
  2814. let oldSyncTypeElement = liElement.querySelector(
  2815. ".w-script-sync-type"
  2816. );
  2817. if (oldSyncTypeElement) {
  2818. oldSyncTypeElement.querySelector(
  2819. "p"
  2820. ).innerText = `同步方式:${syncMode}`;
  2821. } else {
  2822. DOMUtils.append(
  2823. scriptInfoElement,
  2824. `
  2825. <div class="w-script-sync-type">
  2826. <p>同步方式:${syncMode}</p>
  2827. </div>
  2828. `
  2829. );
  2830. }
  2831. let syncUpdateResponse = await GreasyforkApi.sourceCodeSync(
  2832. scriptInfo["id"],
  2833. codeSyncFormData
  2834. );
  2835. if (syncUpdateResponse) {
  2836. Qmsg.success("同步成功");
  2837. } else {
  2838. Qmsg.error("同步失败");
  2839. }
  2840. } else {
  2841. Qmsg.error("该脚本未设置同步信息");
  2842. }
  2843. }
  2844.  
  2845. btn.removeAttribute("disabled");
  2846. btn.removeAttribute("data-icon");
  2847. span.innerText = "同步代码";
  2848. iconElement.remove();
  2849. });
  2850. liElement.appendChild(buttonElement);
  2851. rightContainerElement.appendChild(liElement);
  2852. }
  2853. },
  2854. /**
  2855. * 论坛-过滤
  2856. */
  2857. filterDiscussions() {
  2858. const FILTER_SCRIPT_KEY = "greasyfork-discussions-filter-script";
  2859. const FILTER_POST_USER_KEY = "greasyfork-discussions-filter-post-user";
  2860. const FILTER_REPLY_USER_KEY = "greasyfork-discussions-filter-reply-user";
  2861. if (!globalThis.location.pathname.includes("/discussions")) {
  2862. return;
  2863. }
  2864. const filterScript = PopsPanel.getValue(FILTER_SCRIPT_KEY, "");
  2865. const filterPostUser = PopsPanel.getValue(FILTER_POST_USER_KEY, "");
  2866. const filterReplyUser = PopsPanel.getValue(FILTER_REPLY_USER_KEY, "");
  2867.  
  2868. const filterScriptList =
  2869. filterScript.trim() === "" ? [] : filterScript.split("\n");
  2870. const filterPostUserList =
  2871. filterPostUser.trim() === "" ? [] : filterPostUser.split("\n");
  2872. const filterReplyUserList =
  2873. filterReplyUser.trim() === "" ? [] : filterReplyUser.split("\n");
  2874.  
  2875. /**
  2876. * @type {Map<string,HTMLElement>}
  2877. */
  2878. const SNIPPET_MAP = new Map();
  2879.  
  2880. GM_addStyle(`
  2881. .discussion-list-container {
  2882. --discusstion-repeat-color: #ffa700;
  2883. }
  2884. .discussion-list-container a.discussion-title[data-repeat-tip-show]::before {
  2885. content: attr(data-repeat-tip-show);
  2886. color: var(--discusstion-repeat-color);
  2887. border-radius: 5px;
  2888. border: 2px solid var(--discusstion-repeat-color);
  2889. padding: 2px 5px;
  2890. font-weight: 800;
  2891. font-size: 14px;
  2892. }
  2893. `);
  2894. let discussionListContainer = document.querySelectorAll(
  2895. ".discussion-list-container"
  2896. );
  2897.  
  2898. Array.from(discussionListContainer).forEach((listContainer, index) => {
  2899. if (!listContainer.querySelector("a.script-link")) {
  2900. return;
  2901. }
  2902. const discussionInfo = {
  2903. /** 脚本名 @type {string} */
  2904. scriptName: listContainer.querySelector("a.script-link").innerText,
  2905. /** 脚本主页地址 @type {string} */
  2906. scriptUrl: listContainer.querySelector("a.script-link").href,
  2907. /** 脚本id @type {string} */
  2908. scriptId: GreasyforkApi.getScriptId(
  2909. listContainer.querySelector("a.script-link").href
  2910. ),
  2911. /** 发布的用户名 @type {string} */
  2912. postUserName: listContainer.querySelector("a.user-link").innerText,
  2913. /** 发布的用户主页地址 @type {string} */
  2914. postUserHomeUrl: listContainer.querySelector("a.user-link").href,
  2915. /** 发布的用户id @type {string} */
  2916. postUserId: GreasyforkApi.getUserId(
  2917. listContainer.querySelector("a.user-link").href
  2918. ),
  2919. /** 发布的时间 */
  2920. postTimeStamp: new Date(
  2921. listContainer
  2922. .querySelector("relative-time")
  2923. .getAttribute("datetime")
  2924. ),
  2925. /** 发布的地址 @type {string} */
  2926. snippetUrl: listContainer.querySelector("a.discussion-title").href,
  2927. /** 发布的内容片段 @type {string} */
  2928. snippet: listContainer.querySelector("span.discussion-snippet")
  2929. .innerText,
  2930. /** 回复的用户名 @type {?string} */
  2931. replyUserName: undefined,
  2932. /** 回复的用户主页地址 @type {?string} */
  2933. replyUserHomeUrl: undefined,
  2934. /** 回复的用户id @type {?string} */
  2935. replyUserId: undefined,
  2936. /** 回复的时间 */
  2937. replyTimeStamp: undefined,
  2938. };
  2939.  
  2940. if (
  2941. listContainer.querySelector(
  2942. ".discussion-meta-item .discussion-meta-item"
  2943. )
  2944. ) {
  2945. discussionInfo.replyUserName = listContainer.querySelector(
  2946. ".discussion-meta-item .discussion-meta-item a.user-link"
  2947. ).innerText;
  2948. discussionInfo.replyUserHomeUrl = listContainer.querySelector(
  2949. ".discussion-meta-item .discussion-meta-item a.user-link"
  2950. ).href;
  2951. discussionInfo.replyUserId = GreasyforkApi.getUserId(
  2952. discussionInfo.replyUserHomeUrl
  2953. );
  2954. discussionInfo.replyTimeStamp = new Date(
  2955. listContainer
  2956. .querySelector(
  2957. ".discussion-meta-item .discussion-meta-item relative-time"
  2958. )
  2959. .getAttribute("datetime")
  2960. );
  2961. }
  2962.  
  2963. if (
  2964. SNIPPET_MAP.has(discussionInfo.snippet) &&
  2965. PopsPanel.getValue("greasyfork-discussions-filter-duplicate-comments")
  2966. ) {
  2967. let discussionTitleElement = SNIPPET_MAP.get(
  2968. discussionInfo.snippet
  2969. ).querySelector("a.discussion-title");
  2970. discussionTitleElement.setAttribute("data-repeat-tip-show", true);
  2971. let oldCount = 0;
  2972.  
  2973. if (discussionTitleElement.hasAttribute("data-repeat-count")) {
  2974. oldCount = parseInt(
  2975. discussionTitleElement.getAttribute("data-repeat-count")
  2976. );
  2977. }
  2978. oldCount++;
  2979. discussionTitleElement.setAttribute("data-repeat-count", oldCount);
  2980. discussionTitleElement.setAttribute(
  2981. "data-repeat-tip-show",
  2982. `已过滤:${oldCount}`
  2983. );
  2984. log.success([
  2985. "过滤重复内容:" + discussionInfo.snippet,
  2986. discussionInfo,
  2987. ]);
  2988. listContainer.remove();
  2989. return;
  2990. }
  2991.  
  2992. SNIPPET_MAP.set(discussionInfo.snippet, listContainer);
  2993. for (const filterScriptId of filterScriptList) {
  2994. if (discussionInfo.scriptId === filterScriptId) {
  2995. log.success([
  2996. "过滤脚本id:" + discussionInfo.scriptId,
  2997. discussionInfo,
  2998. ]);
  2999. listContainer.remove();
  3000. return;
  3001. }
  3002. }
  3003.  
  3004. for (const filterPostUserId of filterPostUserList) {
  3005. if (discussionInfo.postUserId === filterPostUserId) {
  3006. log.success([
  3007. "过滤发布用户id:" + discussionInfo.postUserId,
  3008. discussionInfo,
  3009. ]);
  3010. listContainer.remove();
  3011. return;
  3012. }
  3013. }
  3014.  
  3015. if (discussionInfo.replyUserName) {
  3016. for (const filterReplyUserId of filterReplyUserList) {
  3017. if (discussionInfo.replyUserId === filterReplyUserId) {
  3018. log.success([
  3019. "过滤回复用户id:" + discussionInfo.replyUserId,
  3020. discussionInfo,
  3021. ]);
  3022. listContainer.remove();
  3023. return;
  3024. }
  3025. }
  3026. }
  3027. });
  3028. },
  3029. /**
  3030. * 检测gf页面是否正确加载,有时候会出现
  3031. * We're down for maintenance. Check back again soon.
  3032. */
  3033. checkPage() {
  3034. DOMUtils.ready(() => {
  3035. if (
  3036. document.body.firstElementChild &&
  3037. document.body.firstElementChild.localName === "p" &&
  3038. document.body.firstElementChild.innerText.includes(
  3039. "We're down for maintenance. Check back again soon."
  3040. )
  3041. ) {
  3042. let checkPageTime = parseInt(
  3043. GM_getValue("greasyfork-check-page-time", 0)
  3044. );
  3045. if (checkPageTime && Date.now() - checkPageTime > 5 * 1000) {
  3046. /* 上次重载时间在5秒内的话就拒绝重载 */
  3047. Qmsg.error("5秒内拒绝反复重载");
  3048. return;
  3049. }
  3050. GM_setValue("greasyfork-check-page-time", Date.now());
  3051. window.location.reload();
  3052. }
  3053. });
  3054. },
  3055. };
  3056. /* -----------------↑函数区域↑----------------- */
  3057.  
  3058. /* -----------------↓执行入口↓----------------- */
  3059. Greasyfork.checkPage();
  3060. GreasyforkCSS.init();
  3061. PopsPanel.initMenu();
  3062. if (PopsPanel.getValue("beautifyPage")) {
  3063. Greasyfork.beautifyPageElement();
  3064. }
  3065. if (PopsPanel.getValue("beautifyHistoryVersionPage")) {
  3066. Greasyfork.beautifyHistoryVersionPage();
  3067. }
  3068. if (PopsPanel.getValue("beautifyGreasyforkBeautify")) {
  3069. Greasyfork.beautifyGreasyforkBeautify();
  3070. }
  3071. if (PopsPanel.getValue("beautifyUploadImage")) {
  3072. Greasyfork.beautifyUploadImage();
  3073. }
  3074. if (PopsPanel.getValue("fullScreenOptimization")) {
  3075. Greasyfork.fullScreenOptimization();
  3076. }
  3077. DOMUtils.ready(function () {
  3078. GreasyforkMenu.initEnv();
  3079. if (PopsPanel.getValue("autoLogin")) {
  3080. Greasyfork.autoLogin();
  3081. }
  3082. GreasyforkMenu.handleLocalGotoCallBack();
  3083. Greasyfork.setFindCodeSearchBtn();
  3084. Greasyfork.setCollectScriptBtn();
  3085. Greasyfork.repairImgShow();
  3086. Greasyfork.repairCodeLineNumber();
  3087. if (PopsPanel.getValue("optimizeImageBrowsing")) {
  3088. Greasyfork.optimizeImageBrowsing();
  3089. }
  3090. if (PopsPanel.getValue("overlayBedImageClickEvent")) {
  3091. Greasyfork.overlayBedImageClickEvent();
  3092. }
  3093. Greasyfork.scriptHomepageAddedTodaySUpdate();
  3094. if (PopsPanel.getValue("addCopyCodeButton")) {
  3095. Greasyfork.addCopyCodeButton();
  3096. }
  3097. Greasyfork.addMarkdownCopyButton();
  3098. Greasyfork.languageSelectorLocale();
  3099. Greasyfork.filterDiscussions();
  3100. });
  3101. /* -----------------↑执行入口↑----------------- */
  3102. })();