// ==UserScript== // @name Mayday.Blue 页面查找+歌词字体修改 // @namespace https://github.com/freysu // @version 0.1 // @description Mayday.Blue 页面查找功能+歌词字体修改 // @author FreySu // @match https://mayday.blue/* // @icon https://mayday.blue/favicon.ico // @grant GM_addStyle // @require https://cdn.jsdelivr.net/npm/mark.js/dist/mark.min.js // @require https://cdn.jsdelivr.net/npm/mousetrap // ==/UserScript== (function() { 'use strict'; var parent = document // 防抖函数 function debounce(func, delay) { let timeoutId; return function() { clearTimeout(timeoutId); timeoutId = setTimeout(func, delay); }; } const listingClassNameTarget = { "list":"div[data-astro-cid-j7pv25f6]", "lyric":"article.leading-snug" } // 放置所有相关变量 const state = { markInstance: null, currentIndex: -1, allArr: [], keywords: [], matches: null, }; // 放置DOM元素 const elements = { searchInput: parent.createElement('input'), prevButton: parent.createElement('button'), nextButton: parent.createElement('button'), }; // 定义选项常量 const MARKJS_OPTIONS = { element: 'span', exclude: ['script', 'select', 'textarea'], separateWordSearch: false, diacritics: true, synonyms: {}, accuracy: { value: 'partially', limiters: [",", ".", "-", "_", "/", "+", "(", ")", " ", "<", ">", "「", "」"], }, caseSensitive: false, wholeWord: true, wildcards: false, acrossElements: false, ignoreJoiners: true, ignorePunctuation: [",", ".", "?", "!", ";", ":", "-", "–", "—", "(", ")"], each: function(node) { state.allArr.push({ text: node.innerText }); state.currentIndex += 1; }, done: function(totalMatches) { console.log('Marking done!'); console.log(state.allArr); console.log(totalMatches); state.matches = totalMatches; const toLowerCase = (str) => str.toLowerCase(); let keywordCounts = {}; const lowercasedItems = state.allArr.map(({ text }) => toLowerCase(text)); for (const text of lowercasedItems) { keywordCounts[text] = lowercasedItems.filter((t) => t === text).length; } console.log(keywordCounts); }, debug: false, log: window.console }; // 初始化Mark.js实例 function initMarkInstance() { const { list, lyric } = listingClassNameTarget; const elm = parent.querySelectorAll([list, lyric]); state.markInstance = new Mark(elm); } // 清除标记 function clearMark() { state.markInstance.unmark(); state.allArr.length = 0; } // 搜索匹配项 function searchMatches() { const keyword = state.keywords[0]; clearMark(); if (keyword) { state.markInstance.mark(state.keywords, { className: 'frey_highlight_4', ...MARKJS_OPTIONS }); } } // 获取下一个匹配项 function getNext(flg) { const count = parent.querySelectorAll(".frey_highlight_4").length; if (count === 0) { return; } if (flg !== 0) { state.currentIndex += flg; } if (state.currentIndex >= count) { state.currentIndex = 0; } if (state.currentIndex < 0) { state.currentIndex = count - 1; } document.querySelectorAll(".frey_highlight_4")[state.currentIndex].scrollIntoView({ behavior: 'smooth' }); } // 输入事件处理 function handleInput() { const keyword = elements.searchInput.value.trim(); state.keywords.length = 0; state.keywords.push(keyword); searchMatches(); } // 添加事件监听器 elements.searchInput.addEventListener('input', debounce(handleInput, 300)); elements.prevButton.addEventListener("click", function() { getNext(-1) }); elements.nextButton.addEventListener("click", function() { getNext(1) }); function render(){ // 初始化Mark.js实例 initMarkInstance(); Mousetrap.bind("/",function(){ elements.searchInput.focus() return false; }) // 创建DOM元素 const searchDiv = parent.createElement('div'); searchDiv.className = 'flex items-center gap-2'; const searchContainer = document.createElement('div'); searchContainer.className = 'searchContainer'; elements.searchInput.type = 'text'; elements.searchInput.placeholder = 'Type [/] to Search ...'; elements.searchInput.style.marginRight = '10px'; elements.prevButton.textContent = 'Prev'; elements.prevButton.style.marginRight = '10px'; elements.nextButton.textContent = 'Next'; // 获取页面底部的容器 const container = parent.querySelector('footer'); // 添加DOM元素到容器 searchContainer.appendChild(elements.searchInput); searchContainer.appendChild(elements.prevButton); searchContainer.appendChild(elements.nextButton); searchDiv.appendChild(searchContainer); container.appendChild(searchDiv); // 搜索框样式 GM_addStyle(`.frey_highlight_4{color:black;padding:5px;background:pink;}.searchContainer{display:flex;align-items:center;margin:10px;}.searchContainer input[type="text"]{color:black;padding:6px;border:1px solid #ddd;border-radius:4px;font-size:12px;outline:none;transition:border-color 0.3s ease;}.searchContainer input[type="text"]:focus{border-color:#3498db;}.searchContainer button{padding:6px 12px;border:none;border-radius:4px;background-color:#3498db;color:white;font-size:12px;cursor:pointer;transition:background-color 0.3s ease;}.searchContainer button:hover{background-color:#2980b9;}`) // 歌词字体修改 GM_addStyle(`.leading-snug{line-height:1.375;font-family:"华文行楷";}`) } main() async function main(){ await getElement(parent,"footer") render() } function getElement (parent, selector, timeout = 0) { return new Promise((resolve) => { let result = parent.querySelector(selector) if (result) return resolve(result) let timer const mutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver if (mutationObserver) { const observer = new mutationObserver((mutations) => { for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if (addedNode instanceof Element) { result = addedNode.matches(selector) ? addedNode : addedNode.querySelector(selector) if (result) { observer.disconnect() timer && clearTimeout(timer) return resolve(result) } } } } }) observer.observe(parent, { childList: true, subtree: true }) if (timeout > 0) { timer = setTimeout(() => { observer.disconnect() return resolve(null) }, timeout) } } else { const listener = (e) => { if (e.target instanceof Element) { result = e.target.matches(selector) ? e.target : e.target.querySelector(selector) if (result) { parent.removeEventListener('DOMNodeInserted', listener, true) timer && clearTimeout(timer) return resolve(result) } } } parent.addEventListener('DOMNodeInserted', listener, true) if (timeout > 0) { timer = setTimeout(() => { parent.removeEventListener('DOMNodeInserted', listener, true) return resolve(null) }, timeout) } } }) } })()