您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds search to Torn Chats.
// ==UserScript== // @name Torn Chat 3.0 Integrated Search Feature // @namespace https://greasyfork.org/en/users/1431907-theeeunknown // @version 1.2 // @description Adds search to Torn Chats. // @author TR0LL [2561502] // @license CC BY-SA 4.0 // @match *://*.torn.com/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_info // @run-at document-start // ==/UserScript== (function () { 'use strict'; const SCRIPT_ID = 'chat-3-0-search'; const PANEL_ID = `${SCRIPT_ID}-panel`; const FOOTER_BUTTON_ID = `${SCRIPT_ID}-footer-button`; const FACTION_ID_STORAGE_KEY = 'chatHelper_factionId_v2'; const config = { panelVisible: false, panelPosition: { top: '20px', right: '20px', left: 'auto' }, targetChatIdRegex: /^(public_global$|public_trade$|public_company$|public_poker$|public_staff$|private-\d+-\d+|company-\d+|poker-|staff-)$/, chatWindowSelector: `#chatRoot [class*="root___"][class*="visible___"]`, chatListSelector: `div[class*="list___"]`, messageGroupSelector: `div[class*="root___r_1Ra"]`, messageTextSelector: `span[class*="message___pRfWR"] span[class*="root___Xw4jI"]`, senderSelector: `a[class*="sender___"]`, timestampSelector: `[class*="messageGroupTimestamp___"]`, chatFooterBarSelector: 'div[class*="root___oWxEV"]', inputAreaContainerSelector: 'div[class*="root___WUd1h"]', chatHeaderClass: '[class*="root___"][class*="header___"]', chatWindowTitleClass: '[class*="title___"]', RETRY_INTERVAL_MS: 3000, FOOTER_BUTTON_CHECK_INTERVAL_MS: 3000, WAIT_FOR_ELEMENT_TIMEOUT_MS: 5000, INITIAL_SCAN_DELAY_MS: 1500, }; let enhancedPanel = null; let dragOffsetX, dragOffsetY; let activeChatWindows = {}; let currentSearchResults = []; let currentSearchDetails = { query: '', chatId: '', chatName: '' }; let forceSearchContainer = {}; let storedFactionId = null; function saveSettings() { try { const settingsToSave = { panelPosition: (enhancedPanel && enhancedPanel.style.left !== 'auto' && enhancedPanel.style.top) ? { top: enhancedPanel.style.top, left: enhancedPanel.style.left } : config.panelPosition }; GM_setValue('chatEnhancerSettings_v3_pos_only', JSON.stringify(settingsToSave)); } catch (error) { console.error("EnhancedChat: Error saving settings:", error); } } function loadSettings() { try { const savedSettings = GM_getValue('chatEnhancerSettings_v3_pos_only'); if (!savedSettings) return; const loadedSettings = JSON.parse(savedSettings); if (!loadedSettings || typeof loadedSettings !== 'object') return; if (loadedSettings.panelPosition) { if (typeof loadedSettings.panelPosition.top === 'string' && loadedSettings.panelPosition.top !== 'auto' && typeof loadedSettings.panelPosition.left === 'string' && loadedSettings.panelPosition.left !== 'auto') { config.panelPosition = { top: loadedSettings.panelPosition.top, left: loadedSettings.panelPosition.left, right: 'auto', bottom: 'auto' }; } else { config.panelPosition = { top: loadedSettings.panelPosition.top || 'auto', bottom: loadedSettings.panelPosition.bottom || '60px', left: loadedSettings.panelPosition.left || '20px', right: loadedSettings.panelPosition.right || 'auto' }; if(config.panelPosition.top !== 'auto') config.panelPosition.bottom = 'auto'; if(config.panelPosition.left !== 'auto') config.panelPosition.right = 'auto'; } } } catch (error) { console.error("EnhancedChat: Error loading settings:", error); config.panelPosition = { top: 'auto', bottom: '60px', left: '20px', right: 'auto' }; } } function parseTimestampText(text) { if (!text) return null; const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); text = text.trim(); const timeParts = (match) => ({ hours: parseInt(match[1], 10), minutes: parseInt(match[2], 10), seconds: match[3] ? parseInt(match[3], 10) : 0 }); const dateParts = (match) => ({ day: parseInt(match[1], 10), month: parseInt(match[2], 10) - 1, year: 2000 + parseInt(match[3], 10) }); if (text.startsWith('Today')) { const timeMatch = text.match(/Today (\d{1,2}):(\d{2})(?::(\d{2}))?/); if (timeMatch) { const { hours, minutes, seconds } = timeParts(timeMatch); const date = new Date(today); date.setHours(hours, minutes, seconds); return date; } return today; } if (text.startsWith('Yesterday')) { const timeMatch = text.match(/Yesterday (\d{1,2}):(\d{2})(?::(\d{2}))?/); if (timeMatch) { const { hours, minutes, seconds } = timeParts(timeMatch); const date = new Date(yesterday); date.setHours(hours, minutes, seconds); return date; } return yesterday; } const dateTimeMatch = text.match(/^(\d{2}):(\d{2}):(\d{2})\s*-\s*(\d{1,2})\/(\d{1,2})\/(\d{2})$/); if (dateTimeMatch) { try { const { hours, minutes, seconds } = timeParts(dateTimeMatch); const { day, month, year } = dateParts(dateTimeMatch.slice(4)); const date = new Date(year, month, day, hours, minutes, seconds); if (!isNaN(date) && date.getMonth() === month) return date; } catch (e) {} } const simpleDateMatch = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2})$/); if (simpleDateMatch) { try { const { day, month, year } = dateParts(simpleDateMatch); const date = new Date(year, month, day); if (!isNaN(date) && date.getMonth() === month) return date; } catch (e) {} } return null; } function escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } const searchIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>`; function applyStyles() { GM_addStyle(` #${PANEL_ID} {position:fixed; z-index:1001; width:350px; height:300px; background-color: var(--chat-color-background, #f2f2f2); border:1px solid var(--chat-color-border-primary, #ababab); border-radius:5px; box-shadow:0 3px 8px rgba(0,0,0,0.3); display:flex; flex-direction:column; color: var(--default-color, #333); font-size:12px; overflow:hidden; opacity:1; visibility:visible; transform:scale(1); transition:opacity 0.25s ease-in-out, transform 0.25s ease-in-out, visibility 0s linear 0s;} #${PANEL_ID}.hidden {opacity:0; transform:scale(0.98); visibility:hidden; transition:opacity 0.25s ease-in-out, transform 0.25s ease-in-out, visibility 0s linear 0.25s; pointer-events:none;} #${SCRIPT_ID}-panel-header {padding:5px 8px; border-bottom:1px solid var(--chat-color-border-secondary, #ccc); background:var(--title-gray-gradient, linear-gradient(180deg, #888888 0%, #444444 100%)); display:flex; justify-content:space-between; align-items:center; font-size:12px; font-weight:bold; color:var(--title-color, #FFF); border-top-left-radius:4px; border-top-right-radius:4px; text-shadow:var(--title-text-shadow, 0 1px 0 #000); cursor:grab; flex-shrink:0;} #${SCRIPT_ID}-panel-header:active {cursor:grabbing;} #${SCRIPT_ID}-panel-title {white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-right:5px;} #${SCRIPT_ID}-panel-close {cursor:pointer; font-weight:bold; background:none; border:none; color:#fff; font-size:16px; line-height:1; opacity:0.8; text-shadow:0 1px 0 #000; padding:0 2px;} #${SCRIPT_ID}-panel-close:hover {opacity:1;} #${SCRIPT_ID}-tabs {display:flex; flex-shrink:0; border-bottom:1px solid var(--chat-color-border-secondary, #ccc); background-color: var(--torn-tabs-active-background, #fff);} .enh-chat-tab {padding:6px 12px; cursor:default; border:none; font-size:11px; font-weight:bold; color: var(--torn-tabs-active-color, #333); flex-grow:1; text-align:center;} #${SCRIPT_ID}-content-container {flex-grow:1; display:flex; flex-direction:column; overflow:hidden;} .enh-chat-tab-content {display:flex; flex-direction:column; height:100%; overflow:hidden; flex-grow:1; padding:10px; background-color:#fff; color:#333;} #${SCRIPT_ID}-search-controls {display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; flex-shrink:0; border-bottom:1px solid var(--chat-color-border-secondary, #ccc); padding-bottom:5px;} #${SCRIPT_ID}-search-results-title {font-size:12px; font-weight:bold; color: var(--default-gray-6-color); margin:0; padding:0;} #${SCRIPT_ID}-export-button {cursor:pointer; background:var(--btn-background, linear-gradient(180deg,#DEDEDE 0%,#F7F7F7 25%,#CFCFCF 60%,#E7E7E7 78%,#D9D9D9 100%)); border:var(--btn-border, 1px solid #aaa); color:var(--btn-color, #555); border-radius:3px; padding:3px 8px; font-size:10px; text-shadow:var(--btn-text-shadow, 0 1px 0 #FFFFFF40); font-weight:normal; margin-left:5px;} #${SCRIPT_ID}-export-button:hover {background:var(--btn-hover-background, linear-gradient(180deg,#CCCCCC 0%,#FFFFFF 25%,#BBBBBB 60%,#EEEEEE 78%,#CCCCCC 100%));} #${SCRIPT_ID}-search-results {border:1px solid var(--chat-color-border-secondary, #ccc); background-color: #fff; border-radius:3px; overflow-y:auto; flex-grow:1;} .${SCRIPT_ID}-search-result-item {padding:5px 8px; border-bottom:1px solid #eee; font-size:11px; cursor:default; line-height:1.4; color: #333;} .${SCRIPT_ID}-search-result-item:hover {background-color: #f0f0f0;} .${SCRIPT_ID}-search-result-item:last-child {border-bottom:none;} .${SCRIPT_ID}-search-result-item .cloned-sender-link {font-weight:bold; margin-right:3px; color: #369;} .${SCRIPT_ID}-search-result-item .cloned-sender-link:hover {text-decoration:underline;} .${SCRIPT_ID}-search-result-highlight {background-color: #FFFF80; border-radius:2px; padding:0 1px; font-weight:bold; color: #000;} .${SCRIPT_ID}-search-no-results {padding:10px; text-align:center; font-style:italic; color: #666; font-size:11px;} .chat-search-container {padding: 3px 6px; border-top:1px solid var(--chat-color-border-secondary, #ccc); border-bottom:1px solid var(--chat-color-border-secondary, #ccc); display:flex; flex-wrap: nowrap; align-items:center; gap: 3px; background-color: var(--chat-color-background, #f0f0f0); margin-top:-1px;} .chat-search-container input[type=text] {flex-grow:1; height: 22px; line-height:20px; font-size:11px; border:1px solid var(--input-border-color, #ccc); color: var(--input-color, #000); background: var(--input-background-color, linear-gradient(0deg, #fff 0%, #fff 100%)); padding: 1px 5px; border-radius:3px; min-width:60px;} .chat-search-container input[type=text]:focus {border-color:var(--input-focus-border-color, #1864AB80); outline:none;} .chat-search-container button { flex-shrink:0; cursor:pointer; background:var(--btn-background, linear-gradient(180deg,#DEDEDE 0%,#F7F7F7 25%,#CFCFCF 60%,#E7E7E7 78%,#D9D9D9 100%)); border:var(--btn-border, 1px solid #aaa); color:var(--btn-color, #555); border-radius:3px; padding: 1px 3px; height: 22px; line-height:1; display:inline-flex; align-items:center; justify-content:center;} .chat-search-container button svg { width: 12px; height: 12px; } .chat-search-container button:hover {background:var(--btn-hover-background, linear-gradient(180deg,#CCCCCC 0%,#FFFFFF 25%,#BBBBBB 60%,#EEEEEE 78%,#BBBBBB 100%)); border:var(--btn-hover-border, 1px solid #999); color:var(--btn-hover-color, #444);} .chat-search-container .msg-count-display {font-size:9px; color: var(--default-gray-6-color, #666); margin-left:auto; padding-left:5px; flex-shrink:0; white-space:nowrap;} .highlighted-search-result {background-color: #FFFACD !important; border:1px solid #FFD700 !important; box-shadow:0 0 5px #FFD700 !important; border-radius:3px; margin:1px 0; transition:background-color .3s ease;} `); } function createPanelHeader() { const header = document.createElement('div'); header.id = `${SCRIPT_ID}-panel-header`; const title = document.createElement('span'); title.id = `${SCRIPT_ID}-panel-title`; title.textContent = 'Chat Search Results'; const closeButton = document.createElement('button'); closeButton.id = `${SCRIPT_ID}-panel-close`; closeButton.innerHTML = '×'; closeButton.title = 'Close Panel'; closeButton.addEventListener('click', () => togglePanelVisibility(false)); header.appendChild(title); header.appendChild(closeButton); return header; } function createSearchTabContent(container) { const controlsDiv = document.createElement('div'); controlsDiv.id = `${SCRIPT_ID}-search-controls`; controlsDiv.innerHTML = `<h3 id="${SCRIPT_ID}-search-results-title">Search Results</h3><button id="${SCRIPT_ID}-export-button" title="Export current results to a text file">Export Results</button>`; controlsDiv.querySelector(`#${SCRIPT_ID}-export-button`).addEventListener('click', exportSearchResults); const resultsDiv = document.createElement('div'); resultsDiv.id = `${SCRIPT_ID}-search-results`; resultsDiv.innerHTML = `<div class="${SCRIPT_ID}-search-no-results">Perform a search in a chat window.</div>`; container.appendChild(controlsDiv); container.appendChild(resultsDiv); } function createPanelContentContainer() { const container = document.createElement('div'); container.id = `${SCRIPT_ID}-content-container`; const tabsContainer = document.createElement('div'); tabsContainer.id = `${SCRIPT_ID}-tabs`; const searchTabBtn = document.createElement('div'); searchTabBtn.className = 'enh-chat-tab'; searchTabBtn.textContent = 'Search Results'; tabsContainer.appendChild(searchTabBtn); const tabContentArea = document.createElement('div'); tabContentArea.style.flexGrow = '1'; tabContentArea.style.overflow = 'hidden'; tabContentArea.style.display = 'flex'; const searchContent = document.createElement('div'); searchContent.id = `${SCRIPT_ID}-search-content`; searchContent.className = 'enh-chat-tab-content active'; createSearchTabContent(searchContent); tabContentArea.appendChild(searchContent); container.appendChild(tabsContainer); container.appendChild(tabContentArea); return container; } function createEnhancedChatPanel() { if (enhancedPanel) return enhancedPanel; enhancedPanel = document.createElement('div'); enhancedPanel.id = PANEL_ID; enhancedPanel.classList.add('hidden'); const header = createPanelHeader(); const contentContainer = createPanelContentContainer(); enhancedPanel.append(header, contentContainer); document.body.appendChild(enhancedPanel); header.addEventListener('mousedown', dragMouseDown); applyPanelPosition(); return enhancedPanel; } function applyPanelPosition() { if (!enhancedPanel) return; Object.assign(enhancedPanel.style, { top: '', left: '', right: '', bottom: '' }); enhancedPanel.style.top = config.panelPosition.top || 'auto'; enhancedPanel.style.left = config.panelPosition.left || 'auto'; enhancedPanel.style.right = config.panelPosition.right || 'auto'; enhancedPanel.style.bottom = config.panelPosition.bottom || 'auto'; } function displaySearchResultsInPanel(targetChatId, query, results) { console.log(`EnhancedChat: Displaying ${results.length} results for "${query}" in ${targetChatId}`); if (!enhancedPanel) { createEnhancedChatPanel(); if(!enhancedPanel) { console.error("EnhancedChat: Failed to create panel for displaying results."); return; } } const resultsDiv = document.getElementById(`${SCRIPT_ID}-search-results`); const resultsTitle = document.getElementById(`${SCRIPT_ID}-search-results-title`); if (!resultsDiv || !resultsTitle) { console.error("EnhancedChat: Panel internal elements missing for search results display."); return; } if (enhancedPanel.classList.contains('hidden')) { togglePanelVisibility(true); } const chatName = activeChatWindows[targetChatId]?.name || targetChatId.replace(/^(public_|faction-|private-\d+-|company-|poker-|staff-)/, ''); const safeQuery = escapeRegex(query); const regex = new RegExp(safeQuery, 'gi'); resultsDiv.innerHTML = ''; resultsTitle.textContent = `Results for "${query}" in ${chatName} (${results.length})`; currentSearchResults = results; currentSearchDetails = { query: query, chatId: targetChatId, chatName: chatName }; if (results.length > 0) { const fragment = document.createDocumentFragment(); results.forEach(result => { const resultItem = document.createElement('div'); resultItem.className = `${SCRIPT_ID}-search-result-item`; const senderContainer = document.createElement('span'); const originalSenderLink = result.senderElement; let senderHtml = ''; if (originalSenderLink && originalSenderLink.tagName === 'A') { const clonedLink = originalSenderLink.cloneNode(true); clonedLink.className = 'cloned-sender-link'; senderHtml = clonedLink.outerHTML; } else { senderHtml = `<span class="${result.senderElement?.className||''}">${result.sender}</span>`; } if (result.sender.toLowerCase().includes(query.toLowerCase())) { senderHtml = senderHtml.replace(regex, match => `<span class="${SCRIPT_ID}-search-result-highlight">${match}</span>`); } senderContainer.innerHTML = senderHtml + ': '; const textSpan = document.createElement('span'); textSpan.className = 'text'; const highlightedText = result.text.replace(regex, match => `<span class="${SCRIPT_ID}-search-result-highlight">${match}</span>`); textSpan.innerHTML = highlightedText; resultItem.appendChild(senderContainer); resultItem.appendChild(textSpan); fragment.appendChild(resultItem); }); resultsDiv.appendChild(fragment); } else { resultsDiv.innerHTML = `<div class="${SCRIPT_ID}-search-no-results">No results found for "${query}" in ${chatName}.</div>`; } } function exportSearchResults() { if (!currentSearchResults || currentSearchResults.length === 0) return; const { query, chatId, chatName } = currentSearchDetails; let exportText = `Torn Chat Search Results\nChat: ${chatName} (${chatId})\nQuery: "${query}"\nGenerated: ${new Date().toLocaleString()}\nResults Found: ${currentSearchResults.length}\n\n----------------------------------------\n\n`; currentSearchResults.forEach(result => { exportText += `${result.sender}: ${result.text}\n`; }); const blob = new Blob([exportText], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const safeChatName = (chatName || 'UnknownChat').replace(/[^a-z0-9_-]/gi, '_'); const safeQuery = (query || 'no_query').replace(/[^a-z0-9_-]/gi, '_').substring(0, 20); a.download = `torn_search_${safeChatName}_${safeQuery}_${Date.now()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function togglePanelVisibility(forceShow = null) { if (!enhancedPanel) { createEnhancedChatPanel(); if (!enhancedPanel) return; } let makeVisible; if (forceShow === true) makeVisible = true; else if (forceShow === false) makeVisible = false; else makeVisible = enhancedPanel.classList.contains('hidden'); enhancedPanel.classList.toggle('hidden', !makeVisible); } function dragMouseDown(e) { if (e.target.closest('button')) return; e.preventDefault(); dragOffsetX = e.clientX - enhancedPanel.offsetLeft; dragOffsetY = e.clientY - enhancedPanel.offsetTop; document.addEventListener('mousemove', elementDrag); document.addEventListener('mouseup', closeDragElement, { once: true }); } function elementDrag(e) { e.preventDefault(); let newLeft = e.clientX - dragOffsetX; let newTop = e.clientY - dragOffsetY; newTop = Math.max(0, Math.min(newTop, window.innerHeight - enhancedPanel.offsetHeight)); newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - enhancedPanel.offsetWidth)); enhancedPanel.style.top = `${newTop}px`; enhancedPanel.style.left = `${newLeft}px`; enhancedPanel.style.right = "auto"; enhancedPanel.style.bottom = "auto"; } function closeDragElement() { document.removeEventListener('mousemove', elementDrag); config.panelPosition = { top: enhancedPanel.style.top, left: enhancedPanel.style.left, right: 'auto', bottom: 'auto' }; saveSettings(); } function enhanceChatWindow(chatWindow) { if (!chatWindow || chatWindow.dataset.enhancedChatProcessed === 'true') return; const chatId = chatWindow.id; const shouldEnhance = (storedFactionId && chatId === storedFactionId) || config.targetChatIdRegex.test(chatId); if (!chatId || !shouldEnhance) return; const inputAreaContainer = chatWindow.querySelector(config.inputAreaContainerSelector); if (!inputAreaContainer || !inputAreaContainer.parentNode) { forceSearchContainer[chatId] = true; return; } const insertionPoint = inputAreaContainer.parentNode; if (!chatWindow.querySelector(`.${SCRIPT_ID}-search-container`)) { const searchContainer = document.createElement('div'); searchContainer.className = `chat-search-container ${SCRIPT_ID}-search-container`; const searchInput = document.createElement('input'); searchInput.type = 'text'; const titleElement = chatWindow.querySelector(`${config.chatHeaderClass} ${config.chatWindowTitleClass}`); const chatTitle = titleElement?.textContent || chatId?.replace(/^public_|^faction-|^private-.*?(-|$)|^company-|^poker-|^staff-/, '') || 'chat'; searchInput.placeholder = `Search ${chatTitle}...`; const searchButton = document.createElement('button'); searchButton.innerHTML = searchIconSVG; searchButton.title = 'Search'; const msgCountDisplay = document.createElement('span'); msgCountDisplay.className = 'msg-count-display'; msgCountDisplay.textContent = '(...)'; searchContainer.appendChild(searchInput); searchContainer.appendChild(searchButton); searchContainer.appendChild(msgCountDisplay); insertionPoint.parentNode.insertBefore(searchContainer, insertionPoint); const handleSearch = () => { console.log(`EnhancedChat: handleSearch called for ${chatId}`); const query = searchInput.value; if(!query) return; const results = performIndividualChatSearch(chatId, query); displaySearchResultsInPanel(chatId, query, results); }; searchButton.addEventListener('click', handleSearch); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleSearch(); } }); updateMessageCount(chatWindow); } const titleElement = chatWindow.querySelector(`${config.chatHeaderClass} ${config.chatWindowTitleClass}`); activeChatWindows[chatId] = { name: titleElement?.textContent || chatId.replace(/^public_|^faction-|^private-.*?(-|$)|^company-|^poker-|^staff-/, ''), element: chatWindow }; chatWindow.dataset.enhancedChatProcessed = 'true'; if (forceSearchContainer[chatId]) { delete forceSearchContainer[chatId]; } } function performIndividualChatSearch(targetChatId, query) { query = query.trim().toLowerCase(); if (!query) return []; const chatWindowElement = document.getElementById(targetChatId); if (!chatWindowElement) return []; const chatList = chatWindowElement.querySelector(config.chatListSelector); if (!chatList) return []; const results = []; const messageGroups = chatList.querySelectorAll(config.messageGroupSelector); console.log(`EnhancedChat: Found ${messageGroups.length} message groups in ${targetChatId}`); messageGroups.forEach((msgGroup) => { const senderElement = msgGroup.querySelector(config.senderSelector); const messageElement = msgGroup.querySelector(config.messageTextSelector); if (senderElement && messageElement) { const senderText = senderElement.textContent?.replace(':', '').trim() || ''; const messageText = messageElement.textContent?.trim() || ''; if (messageText.toLowerCase().includes(query) || senderText.toLowerCase().includes(query)) { results.push({ element: msgGroup, sender: senderText, senderElement: senderElement, text: messageText }); } } }); console.log(`EnhancedChat: Found ${results.length} matches for "${query}" in ${targetChatId}`); return results; } function updateMessageCount(chatWindow) { const chatList = chatWindow.querySelector(config.chatListSelector); const messageCountSpan = chatWindow.querySelector(`.${SCRIPT_ID}-search-container .msg-count-display`); if (chatList && messageCountSpan) { const messageCount = chatList.querySelectorAll(config.messageGroupSelector).length; messageCountSpan.textContent = `(${messageCount} msgs)`; } } function cleanupChatWindow(chatWindow) { if (chatWindow?.id && activeChatWindows[chatWindow.id]) { delete activeChatWindows[chatWindow.id]; } if (chatWindow?.id && forceSearchContainer[chatWindow.id]) { delete forceSearchContainer[chatWindow.id]; } } async function waitForElement(selector, parent = document, timeout = config.WAIT_FOR_ELEMENT_TIMEOUT_MS) { return new Promise((resolve, reject) => { const existingElement = parent.querySelector(selector); if (existingElement) { resolve(existingElement); return; } let observer = null; let timer = null; const cleanup = () => { if (observer) observer.disconnect(); clearTimeout(timer); }; observer = new MutationObserver(() => { const element = parent.querySelector(selector); if (element) { cleanup(); resolve(element); } }); try { observer.observe(parent, { childList: true, subtree: true }); } catch (e) { cleanup(); reject(new Error(`Observer failed for ${selector} in ${parent.id || parent.tagName}: ${e.message}`)); return; } timer = setTimeout(() => { cleanup(); reject(new Error(`Element ${selector} not found within timeout ${timeout}ms in parent ${parent.id || parent.tagName}`)); }, timeout); }); } async function enhanceChatWindowWhenReady(chatWindow) { if (!chatWindow || !chatWindow.id || chatWindow.dataset.enhancedChatProcessed === 'true') return; const timeoutMs = config.WAIT_FOR_ELEMENT_TIMEOUT_MS; try { await Promise.all([ waitForElement(config.inputAreaContainerSelector, chatWindow, timeoutMs), waitForElement(config.chatListSelector, chatWindow, timeoutMs) ]); enhanceChatWindow(chatWindow); } catch (error) { console.warn(`${GM_info.script.name}: Initial check failed for chat ${chatWindow.id}. Flagging for retry. Error:`, error.message); forceSearchContainer[chatWindow.id] = true; } } function setupObservers() { const bodyObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const chatWindows = node.matches(config.chatWindowSelector) ? [node] : node.querySelectorAll(config.chatWindowSelector); chatWindows.forEach(chatWindow => { if (chatWindow.id && !chatWindow.dataset.enhancedChatProcessed && (chatWindow.id === storedFactionId || config.targetChatIdRegex.test(chatWindow.id))) { enhanceChatWindowWhenReady(chatWindow); } }); } }); mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const chatWindows = node.matches(config.chatWindowSelector) ? [node] : node.querySelectorAll(config.chatWindowSelector); chatWindows.forEach(cleanupChatWindow); } }); }); }); bodyObserver.observe(document.body, { childList: true, subtree: true }); const footerBar = document.querySelector(config.chatFooterBarSelector); if (footerBar) { const buttonObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'class' && mutation.target.nodeName === 'BUTTON' && mutation.target.id.startsWith('channel_panel_button:')) { const button = mutation.target; const chatId = button.id.split(':')[1]; if (chatId && (chatId === storedFactionId || config.targetChatIdRegex.test(chatId))) { const chatWindow = document.getElementById(chatId); if (button.classList.contains('opened___Mwpgz') && chatWindow && !chatWindow.dataset.enhancedChatProcessed) { enhanceChatWindowWhenReady(chatWindow); } } } }); }); buttonObserver.observe(footerBar, { attributes: true, subtree: true, attributeFilter: ['class'] }); } else { console.warn("EnhancedChat: Footer bar not found, cannot observe button states."); } setupMessageObserver(); setInterval(() => { for (const chatId in forceSearchContainer) { if (forceSearchContainer[chatId] === true) { const chatWindow = document.getElementById(chatId); if (chatWindow && !chatWindow.dataset.enhancedChatProcessed && (chatId === storedFactionId || config.targetChatIdRegex.test(chatId))) { enhanceChatWindow(chatWindow); } else if (!chatWindow || chatWindow.dataset.enhancedChatProcessed) { delete forceSearchContainer[chatId]; } } } }, config.RETRY_INTERVAL_MS); setTimeout(() => { document.querySelectorAll(config.chatWindowSelector).forEach(chatWindow => { if (chatWindow.id && !chatWindow.dataset.enhancedChatProcessed && (chatWindow.id === storedFactionId || config.targetChatIdRegex.test(chatWindow.id))) { const button = document.getElementById(`channel_panel_button:${chatWindow.id}`); if (button && button.classList.contains('opened___Mwpgz')) { enhanceChatWindowWhenReady(chatWindow); } } }); }, config.INITIAL_SCAN_DELAY_MS); } function setupMessageObserver() { const observerCallback = (mutationsList) => { const updatedWindows = new Set(); for (const mutation of mutationsList) { if (mutation.type === 'childList') { const chatWindow = mutation.target.closest(config.chatWindowSelector); if (chatWindow && chatWindow.dataset.enhancedChatProcessed === 'true') { updatedWindows.add(chatWindow); } } } updatedWindows.forEach(win => updateMessageCount(win)); }; const listObserver = new MutationObserver(observerCallback); const bodyObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const chatLists = node.matches(config.chatListSelector) ? [node] : node.querySelectorAll(config.chatListSelector); chatLists.forEach(list => { if (!list.dataset.messageObserverAttached) { listObserver.observe(list, { childList: true }); list.dataset.messageObserverAttached = 'true'; const chatWindow = list.closest(config.chatWindowSelector); if(chatWindow) updateMessageCount(chatWindow); } }); } }); mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const chatLists = node.matches(config.chatListSelector) ? [node] : node.querySelectorAll(config.chatListSelector); chatLists.forEach(list => { if(list.dataset.messageObserverAttached) { delete list.dataset.messageObserverAttached; } }); } }); }); }); bodyObserver.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll(config.chatListSelector).forEach(list => { if (!list.dataset.messageObserverAttached) { listObserver.observe(list, { childList: true }); list.dataset.messageObserverAttached = 'true'; const chatWindow = list.closest(config.chatWindowSelector); if(chatWindow) updateMessageCount(chatWindow); } }); } function createFooterButton() { if (document.getElementById(FOOTER_BUTTON_ID)) return null; const buttonContainer = document.createElement('div'); buttonContainer.className = 'root___cYD0i'; const button = document.createElement('button'); button.type = 'button'; button.className = 'root___WHFbh root___K2Yex root___RLOBS'; button.id = FOOTER_BUTTON_ID; button.title = 'Toggle Enhanced Chat Search Panel'; button.innerHTML = searchIconSVG; button.addEventListener('click', () => { togglePanelVisibility(null); }); buttonContainer.appendChild(button); return buttonContainer; } function ensureFooterButtonExists() { const footerBar = document.querySelector(config.chatFooterBarSelector); if (footerBar) { const existingButton = document.getElementById(FOOTER_BUTTON_ID); if (!existingButton || !footerBar.contains(existingButton)) { const buttonContainer = createFooterButton(); if (buttonContainer) { const notesButton = footerBar.querySelector('#notes_panel_button'); if (notesButton) { footerBar.insertBefore(buttonContainer, notesButton); } else { footerBar.appendChild(buttonContainer); } } } } } function initialize() { loadSettings(); applyStyles(); storedFactionId = GM_getValue(FACTION_ID_STORAGE_KEY, null); const currentPath = window.location.pathname; const isOnAllowedPage = currentPath.includes('/preferences.php') || currentPath.includes('/factions.php'); if (!storedFactionId && isOnAllowedPage) { let enteredId = window.prompt("Enter Faction ID (Numbers only):", ""); if (enteredId && /^\d+$/.test(enteredId.trim())) { storedFactionId = 'faction-' + enteredId.trim(); GM_setValue(FACTION_ID_STORAGE_KEY, storedFactionId); console.log(`EnhancedChat: Stored Faction ID: ${storedFactionId}`); } else if (enteredId !== null) { window.alert("Invalid Faction ID entered (only numbers allowed). Faction chat search will not be enabled until a valid ID is provided on the preferences or faction page."); storedFactionId = null; } else { storedFactionId = null; } } else if (storedFactionId) { console.log(`EnhancedChat: Loaded stored Faction ID: ${storedFactionId}`); } ensureFooterButtonExists(); setInterval(ensureFooterButtonExists, config.FOOTER_BUTTON_CHECK_INTERVAL_MS); setupObservers(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();