Civitai Prompt Autocomplete & Tag Wiki

Adds tag autocomplete and wiki lookup features

当前为 2025-03-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Civitai Prompt Autocomplete & Tag Wiki
  3. // @namespace http://tampermonkey.net/
  4. // @version 4.2
  5. // @description Adds tag autocomplete and wiki lookup features
  6. // @author AndroidXL
  7. // @match https://civitai.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=civitai.com
  9. // @grant GM.xmlHttpRequest
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // All variable declarations moved to top
  17. let promptInput = null;
  18. let suggestionsBox = null;
  19. let currentSuggestions = [];
  20. let selectedSuggestionIndex = -1;
  21. let debounceTimer;
  22. const debounceDelay = 50;
  23. let lastCurrentWord = "";
  24. let lastStartPos = 0; // New variable to track word start position
  25. let wikiOverlay = null;
  26. let wikiSearchContainer = null;
  27. let wikiContent = null;
  28. let currentPosts = [];
  29. let currentPostIndex = 0;
  30. let wikiInitialized = false;
  31. let autocompleteEnabled = true; // Default state for autocomplete
  32. let wikiHotkey = 't'; // Default hotkey for wiki
  33. let settingsOpen = false;
  34.  
  35. // Wiki history navigation variables
  36. let wikiHistory = [];
  37. let historyIndex = -1;
  38. let isNavigatingHistory = false;
  39.  
  40. // Initialize customTags with defaults, will be overridden by localStorage if available
  41. let customTags = {
  42. 'quality': 'masterpiece, best quality, amazing quality, very detailed',
  43. 'quality_pony': 'score_9, score_8_up, score_7_up, score_6_up',
  44. };
  45.  
  46. // Create and inject styles without GM_addStyle
  47. const styleElement = document.createElement('style');
  48. styleElement.textContent = `
  49. #autocomplete-suggestions-box {
  50. position: absolute;
  51. background-color: #1a1b1e;
  52. border: 1px solid #333;
  53. border-radius: 5px;
  54. margin-top: 2px;
  55. z-index: 100;
  56. overflow-y: auto;
  57. max-height: 150px;
  58. width: calc(100% - 6px);
  59. padding: 2px;
  60. box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
  61. }
  62. #autocomplete-suggestions-box div {
  63. padding: 4px 8px;
  64. cursor: pointer;
  65. white-space: nowrap;
  66. overflow: hidden;
  67. text-overflow: ellipsis;
  68. color: #C1C2C5;
  69. font-size: 14px;
  70. }
  71. #autocomplete-suggestions-box div:hover {
  72. background-color: #282a2d;
  73. }
  74. .autocomplete-selected {
  75. background-color: #383a3e;
  76. }
  77. .suggestion-count {
  78. color: #98C379;
  79. font-weight: normal;
  80. margin-left: 8px;
  81. font-size: 0.9em;
  82. }
  83.  
  84. .wiki-search-overlay {
  85. position: fixed;
  86. top: 0;
  87. left: 0;
  88. width: 100%;
  89. height: 100%;
  90. background: rgba(0,0,0,0.5);
  91. z-index: 9999;
  92. display: none;
  93. overflow-y: auto;
  94. padding: 20px;
  95. }
  96.  
  97. .wiki-search-container {
  98. position: relative;
  99. width: 90%;
  100. max-width: 800px;
  101. margin: 40px auto;
  102. transition: all 0.3s ease;
  103. }
  104.  
  105. .wiki-search-bar {
  106. width: 100%;
  107. padding: 12px;
  108. background: rgba(26,27,30,0.95);
  109. border: 1px solid #383a3e;
  110. border-radius: 8px;
  111. color: #fff;
  112. font-size: 16px;
  113. }
  114.  
  115. /* Container for all buttons on the right */
  116. .wiki-buttons-container {
  117. position: absolute;
  118. top: 12px;
  119. right: 12px;
  120. display: flex;
  121. align-items: center;
  122. gap: 8px;
  123. z-index: 10002;
  124. }
  125.  
  126. .wiki-settings-button {
  127. background: rgba(26,27,30,0.95);
  128. color: #C1C2C5;
  129. border: 1px solid #383a3e;
  130. border-radius: 4px;
  131. padding: 5px 10px;
  132. cursor: pointer;
  133. font-size: 14px;
  134. height: 30px;
  135. display: flex;
  136. align-items: center;
  137. }
  138.  
  139. /* Wiki navigation buttons */
  140. .wiki-nav-history {
  141. display: flex;
  142. gap: 5px;
  143. }
  144.  
  145. .wiki-nav-button {
  146. background: rgba(26,27,30,0.95);
  147. color: #C1C2C5;
  148. border: 1px solid #383a3e;
  149. border-radius: 4px;
  150. width: 30px;
  151. height: 30px;
  152. display: flex;
  153. align-items: center;
  154. justify-content: center;
  155. cursor: pointer;
  156. font-size: 16px;
  157. opacity: 0.7;
  158. transition: opacity 0.3s, background-color 0.3s;
  159. }
  160.  
  161. .wiki-nav-button:hover:not(:disabled) {
  162. background: #383a3e;
  163. opacity: 1;
  164. }
  165.  
  166. .wiki-nav-button:disabled {
  167. cursor: not-allowed;
  168. opacity: 0.3;
  169. }
  170.  
  171. .wiki-settings-button:hover {
  172. background: #383a3e;
  173. }
  174.  
  175. .wiki-settings-panel {
  176. position: absolute;
  177. top: 50%;
  178. left: 50%;
  179. transform: translate(-50%, -50%);
  180. width: 90%;
  181. max-width: 600px;
  182. background: rgba(26,27,30,0.98);
  183. border: 1px solid #383a3e;
  184. border-radius: 8px;
  185. padding: 20px;
  186. z-index: 10003;
  187. color: #C1C2C5;
  188. box-shadow: 0 4px 20px rgba(0,0,0,0.4);
  189. }
  190.  
  191. .wiki-settings-panel h2 {
  192. margin-top: 0;
  193. border-bottom: 1px solid #383a3e;
  194. padding-bottom: 10px;
  195. }
  196.  
  197. .settings-section {
  198. margin-bottom: 20px;
  199. }
  200.  
  201. .settings-section h3 {
  202. margin-bottom: 10px;
  203. font-size: 16px;
  204. color: #98C379;
  205. }
  206.  
  207. .hotkey-setting {
  208. display: flex;
  209. align-items: center;
  210. margin-bottom: 10px;
  211. }
  212.  
  213. .hotkey-setting label {
  214. margin-right: 10px;
  215. }
  216.  
  217. .hotkey-setting input {
  218. width: 50px;
  219. background: #1a1b1e;
  220. border: 1px solid #383a3e;
  221. border-radius: 4px;
  222. padding: 5px;
  223. color: #fff;
  224. text-align: center;
  225. }
  226.  
  227. .custom-tags-section {
  228. margin-top: 15px;
  229. }
  230.  
  231. .custom-tag-row {
  232. display: flex;
  233. margin-bottom: 8px;
  234. gap: 10px;
  235. }
  236.  
  237. .custom-tag-name,
  238. .custom-tag-value {
  239. flex: 1;
  240. background: #1a1b1e;
  241. border: 1px solid #383a3e;
  242. border-radius: 4px;
  243. padding: 5px 8px;
  244. color: #fff;
  245. }
  246.  
  247. .custom-tag-controls {
  248. display: flex;
  249. gap: 5px;
  250. }
  251.  
  252. .btn {
  253. background: #383a3e;
  254. color: #C1C2C5;
  255. border: none;
  256. border-radius: 4px;
  257. padding: 5px 10px;
  258. cursor: pointer;
  259. font-size: 14px;
  260. }
  261.  
  262. .btn:hover {
  263. background: #4a4c52;
  264. }
  265.  
  266. .btn-save {
  267. background: #2c6e49;
  268. }
  269.  
  270. .btn-save:hover {
  271. background: #358f5f;
  272. }
  273.  
  274. .btn-delete {
  275. background: #6e2c2c;
  276. }
  277.  
  278. .btn-delete:hover {
  279. background: #913a3a;
  280. }
  281.  
  282. .btn-add {
  283. background: #2c4a6e;
  284. margin-top: 10px;
  285. }
  286.  
  287. .btn-add:hover {
  288. background: #385d89;
  289. }
  290.  
  291. .settings-panel-footer {
  292. display: flex;
  293. justify-content: flex-end;
  294. margin-top: 20px;
  295. padding-top: 15px;
  296. border-top: 1px solid #383a3e;
  297. gap: 10px;
  298. }
  299.  
  300. .wiki-content {
  301. background: rgba(26,27,30,0.95);
  302. border-radius: 8px;
  303. margin-top: 20px;
  304. padding: 20px;
  305. width: 100%;
  306. position: relative;
  307. }
  308.  
  309. .wiki-text-content {
  310. padding-right: 420px;
  311. min-height: 500px;
  312. word-break: break-word;
  313. overflow-wrap: break-word;
  314. }
  315.  
  316. .wiki-description {
  317. line-height: 1.4;
  318. white-space: pre-line;
  319. font-size: 15px;
  320. }
  321.  
  322. .wiki-image-section {
  323. position: absolute;
  324. top: 20px;
  325. right: 20px;
  326. width: 400px;
  327. background: rgba(0,0,0,0.2);
  328. border-radius: 8px;
  329. padding: 10px;
  330. display: flex;
  331. flex-direction: column;
  332. gap: 10px;
  333. }
  334.  
  335. .wiki-image-navigation {
  336. display: flex;
  337. justify-content: space-between;
  338. align-items: center;
  339. width: 100%;
  340. padding: 0 10px;
  341. position: relative;
  342. height: 40px;
  343. }
  344.  
  345. .image-nav-button {
  346. position: absolute;
  347. top: 50%;
  348. transform: translateY(-50%);
  349. background: rgba(0,0,0,0.7);
  350. color: white;
  351. border: none;
  352. width: 40px;
  353. height: 40px;
  354. cursor: pointer;
  355. border-radius: 20px;
  356. opacity: 0.8;
  357. transition: opacity 0.3s, background-color 0.3s;
  358. font-size: 18px;
  359. z-index: 2;
  360. display: flex;
  361. align-items: center;
  362. justify-content: center;
  363. }
  364.  
  365. .image-nav-button:hover {
  366. opacity: 1;
  367. background: rgba(0,0,0,0.9);
  368. }
  369.  
  370. .image-nav-button.prev {
  371. left: 10px;
  372. }
  373.  
  374. .image-nav-button.next {
  375. right: 10px;
  376. }
  377.  
  378. .wiki-image-container {
  379. width: 100%;
  380. height: 350px;
  381. position: relative;
  382. margin: 0;
  383. background: rgba(0,0,0,0.1);
  384. border-radius: 4px;
  385. overflow: hidden;
  386. }
  387.  
  388. .wiki-image {
  389. width: 100%;
  390. height: 100%;
  391. object-fit: contain;
  392. border-radius: 4px;
  393. transition: transform 0.3s ease;
  394. }
  395.  
  396. .wiki-image:hover {
  397. transform: scale(1.03);
  398. }
  399.  
  400. .wiki-image-section {
  401. position: absolute;
  402. top: 20px;
  403. right: 20px;
  404. width: 400px;
  405. background: rgba(0,0,0,0.2);
  406. border-radius: 8px;
  407. padding: 10px;
  408. display: flex;
  409. flex-direction: column;
  410. gap: 10px;
  411. }
  412.  
  413. .wiki-image-navigation {
  414. display: flex;
  415. justify-content: space-between;
  416. align-items: center;
  417. width: 100%;
  418. padding: 0 10px;
  419. }
  420.  
  421. .image-nav-button {
  422. background: rgba(0,0,0,0.5);
  423. color: white;
  424. border: none;
  425. padding: 8px 12px;
  426. cursor: pointer;
  427. border-radius: 4px;
  428. opacity: 0.7;
  429. transition: opacity 0.3s;
  430. font-size: 16px;
  431. }
  432.  
  433. .wiki-image-container {
  434. width: 100%;
  435. height: 350px;
  436. display: flex;
  437. justify-content: center;
  438. align-items: center;
  439. position: relative;
  440. margin: 0;
  441. background: rgba(0,0,0,0.1);
  442. border-radius: 4px;
  443. }
  444.  
  445. .wiki-image {
  446. max-width: 100%;
  447. max-height: 100%;
  448. object-fit: contain;
  449. border-radius: 4px;
  450. }
  451.  
  452. .wiki-nav-buttons {
  453. width: 100%;
  454. display: flex;
  455. justify-content: center;
  456. }
  457.  
  458. .wiki-button {
  459. padding: 8px 16px;
  460. background: #383a3e;
  461. border: none;
  462. border-radius: 4px;
  463. color: #fff;
  464. cursor: pointer;
  465. width: 100%;
  466. text-align: center;
  467. }
  468.  
  469. .wiki-tag {
  470. display: inline-block;
  471. margin: 2px 4px;
  472. padding: 2px 4px;
  473. background: rgba(97, 175, 239, 0.1);
  474. border-radius: 3px;
  475. color: #61afef;
  476. cursor: pointer;
  477. text-decoration: underline;
  478. }
  479.  
  480. .wiki-tag:hover {
  481. background: rgba(97, 175, 239, 0.2);
  482. }
  483.  
  484. .wiki-link {
  485. color: #98c379;
  486. text-decoration: underline;
  487. }
  488.  
  489. .wiki-loading {
  490. text-align: center;
  491. padding: 20px;
  492. }
  493.  
  494. .wiki-description {
  495. line-height: 1.6;
  496. white-space: pre-wrap;
  497. font-size: 15px;
  498. }
  499.  
  500. .wiki-description p {
  501. margin: 1em 0;
  502. }
  503.  
  504. .wiki-search-suggestions {
  505. position: fixed;
  506. margin-top: 2px;
  507. background: rgba(26,27,30,0.95);
  508. border: 1px solid #383a3e;
  509. border-radius: 0 0 8px 8px;
  510. max-height: 200px;
  511. overflow-y: auto;
  512. z-index: 10001;
  513. width: 90%;
  514. max-width: 800px;
  515. left: 50%;
  516. transform: translateX(-50%);
  517. }
  518.  
  519. .wiki-search-suggestion {
  520. padding: 8px 12px;
  521. cursor: pointer;
  522. color: #fff;
  523. }
  524.  
  525. .wiki-search-suggestion:hover,
  526. .wiki-search-suggestion.selected {
  527. background: #383a3e;
  528. }
  529.  
  530. .no-images-message {
  531. color: #666;
  532. text-align: center;
  533. padding: 20px;
  534. font-style: italic;
  535. }
  536.  
  537. @keyframes slideUp {
  538. from { transform: translateY(20px); opacity: 0; }
  539. to { transform: translateY(0); opacity: 1; }
  540. }
  541.  
  542. .wiki-description h1 { font-size: 1.8em; margin: 0.8em 0 0.4em; }
  543. .wiki-description h2 { font-size: 1.6em; margin: 0.7em 0 0.4em; }
  544. .wiki-description h3 { font-size: 1.4em; margin: 0.6em 0 0.4em; }
  545. .wiki-description h4 { font-size: 1.2em; margin: 0.5em 0 0.4em; }
  546. .wiki-description h5 { font-size: 1.1em; margin: 0.5em 0 0.4em; }
  547. .wiki-description h6 { font-size: 1em; margin: 0.5em 0 0.4em; }
  548. .wiki-description p { margin: 0.5em 0; }
  549.  
  550. .wiki-description ul {
  551. margin: 0.5em 0 0.5em 1.5em;
  552. padding: 0;
  553. }
  554.  
  555. .wiki-description li {
  556. margin: 0.3em 0;
  557. line-height: 1.4;
  558. }
  559.  
  560. /* Autocomplete toggle checkbox styles */
  561. .autocomplete-toggle {
  562. display: flex;
  563. align-items: center;
  564. margin-bottom: 5px;
  565. font-size: 0.9em;
  566. color: #C1C2C5;
  567. }
  568.  
  569. .autocomplete-toggle input {
  570. margin-right: 5px;
  571. }
  572.  
  573. .tag-validation-error {
  574. color: #f55;
  575. font-size: 12px;
  576. margin-top: 5px;
  577. }
  578. `;
  579. document.head.appendChild(styleElement);
  580.  
  581. // Load settings from localStorage
  582. function loadSettings() {
  583. try {
  584. // Load autocomplete preference
  585. const savedAutoComplete = localStorage.getItem('civitai-autocomplete-enabled');
  586. if (savedAutoComplete !== null) {
  587. autocompleteEnabled = savedAutoComplete === 'true';
  588. }
  589.  
  590. // Load wiki hotkey
  591. const savedHotkey = localStorage.getItem('civitai-wiki-hotkey');
  592. if (savedHotkey) {
  593. wikiHotkey = savedHotkey;
  594. }
  595.  
  596. // Load custom tags
  597. const savedTags = localStorage.getItem('civitai-custom-tags');
  598. if (savedTags) {
  599. try {
  600. customTags = JSON.parse(savedTags);
  601. } catch (e) {
  602. console.error('Error parsing custom tags:', e);
  603. // If parsing fails, keep the default tags
  604. }
  605. }
  606.  
  607. debug('Settings loaded from localStorage');
  608. } catch (e) {
  609. console.error('Error loading settings:', e);
  610. // Use defaults if there's an error
  611. }
  612. }
  613.  
  614. // Save settings to localStorage
  615. function saveSettings() {
  616. try {
  617. localStorage.setItem('civitai-autocomplete-enabled', autocompleteEnabled);
  618. localStorage.setItem('civitai-wiki-hotkey', wikiHotkey);
  619. localStorage.setItem('civitai-custom-tags', JSON.stringify(customTags));
  620. debug('Settings saved to localStorage');
  621. } catch (e) {
  622. console.error('Error saving settings:', e);
  623. }
  624. }
  625.  
  626. // Load settings when script starts
  627. loadSettings();
  628.  
  629. // Replace all initialization code with this new version
  630. function handleInputEvents(e) {
  631. const input = e.target;
  632. if (input.id === 'input_prompt' && autocompleteEnabled) {
  633. const currentWordObj = getCurrentWord(input.value, input.selectionStart);
  634. lastCurrentWord = currentWordObj.word;
  635. lastStartPos = currentWordObj.startPos;
  636. fetchSuggestions(lastCurrentWord);
  637. }
  638. }
  639.  
  640. // Create the toggle checkbox
  641. function createAutocompleteToggle() {
  642. const toggleContainer = document.createElement('div');
  643. toggleContainer.className = 'autocomplete-toggle';
  644.  
  645. const checkbox = document.createElement('input');
  646. checkbox.type = 'checkbox';
  647. checkbox.id = 'autocomplete-toggle-checkbox';
  648. checkbox.checked = autocompleteEnabled;
  649.  
  650. const label = document.createElement('label');
  651. label.htmlFor = 'autocomplete-toggle-checkbox';
  652. label.textContent = 'Enable Tag Autocomplete';
  653.  
  654. toggleContainer.appendChild(checkbox);
  655. toggleContainer.appendChild(label);
  656.  
  657. checkbox.addEventListener('change', function() {
  658. autocompleteEnabled = this.checked;
  659. saveSettings();
  660. if (!autocompleteEnabled) {
  661. clearSuggestions();
  662. }
  663. });
  664.  
  665. return toggleContainer;
  666. }
  667.  
  668. function handleKeydownEvents(e) {
  669. if (e.target.id !== 'input_prompt') return;
  670.  
  671. if (e.key === 'ArrowDown') {
  672. e.preventDefault();
  673. if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) {
  674. selectedSuggestionIndex = Math.min(selectedSuggestionIndex + 1, currentSuggestions.length - 1);
  675. updateSuggestionSelection();
  676. }
  677. } else if (e.key === 'ArrowUp') {
  678. e.preventDefault();
  679. if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) {
  680. selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, -1);
  681. updateSuggestionSelection();
  682. }
  683. } else if (e.key === 'Tab' || e.key === 'Enter') {
  684. if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) {
  685. e.preventDefault();
  686. if (selectedSuggestionIndex !== -1) {
  687. insertSuggestion(currentSuggestions[selectedSuggestionIndex].label);
  688. } else {
  689. insertSuggestion(currentSuggestions[0].label);
  690. }
  691. }
  692. } else if (e.key === 'Escape') {
  693. clearSuggestions();
  694. }
  695. }
  696.  
  697. function setupAutocomplete() {
  698. // Clean up old elements
  699. if (suggestionsBox) {
  700. suggestionsBox.remove();
  701. }
  702.  
  703. // Remove old toggle if it exists
  704. const oldToggle = document.getElementById('autocomplete-toggle-checkbox');
  705. if (oldToggle && oldToggle.parentNode) {
  706. oldToggle.parentNode.remove();
  707. }
  708.  
  709. promptInput = document.getElementById('input_prompt');
  710. if (!promptInput) return;
  711.  
  712. // Create new suggestions box
  713. suggestionsBox = document.createElement('div');
  714. suggestionsBox.id = 'autocomplete-suggestions-box';
  715. suggestionsBox.style.display = 'none';
  716.  
  717. // Create the toggle and insert it before the input
  718. const toggleContainer = createAutocompleteToggle();
  719. promptInput.parentNode.parentNode.parentNode.parentNode.insertBefore(toggleContainer, promptInput.parentNode.parentNode.parentNode.parentNode.firstChild);
  720.  
  721. // Insert suggestions box after the input
  722. promptInput.parentNode.insertBefore(suggestionsBox, promptInput.nextSibling);
  723.  
  724. // Remove old event listeners and add new ones using event delegation
  725. document.removeEventListener('input', handleInputEvents, true);
  726. document.removeEventListener('keydown', handleKeydownEvents, true);
  727. document.addEventListener('input', handleInputEvents, true);
  728. document.addEventListener('keydown', handleKeydownEvents, true);
  729.  
  730. // Handle clicks outside
  731. document.addEventListener('click', (e) => {
  732. if (!promptInput?.contains(e.target) && !suggestionsBox?.contains(e.target)) {
  733. clearSuggestions();
  734. }
  735. });
  736. }
  737.  
  738. // Set up a more aggressive observer
  739. const observer = new MutationObserver((mutations) => {
  740. for (const mutation of mutations) {
  741. const addedNodes = Array.from(mutation.addedNodes);
  742. const hasPromptInput = addedNodes.some(node =>
  743. node.id === 'input_prompt' ||
  744. node.querySelector?.('#input_prompt')
  745. );
  746.  
  747. if (hasPromptInput || !document.getElementById('autocomplete-suggestions-box')) {
  748. setupAutocomplete();
  749. break;
  750. }
  751. }
  752. });
  753.  
  754. // Start observing with more specific config
  755. observer.observe(document.body, {
  756. childList: true,
  757. subtree: true,
  758. attributes: true,
  759. attributeFilter: ['id']
  760. });
  761.  
  762. // Initial setup
  763. setupAutocomplete();
  764. initializeWiki();
  765.  
  766. function fetchSuggestions(term) {
  767. if (!term || !autocompleteEnabled) {
  768. clearSuggestions();
  769. return;
  770. }
  771.  
  772. // First, check custom tags
  773. const matchingCustomTags = Object.keys(customTags)
  774. .filter(tag => tag.toLowerCase().startsWith(term.toLowerCase()))
  775. .map(tag => ({
  776. label: tag,
  777. count: '⭐', // Star to indicate custom tag
  778. isCustom: true,
  779. insertText: customTags[tag]
  780. }));
  781.  
  782. // If we have matching custom tags, show them immediately
  783. if (matchingCustomTags.length > 0) {
  784. currentSuggestions = matchingCustomTags;
  785. showSuggestions();
  786. }
  787.  
  788. // Continue with API request for regular tags
  789. const apiTerm = term.replace(/ /g, '_');
  790.  
  791. clearTimeout(debounceTimer);
  792. debounceTimer = setTimeout(() => {
  793. GM.xmlHttpRequest({
  794. method: 'GET',
  795. url: `https://gelbooru.com/index.php?page=autocomplete2&term=${encodeURIComponent(apiTerm)}&type=tag_query&limit=10`,
  796. onload: function(response) {
  797. if (response.status === 200) {
  798. try {
  799. const data = JSON.parse(response.responseText);
  800. const fetchedSuggestions = data.map(item => ({
  801. label: item.label.replace(/[()]/g, '\\$&'),
  802. count: item.post_count,
  803. isCustom: false
  804. }));
  805. // Combine custom and API suggestions
  806. filterAndShowSuggestions([...matchingCustomTags, ...fetchedSuggestions]);
  807. } catch (e) {
  808. console.error("Error parsing Gelbooru API response:", e);
  809. clearSuggestions();
  810. }
  811. } else {
  812. console.error("Gelbooru API request failed:", response.status, response.statusText);
  813. clearSuggestions();
  814. }
  815. },
  816. onerror: function(error) {
  817. console.error("Gelbooru API request error:", error);
  818. clearSuggestions();
  819. }
  820. });
  821. }, debounceDelay);
  822. }
  823.  
  824. function filterAndShowSuggestions(fetchedSuggestions) {
  825. const existingTags = promptInput.value.split(',').map(tag => tag.trim().toLowerCase());
  826. const filteredSuggestions = fetchedSuggestions.filter(suggestion => {
  827. return !existingTags.includes(suggestion.label.toLowerCase())
  828. });
  829.  
  830. currentSuggestions = filteredSuggestions;
  831.  
  832. showSuggestions();
  833. }
  834.  
  835.  
  836. function showSuggestions() {
  837. if (currentSuggestions.length === 0) {
  838. clearSuggestions();
  839. return;
  840. }
  841.  
  842. suggestionsBox.innerHTML = '';
  843.  
  844.  
  845. currentSuggestions.forEach((suggestion, index) => {
  846. const suggestionDiv = document.createElement('div');
  847. suggestionDiv.innerHTML = `${suggestion.label} <span class="suggestion-count">[${suggestion.count}]</span>`;
  848. suggestionDiv.addEventListener('click', () => {
  849. insertSuggestion(suggestion.label);
  850. });
  851. suggestionsBox.appendChild(suggestionDiv);
  852. });
  853.  
  854. suggestionsBox.style.display = 'block';
  855. selectedSuggestionIndex = -1;
  856. }
  857.  
  858. function clearSuggestions() {
  859. if (suggestionsBox) {
  860. suggestionsBox.style.display = 'none';
  861. suggestionsBox.innerHTML = '';
  862. }
  863. currentSuggestions = [];
  864. selectedSuggestionIndex = -1;
  865. }
  866.  
  867. function insertSuggestion(suggestion) {
  868. // Find the matching suggestion object
  869. const suggestionObj = currentSuggestions.find(s => s.label === suggestion);
  870. const textToInsert = (suggestionObj?.isCustom ? suggestionObj.insertText : suggestion)
  871.  
  872. // Use setRangeText to replace the current word with the suggestion
  873. const start = lastStartPos;
  874. const end = promptInput.selectionStart;
  875.  
  876. // Focus the input to ensure changes register in the undo stack
  877. promptInput.focus();
  878.  
  879. // Create a composition session to properly register in the undo stack
  880. // First delete the current word manually
  881. promptInput.setSelectionRange(start, end);
  882. document.execCommand('delete');
  883.  
  884. // Then insert the new text with execCommand
  885. document.execCommand('insertText', false, textToInsert + ', ');
  886.  
  887. // Simulate focus and blur to mimic user interaction
  888. promptInput.focus();
  889. promptInput.blur();
  890. setTimeout(() => {
  891. promptInput.focus();
  892. }, 0); // Delay refocus to allow React to process
  893.  
  894. // Clear suggestions and keep focus
  895. clearSuggestions();
  896. promptInput.focus();
  897. }
  898.  
  899. function updateSuggestionSelection() {
  900. if (!suggestionsBox) return;
  901.  
  902. const suggestionDivs = suggestionsBox.querySelectorAll('div');
  903. suggestionDivs.forEach((div, index) => {
  904. if (index === selectedSuggestionIndex) {
  905. div.classList.add('autocomplete-selected');
  906. div.scrollIntoView({ block: 'nearest' });
  907. } else {
  908. div.classList.remove('autocomplete-selected');
  909. }
  910. });
  911. }
  912.  
  913. function getCurrentWord(text, cursorPosition) {
  914. if (cursorPosition === undefined) cursorPosition = text.length;
  915.  
  916. const textBeforeCursor = text.substring(0, cursorPosition);
  917. const lastCommaIndex = textBeforeCursor.lastIndexOf(',');
  918. let startPos, word;
  919.  
  920. if (lastCommaIndex !== -1) {
  921. startPos = lastCommaIndex + 1;
  922. word = textBeforeCursor.substring(startPos).trim();
  923. // Find the exact position where the trimmed word starts
  924. if (word) {
  925. const leadingSpaces = textBeforeCursor.substring(startPos).length - textBeforeCursor.substring(startPos).trimLeft().length;
  926. startPos = startPos + leadingSpaces;
  927. }
  928. } else {
  929. startPos = 0;
  930. word = textBeforeCursor.trim();
  931. // If the text has leading spaces, adjust the start position
  932. if (word && textBeforeCursor !== word) {
  933. startPos = textBeforeCursor.indexOf(word);
  934. }
  935. }
  936. return { word, startPos };
  937. }
  938.  
  939. // Add debug logging function
  940. function debug(msg) {
  941. console.log(`[Wiki Debug] ${msg}`);
  942. }
  943.  
  944. // Create settings panel DOM
  945. function createSettingsPanel() {
  946. const settingsPanel = document.createElement('div');
  947. settingsPanel.className = 'wiki-settings-panel';
  948.  
  949. // Header
  950. const header = document.createElement('h2');
  951. header.textContent = 'Wiki & Autocomplete Settings';
  952. settingsPanel.appendChild(header);
  953.  
  954. // Hotkey section
  955. const hotkeySection = document.createElement('div');
  956. hotkeySection.className = 'settings-section';
  957.  
  958. const hotkeyTitle = document.createElement('h3');
  959. hotkeyTitle.textContent = 'Hotkeys';
  960. hotkeySection.appendChild(hotkeyTitle);
  961.  
  962. const hotkeyContent = document.createElement('div');
  963. hotkeyContent.className = 'hotkey-setting';
  964.  
  965. const hotkeyLabel = document.createElement('label');
  966. hotkeyLabel.textContent = 'Wiki search hotkey:';
  967.  
  968. const hotkeyInput = document.createElement('input');
  969. hotkeyInput.type = 'text';
  970. hotkeyInput.value = wikiHotkey;
  971. hotkeyInput.maxLength = 1;
  972. hotkeyInput.addEventListener('keydown', function(e) {
  973. e.preventDefault();
  974. this.value = e.key.toLowerCase();
  975. });
  976.  
  977. hotkeyContent.appendChild(hotkeyLabel);
  978. hotkeyContent.appendChild(hotkeyInput);
  979. hotkeySection.appendChild(hotkeyContent);
  980. settingsPanel.appendChild(hotkeySection);
  981.  
  982. // Custom tags section
  983. const tagsSection = document.createElement('div');
  984. tagsSection.className = 'settings-section';
  985.  
  986. const tagsTitle = document.createElement('h3');
  987. tagsTitle.textContent = 'Custom Tags';
  988. tagsSection.appendChild(tagsTitle);
  989.  
  990. const tagsContainer = document.createElement('div');
  991. tagsContainer.className = 'custom-tags-section';
  992.  
  993. // Create UI for each existing tag
  994. Object.keys(customTags).forEach(tag => {
  995. const tagRow = createTagRow(tag, customTags[tag]);
  996. tagsContainer.appendChild(tagRow);
  997. });
  998.  
  999. // Add new tag button
  1000. const addTagBtn = document.createElement('button');
  1001. addTagBtn.className = 'btn btn-add';
  1002. addTagBtn.textContent = '+ Add New Tag';
  1003. addTagBtn.addEventListener('click', function() {
  1004. const newTagRow = createTagRow('', '');
  1005. tagsContainer.insertBefore(newTagRow, addTagBtn);
  1006. newTagRow.querySelector('.custom-tag-name').focus();
  1007. });
  1008.  
  1009. tagsContainer.appendChild(addTagBtn);
  1010. tagsSection.appendChild(tagsContainer);
  1011. settingsPanel.appendChild(tagsSection);
  1012.  
  1013. // Footer with buttons
  1014. const footer = document.createElement('div');
  1015. footer.className = 'settings-panel-footer';
  1016.  
  1017. const cancelBtn = document.createElement('button');
  1018. cancelBtn.className = 'btn';
  1019. cancelBtn.textContent = 'Cancel';
  1020. cancelBtn.addEventListener('click', hideSettingsPanel);
  1021.  
  1022. const saveBtn = document.createElement('button');
  1023. saveBtn.className = 'btn btn-save';
  1024. saveBtn.textContent = 'Save Settings';
  1025. saveBtn.addEventListener('click', function() {
  1026. const errors = validateAndSaveSettings(hotkeyInput, tagsContainer);
  1027. if (errors.length === 0) {
  1028. hideSettingsPanel();
  1029. } else {
  1030. // Display errors
  1031. const existingError = settingsPanel.querySelector('.tag-validation-error');
  1032. if (existingError) existingError.remove();
  1033.  
  1034. const errorDiv = document.createElement('div');
  1035. errorDiv.className = 'tag-validation-error';
  1036. errorDiv.textContent = errors.join(', ');
  1037. footer.insertBefore(errorDiv, cancelBtn);
  1038. }
  1039. });
  1040.  
  1041. footer.appendChild(cancelBtn);
  1042. footer.appendChild(saveBtn);
  1043. settingsPanel.appendChild(footer);
  1044.  
  1045. return settingsPanel;
  1046. }
  1047.  
  1048. // Helper function to create a tag row
  1049. function createTagRow(name, value) {
  1050. const row = document.createElement('div');
  1051. row.className = 'custom-tag-row';
  1052.  
  1053. const nameInput = document.createElement('input');
  1054. nameInput.type = 'text';
  1055. nameInput.className = 'custom-tag-name';
  1056. nameInput.placeholder = 'Tag name';
  1057. nameInput.value = name;
  1058.  
  1059. const valueInput = document.createElement('input');
  1060. valueInput.type = 'text';
  1061. valueInput.className = 'custom-tag-value';
  1062. valueInput.placeholder = 'Tag value (comma separated)';
  1063. valueInput.value = value;
  1064.  
  1065. const controlsDiv = document.createElement('div');
  1066. controlsDiv.className = 'custom-tag-controls';
  1067.  
  1068. const deleteBtn = document.createElement('button');
  1069. deleteBtn.className = 'btn btn-delete';
  1070. deleteBtn.textContent = '🗑️';
  1071. deleteBtn.title = 'Delete tag';
  1072. deleteBtn.addEventListener('click', function() {
  1073. row.remove();
  1074. });
  1075.  
  1076. controlsDiv.appendChild(deleteBtn);
  1077.  
  1078. row.appendChild(nameInput);
  1079. row.appendChild(valueInput);
  1080. row.appendChild(controlsDiv);
  1081.  
  1082. return row;
  1083. }
  1084.  
  1085. // Validate settings and save
  1086. function validateAndSaveSettings(hotkeyInput, tagsContainer) {
  1087. const errors = [];
  1088.  
  1089. // Validate hotkey
  1090. const newHotkey = hotkeyInput.value.trim();
  1091. if (!newHotkey) {
  1092. errors.push('Hotkey cannot be empty');
  1093. } else {
  1094. wikiHotkey = newHotkey;
  1095. }
  1096.  
  1097. // Validate and collect tags
  1098. const newCustomTags = {};
  1099. const tagRows = tagsContainer.querySelectorAll('.custom-tag-row');
  1100. const tagNames = new Set();
  1101.  
  1102. tagRows.forEach(row => {
  1103. const nameInput = row.querySelector('.custom-tag-name');
  1104. const valueInput = row.querySelector('.custom-tag-value');
  1105.  
  1106. const name = nameInput.value.trim();
  1107. const value = valueInput.value.trim();
  1108.  
  1109. if (name && value) {
  1110. if (tagNames.has(name)) {
  1111. errors.push(`Duplicate tag name: ${name}`);
  1112. } else {
  1113. tagNames.add(name);
  1114. newCustomTags[name] = value;
  1115. }
  1116. } else if (name || value) {
  1117. errors.push(`Tag ${name || 'name'} is missing ${name ? 'value' : 'name'}`);
  1118. }
  1119. // Skip empty rows (both name and value empty)
  1120. });
  1121.  
  1122. if (errors.length === 0) {
  1123. customTags = newCustomTags;
  1124. saveSettings();
  1125. }
  1126.  
  1127. return errors;
  1128. }
  1129.  
  1130. // Show settings panel
  1131. function showSettingsPanel() {
  1132. settingsOpen = true;
  1133.  
  1134. // Remove any existing panel
  1135. const existingPanel = document.querySelector('.wiki-settings-panel');
  1136. if (existingPanel) existingPanel.remove();
  1137.  
  1138. // Create and append new panel
  1139. const settingsPanel = createSettingsPanel();
  1140. wikiOverlay.appendChild(settingsPanel);
  1141. }
  1142.  
  1143. // Hide settings panel
  1144. function hideSettingsPanel() {
  1145. const panel = document.querySelector('.wiki-settings-panel');
  1146. if (panel) panel.remove();
  1147. settingsOpen = false;
  1148. }
  1149.  
  1150. // Initialize wiki interface immediately
  1151. function initializeWiki() {
  1152. if (wikiInitialized) {
  1153. debug('Wiki already initialized');
  1154. return;
  1155. }
  1156.  
  1157. debug('Initializing wiki interface');
  1158.  
  1159. // Make sure settings are loaded
  1160. loadSettings();
  1161. // Continue with wiki initialization
  1162. wikiOverlay = document.createElement('div');
  1163. wikiOverlay.className = 'wiki-search-overlay';
  1164.  
  1165. wikiSearchContainer = document.createElement('div');
  1166. wikiSearchContainer.className = 'wiki-search-container';
  1167.  
  1168. const searchBar = document.createElement('input');
  1169. searchBar.className = 'wiki-search-bar';
  1170. searchBar.placeholder = 'Search tag wiki...';
  1171.  
  1172. // Create container for all buttons
  1173. const buttonsContainer = document.createElement('div');
  1174. buttonsContainer.className = 'wiki-buttons-container';
  1175.  
  1176. // Add navigation history buttons
  1177. const navContainer = document.createElement('div');
  1178. navContainer.className = 'wiki-nav-history';
  1179.  
  1180. const backButton = document.createElement('button');
  1181. backButton.className = 'wiki-nav-button back';
  1182. backButton.textContent = '<';
  1183. backButton.disabled = true;
  1184. backButton.title = 'Go back to previous tag';
  1185. backButton.addEventListener('click', navigateWikiHistory.bind(null, -1));
  1186.  
  1187. const forwardButton = document.createElement('button');
  1188. forwardButton.className = 'wiki-nav-button forward';
  1189. forwardButton.textContent = '>';
  1190. forwardButton.disabled = true;
  1191. forwardButton.title = 'Go forward to next tag';
  1192. forwardButton.addEventListener('click', navigateWikiHistory.bind(null, 1));
  1193.  
  1194. navContainer.appendChild(backButton);
  1195. navContainer.appendChild(forwardButton);
  1196.  
  1197. // Add settings button
  1198. const settingsButton = document.createElement('button');
  1199. settingsButton.className = 'wiki-settings-button';
  1200. settingsButton.textContent = '⚙️ Settings';
  1201. settingsButton.addEventListener('click', function(e) {
  1202. e.preventDefault();
  1203. showSettingsPanel();
  1204. });
  1205.  
  1206. // Add navigation buttons first, then settings button
  1207. buttonsContainer.appendChild(navContainer);
  1208. buttonsContainer.appendChild(settingsButton);
  1209.  
  1210. wikiContent = document.createElement('div');
  1211. wikiContent.className = 'wiki-content';
  1212. wikiContent.style.display = 'none';
  1213.  
  1214. wikiSearchContainer.appendChild(searchBar);
  1215. wikiSearchContainer.appendChild(buttonsContainer);
  1216. wikiSearchContainer.appendChild(wikiContent);
  1217. wikiOverlay.appendChild(wikiSearchContainer);
  1218. document.body.appendChild(wikiOverlay);
  1219.  
  1220. // Separate key handler based on configurable hotkey
  1221. document.addEventListener('keydown', function(e) {
  1222. if (e.key.toLowerCase() === wikiHotkey.toLowerCase() && !isInputFocused()) {
  1223. debug(`Hotkey ${wikiHotkey} pressed, showing wiki search`);
  1224. e.preventDefault();
  1225. showWikiSearch();
  1226. }
  1227. });
  1228.  
  1229. searchBar.addEventListener('keydown', async function(e) {
  1230. if (e.key === 'Enter') {
  1231. e.preventDefault();
  1232. await loadWikiInfo(searchBar.value);
  1233. } else if (e.key === 'Escape') {
  1234. if (settingsOpen) {
  1235. hideSettingsPanel();
  1236. } else {
  1237. hideWikiSearch();
  1238. }
  1239. }
  1240. });
  1241.  
  1242. wikiOverlay.addEventListener('click', function(e) {
  1243. if (e.target === wikiOverlay) {
  1244. if (settingsOpen) {
  1245. hideSettingsPanel();
  1246. } else {
  1247. hideWikiSearch();
  1248. }
  1249. }
  1250. });
  1251.  
  1252. setupWikiSearchAutocomplete(searchBar);
  1253.  
  1254. wikiInitialized = true;
  1255. debug('Wiki interface initialized');
  1256. }
  1257.  
  1258. // Navigate through wiki history
  1259. function navigateWikiHistory(direction) {
  1260. if (!wikiHistory.length) return;
  1261.  
  1262. const newIndex = historyIndex + direction;
  1263.  
  1264. if (newIndex >= 0 && newIndex < wikiHistory.length) {
  1265. isNavigatingHistory = true;
  1266. historyIndex = newIndex;
  1267. updateHistoryButtons();
  1268. loadWikiInfo(wikiHistory[historyIndex]);
  1269. }
  1270. }
  1271.  
  1272. // Update the state of history navigation buttons
  1273. function updateHistoryButtons() {
  1274. const backButton = document.querySelector('.wiki-nav-button.back');
  1275. const forwardButton = document.querySelector('.wiki-nav-button.forward');
  1276.  
  1277. if (!backButton || !forwardButton) return;
  1278.  
  1279. backButton.disabled = historyIndex <= 0;
  1280. forwardButton.disabled = historyIndex >= wikiHistory.length - 1;
  1281. }
  1282.  
  1283. function hideWikiSearch() {
  1284. debug('Hiding wiki search interface');
  1285. wikiOverlay.style.display = 'none';
  1286. hideSettingsPanel();
  1287. }
  1288.  
  1289. // Modified showWikiSearch function
  1290. function showWikiSearch() {
  1291. if (!wikiInitialized) {
  1292. debug('Attempting to show wiki before initialization');
  1293. initializeWiki();
  1294. }
  1295. debug('Showing wiki search interface');
  1296. wikiOverlay.style.display = 'block';
  1297. const searchBar = wikiSearchContainer.querySelector('.wiki-search-bar');
  1298. searchBar.value = '';
  1299. searchBar.focus();
  1300. wikiContent.style.display = 'none';
  1301.  
  1302. // Reset navigation buttons when opening search
  1303. updateHistoryButtons();
  1304. }
  1305.  
  1306. // Add keyboard shortcut for closing with escape
  1307. document.addEventListener('keydown', e => {
  1308. if (e.key === 'Escape' && wikiOverlay.style.display === 'block') {
  1309. if (settingsOpen) {
  1310. hideSettingsPanel();
  1311. } else {
  1312. hideWikiSearch();
  1313. }
  1314. }
  1315. });
  1316.  
  1317. // Initialize wiki immediately
  1318. initializeWiki();
  1319.  
  1320. // The rest of the script remains unchanged
  1321. function isInputFocused() {
  1322. const activeElement = document.activeElement;
  1323. return activeElement && (
  1324. activeElement.tagName === 'INPUT' ||
  1325. activeElement.tagName === 'TEXTAREA' ||
  1326. activeElement.isContentEditable
  1327. );
  1328. }
  1329.  
  1330. // Wiki helper functions
  1331. async function loadWikiInfo(tag) {
  1332. // Reset animation
  1333. wikiSearchContainer.style.animation = 'none';
  1334. wikiSearchContainer.offsetHeight; // Trigger reflow
  1335. wikiSearchContainer.style.animation = null;
  1336.  
  1337. // Update search bar value
  1338. const searchBar = wikiSearchContainer.querySelector('.wiki-search-bar');
  1339. searchBar.value = tag;
  1340.  
  1341. // Add to history if not navigating through history
  1342. if (!isNavigatingHistory) {
  1343. // If we're in the middle of the history and searching a new tag,
  1344. // remove all entries after current position
  1345. if (historyIndex < wikiHistory.length - 1 && historyIndex >= 0) {
  1346. wikiHistory = wikiHistory.slice(0, historyIndex + 1);
  1347. }
  1348.  
  1349. // Don't add duplicate consecutive entries
  1350. if (wikiHistory.length === 0 || wikiHistory[wikiHistory.length - 1] !== tag) {
  1351. wikiHistory.push(tag);
  1352. historyIndex = wikiHistory.length - 1;
  1353. }
  1354. } else {
  1355. // Reset the flag after navigation
  1356. isNavigatingHistory = false;
  1357. }
  1358.  
  1359. // Update button states
  1360. updateHistoryButtons();
  1361.  
  1362. wikiContent.innerHTML = '<div class="wiki-loading">Loading...</div>';
  1363. wikiContent.style.display = 'block';
  1364. wikiSearchContainer.style.animation = 'slideUp 0.3s forwards';
  1365.  
  1366. try {
  1367. const [wikiData, postsData] = await Promise.all([
  1368. fetchDanbooruWiki(tag),
  1369. fetchDanbooruPosts(tag)
  1370. ]);
  1371.  
  1372. currentPosts = postsData;
  1373. currentPostIndex = 0;
  1374.  
  1375. displayWikiContent(wikiData, tag);
  1376. if (currentPosts.length > 0) {
  1377. displayPostImage(currentPosts[0]);
  1378. }
  1379. } catch (error) {
  1380. wikiContent.innerHTML = `<div class="error">Error loading wiki: ${error.message}</div>`;
  1381. }
  1382. }
  1383.  
  1384. function fetchDanbooruWiki(tag) {
  1385. // Convert to lowercase and replace spaces with underscores
  1386. const formattedTag = tag.trim().toLowerCase().replace(/\s+/g, '_');
  1387. return new Promise((resolve, reject) => {
  1388. GM.xmlHttpRequest({
  1389. method: 'GET',
  1390. url: `https://danbooru.donmai.us/wiki_pages.json?search[title]=${encodeURIComponent(formattedTag)}`,
  1391. onload: response => resolve(JSON.parse(response.responseText)),
  1392. onerror: reject
  1393. });
  1394. });
  1395. }
  1396.  
  1397. function fetchDanbooruPosts(tag) {
  1398. const formattedTag = tag.trim().toLowerCase().replace(/\s+/g, '_');
  1399. return new Promise((resolve, reject) => {
  1400. GM.xmlHttpRequest({
  1401. method: 'GET',
  1402. url: `https://danbooru.donmai.us/posts.json?tags=${encodeURIComponent(formattedTag)}&limit=10`,
  1403. onload: response => resolve(JSON.parse(response.responseText)),
  1404. onerror: reject
  1405. });
  1406. });
  1407. }
  1408.  
  1409. function displayWikiContent(wikiData, tag) {
  1410. const hasWiki = wikiData && wikiData[0];
  1411. const hasPosts = currentPosts && currentPosts.length > 0;
  1412.  
  1413. wikiContent.innerHTML = `
  1414. <div class="wiki-text-content">
  1415. <h2>${tag}</h2>
  1416. <div class="wiki-description">
  1417. ${hasWiki ? `<p>${formatWikiText(wikiData[0].body)}</p>` :
  1418. `<p>No wiki information available for this tag${hasPosts ? ', but images are available.' : '.'}</p>`}
  1419. </div>
  1420. </div>
  1421. <div class="wiki-image-section">
  1422. ${hasPosts ? `
  1423. <div class="wiki-image-container">
  1424. <button class="image-nav-button prev" title="Previous image">←</button>
  1425. <img class="wiki-image" src="" alt="Tag example">
  1426. <button class="image-nav-button next" title="Next image">→</button>
  1427. </div>
  1428. <div class="wiki-nav-buttons">
  1429. <button class="wiki-button view-on-danbooru">View on Danbooru</button>
  1430. </div>
  1431. ` : `
  1432. <div class="no-images-message">No images available for this tag</div>
  1433. `}
  1434. </div>
  1435. `;
  1436.  
  1437. // Always attach wiki tag event listeners
  1438. attachWikiEventListeners();
  1439.  
  1440. // Only display images if we have posts
  1441. if (hasPosts) {
  1442. displayPostImage(currentPosts[0]);
  1443. }
  1444. }
  1445.  
  1446. function formatWikiText(text) {
  1447. // Remove backticks that sometimes wrap the content
  1448. text = text.replace(/^`|`$/g, '');
  1449.  
  1450. // First handle the complex patterns
  1451. text = text
  1452. // Handle list items with proper indentation
  1453. .replace(/^\* (.+)$/gm, '<li>$1</li>')
  1454.  
  1455.  
  1456. // Handle Danbooru internal paths (using absolute URLs)
  1457. .replace(/"([^"]+)":\s*\/((?:[\w-]+\/)*[\w-]+(?:\?[^"\s]+)?)/g, (match, text, path) => {
  1458. const fullUrl = `https://danbooru.donmai.us/${path.trim()}`;
  1459. return `<a class="wiki-link" href="${fullUrl}" target="_blank">${text}</a>`;
  1460. })
  1461.  
  1462. // Handle named links with square brackets
  1463. .replace(/"([^"]+)":\[([^\]]+)\]/g, '<a class="wiki-link" href="$2" target="_blank">$1</a>')
  1464.  
  1465. // Handle post references
  1466. .replace(/!post #(\d+)/g, '<a class="wiki-link" href="https://danbooru.donmai.us/posts/$1" target="_blank">post #$1</a>')
  1467.  
  1468. // Handle external links with proper URL capture (must come before wiki links)
  1469. .replace(/"([^"]+)":\s*(https?:\/\/[^\s"]+)/g, '<a class="wiki-link" href="$2" target="_blank">$1</a>')
  1470.  
  1471. // Handle wiki links with display text, preserving special characters
  1472. .replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, (match, tag, display) => {
  1473. const cleanTag = tag.trim();
  1474. return `<span class="wiki-tag" data-tag="${cleanTag}">${display}</span>`;
  1475. })
  1476.  
  1477. // Handle simple wiki links, preserving special characters
  1478. .replace(/\[\[([^\]]+)\]\]/g, (match, tag) => {
  1479. const cleanTag = tag.trim();
  1480. return `<span class="wiki-tag" data-tag="${cleanTag}">${cleanTag}</span>`;
  1481. })
  1482.  
  1483. // Handle BBCode
  1484. .replace(/\[b\](.*?)\[\/b\]/g, '<strong>$1</strong>')
  1485. .replace(/\[i\](.*?)\[\/i\]/g, '<em>$1</em>')
  1486. .replace(/\[code\](.*?)\[\/code\]/g, '<code>$1</code>')
  1487. .replace(/\[u\](.*?)\[\/u\]/g, '<u>$1</u>')
  1488.  
  1489. // Handle headers with proper spacing
  1490. .replace(/^h([1-6])\.\s*(.+)$/gm, (_, size, content) => `\n<h${size}>${content}</h${size}>\n`)
  1491.  
  1492. // Add spacing after tag name at start of line
  1493. // Handle line breaks and paragraphs
  1494. text = text
  1495. .replace(/\r\n/g, '\n') // Normalize line endings
  1496. .replace(/\n\n+/g, '</p><p>')
  1497. .replace(/\n/g, '<br>');
  1498.  
  1499. // Wrap lists in ul tags
  1500. text = text.replace(/(<li>.*?<\/li>)\s*(?=<li>|$)/gs, '<ul>$1</ul>');
  1501.  
  1502. // Wrap in paragraph if not already wrapped
  1503. if (!text.startsWith('<p>')) {
  1504. text = `<p>${text}</p>`;
  1505. }
  1506.  
  1507. return text;
  1508. }
  1509.  
  1510. // Separate the keyboard handler into its own function
  1511. function handleWikiKeydown(e) {
  1512. if (wikiOverlay.style.display === 'block') {
  1513. if (e.key === 'ArrowLeft') navigateImage(-1);
  1514. if (e.key === 'ArrowRight') navigateImage(1);
  1515. }
  1516. }
  1517.  
  1518. function attachWikiEventListeners() {
  1519. const prevButton = wikiContent.querySelector('.image-nav-button.prev');
  1520. const nextButton = wikiContent.querySelector('.image-nav-button.next');
  1521. const viewButton = wikiContent.querySelector('.view-on-danbooru');
  1522. const wikiImage = wikiContent.querySelector('.wiki-image');
  1523. const wikiTags = wikiContent.querySelectorAll('.wiki-tag');
  1524.  
  1525. // Only attach image navigation related listeners if we have posts
  1526. if (currentPosts.length > 0) {
  1527. if (prevButton) {
  1528. prevButton.addEventListener('click', () => navigateImage(-1));
  1529. }
  1530. if (nextButton) {
  1531. nextButton.addEventListener('click', () => navigateImage(1));
  1532. }
  1533.  
  1534. // Add keyboard navigation only if we have posts
  1535. document.removeEventListener('keydown', handleWikiKeydown);
  1536. document.addEventListener('keydown', handleWikiKeydown);
  1537.  
  1538. if (wikiImage) {
  1539. wikiImage.addEventListener('click', () => {
  1540. if (currentPosts[currentPostIndex]) {
  1541. window.open(currentPosts[currentPostIndex].large_file_url, '_blank');
  1542. }
  1543. });
  1544. }
  1545.  
  1546. if (viewButton) {
  1547. viewButton.addEventListener('click', () => {
  1548. if (currentPosts[currentPostIndex]) {
  1549. window.open(`https://danbooru.donmai.us/posts/${currentPosts[currentPostIndex].id}`, '_blank');
  1550. }
  1551. });
  1552. }
  1553. }
  1554.  
  1555. // Wiki tag navigation works regardless of posts
  1556. if (wikiTags) {
  1557. wikiTags.forEach(tag => {
  1558. tag.addEventListener('click', () => {
  1559. const tagName = tag.dataset.tag;
  1560. loadWikiInfo(tagName);
  1561. });
  1562. });
  1563. }
  1564. }
  1565.  
  1566. function displayPostImage(post) {
  1567. const imageContainer = wikiContent.querySelector('.wiki-image-container');
  1568. if (!imageContainer) return; // Guard against missing container
  1569.  
  1570. if (!post || (!post.preview_file_url && !post.file_url)) return;
  1571.  
  1572. const prevButton = imageContainer.querySelector('.image-nav-button.prev');
  1573. const nextButton = imageContainer.querySelector('.image-nav-button.next');
  1574. const image = imageContainer.querySelector('.wiki-image');
  1575.  
  1576. if (!image) return; // Guard against missing image element
  1577.  
  1578. image.src = post.large_file_url || post.preview_file_url || post.file_url;
  1579.  
  1580. if (prevButton) prevButton.style.visibility = currentPostIndex <= 0 ? 'hidden' : 'visible';
  1581. if (nextButton) nextButton.style.visibility = currentPostIndex >= currentPosts.length - 1 ? 'hidden' : 'visible';
  1582.  
  1583. // Remove any existing event listeners first to prevent duplicates
  1584. const newPrevButton = prevButton.cloneNode(true);
  1585. const newNextButton = nextButton.cloneNode(true);
  1586.  
  1587. prevButton.parentNode.replaceChild(newPrevButton, prevButton);
  1588. nextButton.parentNode.replaceChild(newNextButton, nextButton);
  1589.  
  1590. // Attach fresh event listeners
  1591. newPrevButton.addEventListener('click', (e) => {
  1592. e.stopPropagation();
  1593. navigateImage(-1);
  1594. });
  1595.  
  1596. newNextButton.addEventListener('click', (e) => {
  1597. e.stopPropagation();
  1598. navigateImage(1);
  1599. });
  1600.  
  1601. // Reattach image click listener
  1602. image.addEventListener('click', () => {
  1603. window.open(post.large_file_url || post.file_url, '_blank');
  1604. });
  1605. }
  1606.  
  1607. function navigateImage(direction) {
  1608. const newIndex = currentPostIndex + direction;
  1609. if (newIndex >= 0 && newIndex < currentPosts.length) {
  1610. currentPostIndex = newIndex;
  1611. displayPostImage(currentPosts[newIndex]);
  1612. }
  1613. }
  1614.  
  1615. // Add new function for wiki search autocomplete
  1616. function setupWikiSearchAutocomplete(searchBar) {
  1617. const suggestionsBox = document.createElement('div');
  1618. suggestionsBox.className = 'wiki-search-suggestions';
  1619. suggestionsBox.style.display = 'none';
  1620. document.body.appendChild(suggestionsBox); // Append to body instead
  1621.  
  1622. let selectedIndex = -1;
  1623.  
  1624. // Update suggestions box position when showing
  1625. function updateSuggestionsPosition() {
  1626. const searchBarRect = searchBar.getBoundingClientRect();
  1627. suggestionsBox.style.top = `${searchBarRect.bottom + window.scrollY}px`;
  1628. }
  1629.  
  1630. searchBar.addEventListener('input', () => {
  1631. const term = searchBar.value.replace(/\s+/g, '_').trim();
  1632. if (term) {
  1633. fetchSuggestionsForWiki(term, suggestionsBox);
  1634. updateSuggestionsPosition();
  1635. } else {
  1636. suggestionsBox.style.display = 'none';
  1637. }
  1638. });
  1639.  
  1640. // Update position on scroll or resize
  1641. window.addEventListener('scroll', () => {
  1642. if (suggestionsBox.style.display === 'block') {
  1643. updateSuggestionsPosition();
  1644. }
  1645. });
  1646.  
  1647. window.addEventListener('resize', () => {
  1648. if (suggestionsBox.style.display === 'block') {
  1649. updateSuggestionsPosition();
  1650. }
  1651. });
  1652.  
  1653. searchBar.addEventListener('keydown', (e) => {
  1654. const suggestions = suggestionsBox.children;
  1655. if (suggestions.length === 0) return;
  1656.  
  1657. if (e.key === 'ArrowDown') {
  1658. e.preventDefault();
  1659. selectedIndex = Math.min(selectedIndex + 1, suggestions.length - 1);
  1660. updateWikiSuggestionSelection(suggestions, selectedIndex);
  1661. } else if (e.key === 'ArrowUp') {
  1662. e.preventDefault();
  1663. selectedIndex = Math.max(selectedIndex - 1, -1);
  1664. updateWikiSuggestionSelection(suggestions, selectedIndex);
  1665. } else if (e.key === 'Enter' && selectedIndex !== -1) {
  1666. e.preventDefault();
  1667. searchBar.value = suggestions[selectedIndex].textContent;
  1668. suggestionsBox.style.display = 'none';
  1669. loadWikiInfo(searchBar.value);
  1670. }
  1671. });
  1672.  
  1673. // Close suggestions when clicking outside
  1674. document.addEventListener('click', (e) => {
  1675. if (!searchBar.contains(e.target) && !suggestionsBox.contains(e.target)) {
  1676. suggestionsBox.style.display = 'none';
  1677. }
  1678. });
  1679. }
  1680.  
  1681. function fetchSuggestionsForWiki(term, suggestionsBox) {
  1682. clearTimeout(debounceTimer);
  1683. debounceTimer = setTimeout(() => {
  1684. GM.xmlHttpRequest({
  1685. method: 'GET',
  1686. url: `https://gelbooru.com/index.php?page=autocomplete2&term=${encodeURIComponent(term)}&type=tag_query&limit=10`,
  1687. onload: function(response) {
  1688. if (response.status === 200) {
  1689. try {
  1690. const data = JSON.parse(response.responseText);
  1691. showWikiSuggestions(data, suggestionsBox);
  1692. } catch (e) {
  1693. console.error("Error parsing suggestions:", e);
  1694. }
  1695. }
  1696. }
  1697. });
  1698. }, debounceDelay);
  1699. }
  1700.  
  1701. function showWikiSuggestions(suggestions, suggestionsBox) {
  1702. suggestionsBox.innerHTML = '';
  1703. if (suggestions.length === 0) {
  1704. suggestionsBox.style.display = 'none';
  1705. return;
  1706. }
  1707.  
  1708. suggestions.forEach(suggestion => {
  1709. const div = document.createElement('div');
  1710. div.className = 'wiki-search-suggestion';
  1711. div.textContent = suggestion.label;
  1712. div.addEventListener('click', () => {
  1713. const searchBar = suggestionsBox.parentNode.querySelector('.wiki-search-bar');
  1714. searchBar.value = suggestion.label;
  1715. suggestionsBox.style.display = 'none';
  1716. loadWikiInfo(suggestion.label);
  1717. });
  1718. suggestionsBox.appendChild(div);
  1719. });
  1720.  
  1721. suggestionsBox.style.display = 'block';
  1722. }
  1723.  
  1724. function updateWikiSuggestionSelection(suggestions, selectedIndex) {
  1725. Array.from(suggestions).forEach((suggestion, index) => {
  1726. suggestion.classList.toggle('selected', index === selectedIndex);
  1727. if (index === selectedIndex) {
  1728. suggestion.scrollIntoView({ block: 'nearest' });
  1729. }
  1730. });
  1731. }
  1732.  
  1733. // Ensure script runs as soon as DOM is ready
  1734. document.addEventListener('DOMContentLoaded', () => {
  1735. loadSettings();
  1736. setupAutocomplete();
  1737. initializeWiki();
  1738. });
  1739.  
  1740. })();