Civitai Prompt Autocomplete & Tag Wiki

Adds tag autocomplete and wiki lookup features

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