Google Gemini Mod (Toolbar & Download)

Enhances Google Gemini with a toolbar for snippets and canvas content download.

当前为 2025-05-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Google Gemini Mod (Toolbar & Download)
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.0.5
  5. // @description Enhances Google Gemini with a toolbar for snippets and canvas content download.
  6. // @description[de] Verbessert Google Gemini mit einer Symbolleiste für Snippets und dem Herunterladen von Canvas-Inhalten.
  7. // @author Adromir
  8. // @match https://gemini.google.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
  10. // @license MIT
  11. // @licenseURL https://opensource.org/licenses/MIT
  12. // @homepageURL https://github.com/adromir/scripts/tree/main/userscripts/gemini-snippets
  13. // @supportURL https://github.com/adromir/scripts/issues
  14. // @grant GM_addStyle
  15. // @grant GM_setClipboard
  16. // @grant GM_getClipboard
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. // ===================================================================================
  23. // I. CONFIGURATION SECTION
  24. // ===================================================================================
  25.  
  26. // --- Customizable Labels for Toolbar Buttons ---
  27. const PASTE_BUTTON_LABEL = "📋 Paste";
  28. const DOWNLOAD_BUTTON_LABEL = "💾 Download Canvas as File";
  29.  
  30. // --- CSS Selectors for DOM Elements ---
  31. // Selector to find the h2 title element of an active canvas.
  32. const GEMINI_CANVAS_TITLE_TEXT_SELECTOR = "#app-root > main > side-navigation-v2 > bard-sidenav-container > bard-sidenav-content > div.content-wrapper > div > div.content-container > chat-window > immersive-panel > code-immersive-panel > toolbar > div > div.left-panel > h2.title-text.gds-title-s.ng-star-inserted";
  33. // Selector for the "Copy to Clipboard" button, relative to the toolbar element.
  34. const GEMINI_COPY_BUTTON_IN_TOOLBAR_SELECTOR = "div.action-buttons > copy-button.ng-star-inserted > button.copy-button";
  35.  
  36. // Selectors for the Gemini input field (for snippet insertion)
  37. const GEMINI_INPUT_FIELD_SELECTORS = [
  38. '.ql-editor p',
  39. '.ql-editor',
  40. 'div[contenteditable="true"]'
  41. ];
  42.  
  43. // --- Download Feature Configuration ---
  44. const DEFAULT_DOWNLOAD_EXTENSION = "txt";
  45.  
  46. // --- Regular Expressions for Filename Sanitization ---
  47. // eslint-disable-next-line no-control-regex
  48. const INVALID_FILENAME_CHARS_REGEX = /[<>:"/\\|?*\x00-\x1F]/g;
  49. const RESERVED_WINDOWS_NAMES_REGEX = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
  50. const FILENAME_WITH_EXT_REGEX = /^(.+)\.([a-zA-Z0-9]{1,8})$/;
  51. const SUBSTRING_FILENAME_REGEX = /([\w\s.,\-()[\\]{}'!~@#$%^&+=]+?\.([a-zA-Z0-9]{1,8}))(?=\s|$|[,.;:!?])/g;
  52.  
  53. // ===================================================================================
  54. // II. TOOLBAR ELEMENT DEFINITIONS
  55. // ===================================================================================
  56.  
  57. const buttonSnippets = [
  58. { label: "Greeting", text: "Hello Gemini!" },
  59. { label: "Explain", text: "Could you please explain ... in more detail?" },
  60. ];
  61.  
  62. const dropdownConfigurations = [
  63. {
  64. placeholder: "Actions...",
  65. options: [
  66. { label: "Summarize", text: "Please summarize the following text:\n" },
  67. { label: "Ideas", text: "Give me 5 ideas for ..." },
  68. { label: "Code (JS)", text: "Give me a JavaScript code example for ..." },
  69. ]
  70. },
  71. {
  72. placeholder: "Translations",
  73. options: [
  74. { label: "DE -> EN", text: "Translate the following into English:\n" },
  75. { label: "EN -> DE", text: "Translate the following into German:\n" },
  76. { label: "Correct Text", text: "Please correct the grammar and spelling in the following text:\n" }
  77. ]
  78. },
  79. ];
  80.  
  81. // ===================================================================================
  82. // III. SCRIPT LOGIC
  83. // ===================================================================================
  84. // --- Embedded CSS for the Toolbar ---
  85. const embeddedCSS = `
  86. #gemini-snippet-toolbar-userscript {
  87. position: fixed !important; top: 0 !important; left: 50% !important;
  88. transform: translateX(-50%) !important;
  89. width: auto !important;
  90. max-width: 80% !important;
  91. padding: 10px 15px !important;
  92. z-index: 999999 !important;
  93. display: flex !important; flex-wrap: wrap !important;
  94. gap: 8px !important; align-items: center !important; font-family: 'Roboto', 'Arial', sans-serif !important;
  95. box-sizing: border-box !important; background-color: rgba(40, 42, 44, 0.95) !important;
  96. border-radius: 0 0 16px 16px !important;
  97. box-shadow: 0 4px 12px rgba(0,0,0,0.25);
  98. }
  99. #gemini-snippet-toolbar-userscript button,
  100. #gemini-snippet-toolbar-userscript select {
  101. padding: 4px 10px !important; cursor: pointer !important; background-color: #202122 !important;
  102. color: #e3e3e3 !important; border-radius: 16px !important; font-size: 13px !important;
  103. font-family: inherit !important; font-weight: 500 !important; height: 28px !important;
  104. box-sizing: border-box !important; vertical-align: middle !important;
  105. transition: background-color 0.2s ease, transform 0.1s ease !important;
  106. border: none !important; flex-shrink: 0;
  107. }
  108. #gemini-snippet-toolbar-userscript select {
  109. padding-right: 25px !important;
  110. appearance: none !important;
  111. background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="%23e3e3e3" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/></svg>') !important;
  112. background-repeat: no-repeat !important;
  113. background-position: right 8px center !important;
  114. background-size: 12px 12px !important;
  115. }
  116. #gemini-snippet-toolbar-userscript option {
  117. background-color: #2a2a2a !important;
  118. color: #e3e3e3 !important;
  119. font-weight: normal !important;
  120. padding: 5px 10px !important;
  121. }
  122. #gemini-snippet-toolbar-userscript button:hover,
  123. #gemini-snippet-toolbar-userscript select:hover {
  124. background-color: #4a4e51 !important;
  125. }
  126. #gemini-snippet-toolbar-userscript button:active {
  127. background-color: #5f6368 !important;
  128. transform: scale(0.98) !important;
  129. }
  130. .userscript-toolbar-spacer {
  131. margin-left: auto !important;
  132. }
  133. `;
  134.  
  135. /**
  136. * Injects the embedded CSS using GM_addStyle.
  137. */
  138. function injectCustomCSS() {
  139. try {
  140. GM_addStyle(embeddedCSS);
  141. console.log("Gemini Mod Userscript: Custom CSS injected successfully.");
  142. } catch (error) {
  143. console.error("Gemini Mod Userscript: Failed to inject custom CSS:", error);
  144. const styleId = 'gemini-mod-userscript-styles';
  145. if (document.getElementById(styleId)) return;
  146. const style = document.createElement('style');
  147. style.id = styleId;
  148. style.textContent = embeddedCSS;
  149. document.head.appendChild(style);
  150. }
  151. }
  152.  
  153. /**
  154. * Displays a message to the user (console and alert).
  155. * @param {string} message - The message to display.
  156. * @param {boolean} isError - True if it's an error message.
  157. */
  158. function displayUserscriptMessage(message, isError = true) {
  159. const prefix = "Gemini Mod Userscript: ";
  160. if (isError) console.error(prefix + message);
  161. else console.log(prefix + message);
  162. alert(prefix + message);
  163. }
  164.  
  165. /**
  166. * Moves the cursor to the end of the provided element's content.
  167. * @param {Element} element - The contenteditable element or paragraph within it.
  168. */
  169. function moveCursorToEnd(element) {
  170. try {
  171. const range = document.createRange();
  172. const sel = window.getSelection();
  173. range.selectNodeContents(element);
  174. range.collapse(false);
  175. sel.removeAllRanges();
  176. sel.addRange(range);
  177. element.focus();
  178. } catch (e) {
  179. console.error("Gemini Mod Userscript: Error setting cursor position:", e);
  180. }
  181. }
  182.  
  183. /**
  184. * Finds the target Gemini input element.
  185. * @returns {Element | null} The found input element or null.
  186. */
  187. function findTargetInputElement() {
  188. let targetInputElement = null;
  189. for (const selector of GEMINI_INPUT_FIELD_SELECTORS) {
  190. const element = document.querySelector(selector);
  191. if (element) {
  192. if (element.classList.contains('ql-editor')) {
  193. const pInEditor = element.querySelector('p');
  194. targetInputElement = pInEditor || element;
  195. } else {
  196. targetInputElement = element;
  197. }
  198. break;
  199. }
  200. }
  201. return targetInputElement;
  202. }
  203.  
  204. /**
  205. * Inserts text into the Gemini input field, always appending.
  206. * @param {string} textToInsert - The text snippet to insert.
  207. */
  208. function insertSnippetText(textToInsert) {
  209. let targetInputElement = findTargetInputElement();
  210. if (!targetInputElement) {
  211. displayUserscriptMessage("Could not find Gemini input field.");
  212. return;
  213. }
  214. let actualInsertionPoint = targetInputElement;
  215. if (targetInputElement.classList.contains('ql-editor')) {
  216. let p = targetInputElement.querySelector('p');
  217. if (!p) {
  218. p = document.createElement('p');
  219. targetInputElement.appendChild(p);
  220. }
  221. actualInsertionPoint = p;
  222. }
  223. actualInsertionPoint.focus();
  224. setTimeout(() => {
  225. moveCursorToEnd(actualInsertionPoint);
  226. let insertedViaExec = false;
  227. try {
  228. insertedViaExec = document.execCommand('insertText', false, textToInsert);
  229. } catch (e) {
  230. console.warn("Gemini Mod Userscript: execCommand('insertText') threw an error:", e);
  231. }
  232. if (!insertedViaExec) {
  233. if (actualInsertionPoint.innerHTML === '<br>') actualInsertionPoint.innerHTML = '';
  234. actualInsertionPoint.textContent += textToInsert;
  235. moveCursorToEnd(actualInsertionPoint);
  236. }
  237. const editorToDispatchOn = document.querySelector('.ql-editor') || targetInputElement;
  238. if (editorToDispatchOn) {
  239. editorToDispatchOn.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
  240. editorToDispatchOn.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
  241. }
  242. console.log("Gemini Mod Userscript: Snippet inserted.");
  243. }, 50);
  244. }
  245.  
  246. /**
  247. * Handles the paste button click. Reads from clipboard and inserts text.
  248. */
  249. async function handlePasteButtonClick() {
  250. try {
  251. if (!navigator.clipboard || !navigator.clipboard.readText) {
  252. displayUserscriptMessage("Clipboard access is not available or not permitted.");
  253. return;
  254. }
  255. const text = await navigator.clipboard.readText();
  256. if (text) insertSnippetText(text);
  257. else console.log("Gemini Mod Userscript: Clipboard is empty.");
  258. } catch (err) {
  259. console.error('Gemini Mod Userscript: Failed to read clipboard contents: ', err);
  260. displayUserscriptMessage(err.name === 'NotAllowedError' ? 'Permission to read clipboard was denied.' : 'Failed to paste from clipboard. See console.');
  261. }
  262. }
  263.  
  264. /**
  265. * Helper function to ensure filename length does not exceed a maximum.
  266. * @param {string} filename - The filename to check.
  267. * @param {number} maxLength - The maximum allowed length.
  268. * @returns {string} The potentially truncated filename.
  269. */
  270. function ensureLength(filename, maxLength = 255) {
  271. if (filename.length <= maxLength) {
  272. return filename;
  273. }
  274. const dotIndex = filename.lastIndexOf('.');
  275. if (dotIndex === -1 || dotIndex < filename.length - 10 ) {
  276. return filename.substring(0, maxLength);
  277. }
  278. const base = filename.substring(0, dotIndex);
  279. const ext = filename.substring(dotIndex);
  280. const maxBaseLength = maxLength - ext.length;
  281. if (maxBaseLength <= 0) {
  282. return filename.substring(0, maxLength);
  283. }
  284. return base.substring(0, maxBaseLength) + ext;
  285. }
  286.  
  287. /**
  288. * Sanitizes a base filename part (no extension).
  289. * @param {string} baseName - The base name to sanitize.
  290. * @returns {string} The sanitized base name.
  291. */
  292. function sanitizeBasename(baseName) {
  293. if (typeof baseName !== 'string' || baseName.trim() === "") return "downloaded_document";
  294. let sanitized = baseName.trim()
  295. .replace(INVALID_FILENAME_CHARS_REGEX, '_')
  296. .replace(/\s+/g, '_')
  297. .replace(/__+/g, '_')
  298. .replace(/^[_.-]+|[_.-]+$/g, '');
  299. if (!sanitized || RESERVED_WINDOWS_NAMES_REGEX.test(sanitized)) {
  300. sanitized = `_${sanitized || "file"}_`;
  301. sanitized = sanitized.replace(INVALID_FILENAME_CHARS_REGEX, '_').replace(/\s+/g, '_').replace(/__+/g, '_').replace(/^[_.-]+|[_.-]+$/g, '');
  302. }
  303. return sanitized || "downloaded_document";
  304. }
  305.  
  306. /**
  307. * Determines the filename for download based on the canvas title,
  308. * prioritizing a `basename.ext` structure if found.
  309. * @param {string} title - The original string (e.g., canvas title).
  310. * @param {string} defaultExtension - The default extension if no structure is found.
  311. * @returns {string} A processed filename.
  312. */
  313. function determineFilename(title, defaultExtension = "txt") {
  314. const logPrefix = "Gemini Mod Userscript: determineFilename - ";
  315. if (!title || typeof title !== 'string' || title.trim() === "") {
  316. console.log(`${logPrefix}Input title invalid or empty, defaulting to "downloaded_document.${defaultExtension}".`);
  317. return ensureLength(`downloaded_document.${defaultExtension}`);
  318. }
  319. let trimmedTitle = title.trim();
  320. let baseNamePart = "";
  321. let extensionPart = "";
  322. const fullTitleMatch = trimmedTitle.match(FILENAME_WITH_EXT_REGEX);
  323. if (fullTitleMatch) {
  324. const potentialBase = fullTitleMatch[1];
  325. const potentialExt = fullTitleMatch[2].toLowerCase();
  326. if (!INVALID_FILENAME_CHARS_REGEX.test(potentialBase.replace(/\s/g, '_'))) {
  327. baseNamePart = potentialBase;
  328. extensionPart = potentialExt;
  329. console.log(`${logPrefix}Entire title "${trimmedTitle}" matches basename.ext. Base: "${baseNamePart}", Ext: "${extensionPart}"`);
  330. }
  331. }
  332. if (!extensionPart) {
  333. let lastMatch = null;
  334. let currentMatch;
  335. SUBSTRING_FILENAME_REGEX.lastIndex = 0;
  336. while ((currentMatch = SUBSTRING_FILENAME_REGEX.exec(trimmedTitle)) !== null) {
  337. lastMatch = currentMatch;
  338. }
  339. if (lastMatch) {
  340. const substringExtMatch = lastMatch[1].match(FILENAME_WITH_EXT_REGEX);
  341. if (substringExtMatch) {
  342. baseNamePart = substringExtMatch[1];
  343. extensionPart = substringExtMatch[2].toLowerCase();
  344. console.log(`${logPrefix}Found substring "${lastMatch[1]}" matching basename.ext. Base: "${baseNamePart}", Ext: "${extensionPart}"`);
  345. }
  346. }
  347. }
  348. if (extensionPart) {
  349. const sanitizedBase = sanitizeBasename(baseNamePart);
  350. return ensureLength(`${sanitizedBase}.${extensionPart}`);
  351. } else {
  352. console.log(`${logPrefix}No basename.ext pattern found. Sanitizing full title "${trimmedTitle}" with default extension "${defaultExtension}".`);
  353. const sanitizedTitleBase = sanitizeBasename(trimmedTitle);
  354. return ensureLength(`${sanitizedTitleBase}.${defaultExtension}`);
  355. }
  356. }
  357.  
  358. /**
  359. * Creates and triggers a download for the given text content.
  360. * @param {string} filename - The desired filename.
  361. * @param {string} content - The text content to download.
  362. */
  363. function triggerDownload(filename, content) {
  364. try {
  365. const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
  366. const url = URL.createObjectURL(blob);
  367. const a = document.createElement('a');
  368. a.href = url;
  369. a.download = filename;
  370. document.body.appendChild(a);
  371. a.click();
  372. document.body.removeChild(a);
  373. URL.revokeObjectURL(url);
  374. console.log(`Gemini Mod Userscript: Download triggered for "${filename}".`);
  375. } catch (error) {
  376. console.error(`Gemini Mod Userscript: Failed to trigger download for "${filename}":`, error);
  377. displayUserscriptMessage(`Failed to download: ${error.message}`);
  378. }
  379. }
  380.  
  381. /**
  382. * Handles the click of the global canvas download button.
  383. * Finds the active canvas title, then its toolbar and copy button,
  384. * then reads from clipboard and initiates download.
  385. */
  386. async function handleGlobalCanvasDownload() {
  387. const titleTextElement = document.querySelector(GEMINI_CANVAS_TITLE_TEXT_SELECTOR);
  388. if (!titleTextElement) {
  389. console.warn("Gemini Mod Userscript: No active canvas title found. Selector:", GEMINI_CANVAS_TITLE_TEXT_SELECTOR);
  390. displayUserscriptMessage("No active canvas found to download.");
  391. return;
  392. }
  393. console.log("Gemini Mod Userscript: Found canvas title element:", titleTextElement);
  394.  
  395. const toolbarElement = titleTextElement.closest('toolbar');
  396. if (!toolbarElement) {
  397. console.warn("Gemini Mod Userscript: Could not find parent toolbar for the title element. Searched for 'toolbar' tag from title.");
  398. displayUserscriptMessage("Could not locate the toolbar for the active canvas.");
  399. return;
  400. }
  401. console.log("Gemini Mod Userscript: Found toolbar element relative to title:", toolbarElement);
  402.  
  403. const copyButton = toolbarElement.querySelector(GEMINI_COPY_BUTTON_IN_TOOLBAR_SELECTOR);
  404. if (!copyButton) {
  405. console.warn("Gemini Mod Userscript: 'Copy to Clipboard' button not found within the identified toolbar. Selector used on toolbar:", GEMINI_COPY_BUTTON_IN_TOOLBAR_SELECTOR);
  406. displayUserscriptMessage("Could not find the 'Copy to Clipboard' button in the active canvas's toolbar.");
  407. return;
  408. }
  409. console.log("Gemini Mod Userscript: Found 'Copy to Clipboard' button:", copyButton);
  410. copyButton.click();
  411. console.log("Gemini Mod Userscript: Programmatically clicked 'Copy to Clipboard' button.");
  412.  
  413. setTimeout(async () => {
  414. try {
  415. if (!navigator.clipboard || !navigator.clipboard.readText) {
  416. displayUserscriptMessage("Clipboard access not available.");
  417. return;
  418. }
  419. const clipboardContent = await navigator.clipboard.readText();
  420. console.log("Gemini Mod Userscript: Successfully read from clipboard.");
  421. if (!clipboardContent || clipboardContent.trim() === "") {
  422. displayUserscriptMessage("Clipboard empty after copy. Nothing to download.");
  423. return;
  424. }
  425. const canvasTitle = (titleTextElement.textContent || "Untitled Canvas").trim();
  426. const filename = determineFilename(canvasTitle);
  427. triggerDownload(filename, clipboardContent);
  428. console.log("Gemini Mod Userscript: Global download initiated for canvas title:", canvasTitle, "using clipboard content. Filename:", filename);
  429. } catch (err) {
  430. console.error('Gemini Mod Userscript: Error reading from clipboard:', err);
  431. displayUserscriptMessage(err.name === 'NotAllowedError' ? 'Clipboard permission denied.' : 'Failed to read clipboard.');
  432. }
  433. }, 300);
  434. }
  435.  
  436. /**
  437. * Creates the snippet toolbar and adds it to the page.
  438. */
  439. function createToolbar() {
  440. const toolbarId = 'gemini-snippet-toolbar-userscript';
  441. if (document.getElementById(toolbarId)) {
  442. console.log("Gemini Mod Userscript: Toolbar already exists.");
  443. return;
  444. }
  445. console.log("Gemini Mod Userscript: Initializing toolbar...");
  446. const toolbar = document.createElement('div');
  447. toolbar.id = toolbarId;
  448. buttonSnippets.forEach(snippet => {
  449. const button = document.createElement('button');
  450. button.textContent = snippet.label;
  451. button.title = snippet.text;
  452. button.addEventListener('click', () => insertSnippetText(snippet.text));
  453. toolbar.appendChild(button);
  454. });
  455. dropdownConfigurations.forEach(config => {
  456. if (config.options && config.options.length > 0) {
  457. const select = document.createElement('select');
  458. select.title = config.placeholder || "Select snippet";
  459. const defaultOption = document.createElement('option');
  460. defaultOption.textContent = config.placeholder || "Select...";
  461. defaultOption.value = "";
  462. defaultOption.disabled = true;
  463. defaultOption.selected = true;
  464. select.appendChild(defaultOption);
  465. config.options.forEach(snippet => {
  466. const option = document.createElement('option');
  467. option.textContent = snippet.label;
  468. option.value = snippet.text;
  469. select.appendChild(option);
  470. });
  471. select.addEventListener('change', (event) => {
  472. const selectedText = event.target.value;
  473. if (selectedText) {
  474. insertSnippetText(selectedText);
  475. event.target.selectedIndex = 0;
  476. }
  477. });
  478. toolbar.appendChild(select);
  479. }
  480. });
  481. const spacer = document.createElement('div');
  482. spacer.className = 'userscript-toolbar-spacer';
  483. toolbar.appendChild(spacer);
  484. const pasteButton = document.createElement('button');
  485. pasteButton.textContent = PASTE_BUTTON_LABEL;
  486. pasteButton.title = "Paste from Clipboard";
  487. pasteButton.addEventListener('click', handlePasteButtonClick);
  488. toolbar.appendChild(pasteButton);
  489. const globalDownloadButton = document.createElement('button');
  490. globalDownloadButton.textContent = DOWNLOAD_BUTTON_LABEL;
  491. globalDownloadButton.title = "Download active canvas content (uses canvas's copy button)";
  492. globalDownloadButton.addEventListener('click', handleGlobalCanvasDownload);
  493. toolbar.appendChild(globalDownloadButton);
  494. document.body.insertBefore(toolbar, document.body.firstChild);
  495. console.log("Gemini Mod Userscript: Toolbar inserted.");
  496. }
  497.  
  498. /**
  499. * Handles dark mode. For a userscript, this is mostly about adapting to the site's
  500. * existing dark mode, if necessary for the toolbar.
  501. */
  502. function handleDarkModeForUserscript() {
  503. console.log("Gemini Mod Userscript: Dark mode handling is passive (toolbar is dark by default).");
  504. }
  505.  
  506. // --- Initialization Logic ---
  507. function init() {
  508. console.log("Gemini Mod Userscript: Initializing...");
  509. injectCustomCSS();
  510. const M_INITIALIZATION_DELAY = 1500;
  511. setTimeout(() => {
  512. try {
  513. createToolbar();
  514. handleDarkModeForUserscript();
  515. console.log("Gemini Mod Userscript: Fully initialized.");
  516. } catch(e) {
  517. console.error("Gemini Mod Userscript: Error during delayed initialization:", e);
  518. displayUserscriptMessage("Error initializing toolbar. See console.");
  519. }
  520. }, M_INITIALIZATION_DELAY);
  521. }
  522.  
  523. if (document.readyState === 'loading') {
  524. window.addEventListener('DOMContentLoaded', init);
  525. } else {
  526. init();
  527. }
  528.  
  529. })();

QingJ © 2025

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