您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Highlight articles in FreshRSS that match the rule. Rules are described by regular expressions.
// ==UserScript== // @name FreshRSS Keyword Highlight // @namespace https://github.com/hiroki-miya // @version 1.0.3 // @description Highlight articles in FreshRSS that match the rule. Rules are described by regular expressions. // @author hiroki-miya // @license MIT // @match https://freshrss.example.net/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_setValue // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // Language for sorting const sortLocale = 'ja'; // Highlight Color const highlightColor = '#ffff60'; // Retrieve saved highlights let savedHighlights = GM_getValue('highlights', {}); // Define editingHighlightName globally (the name of the highlight currently being edited) let editingHighlightName = null; // Add styles GM_addStyle(` #freshrss-keyword-highlight { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10000; background-color: white; border: 1px solid black; padding: 10px; width: max-content; } #freshrss-keyword-highlight > h2 { box-shadow: inset 0 0 0 0.5px black; padding: 5px 10px; text-align: center; cursor: move; } #freshrss-keyword-highlight > h4 { margin-top: 0; } #highlight-list { margin-bottom: 10px; max-height: 50vh; overflow-y: auto; } .highlight-item { display: flex; justify-content: space-between; align-items: center; } #highlight-edit > div { display: flex; justify-content: space-between; align-items: center; line-height: 2; margin-bottom: 5px; } #highlight-edit > div input { line-height: 2; margin: 0; } .highlight-name, #highlight-edit > div > label { flex-grow: 1; margin-right: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #highlight-edit > div > div:has(input[type="checkbox"]) { margin-left: 5px; max-width: 90%; width: 300px; } #highlight-edit > div input[type="checkbox"] { transform: scale(1.5); margin-left: 4px; } .edit-highlight, .delete-highlight, #highlight-edit > div > input { margin-left: 5px; } .highlight-info-label { display: inline; } .highlight-info { display: inline-block; border-radius: 50%; width: 16px; height: 16px; min-height: 16px; line-height: 1.2; margin-left: 4px; text-align: center; background-color: black; color: white; font-weight: 700; } `); // Function to render the highlight list function updateHighlightList() { // Sort highlights const highlightNames = Object.keys(savedHighlights).sort((a, b) => a.localeCompare(b, sortLocale)); const highlightList = highlightNames.map(name => { return ` <div class="highlight-item"> <div class="highlight-name">${name}</div> <button class="edit-highlight" data-name="${name}">Edit</button> <button class="delete-highlight" data-name="${name}">Delete</button> </div> `; }).join(''); // Render the highlight list document.getElementById('highlight-list').innerHTML = highlightList || 'No registered highlight rules'; // Re-register the highlight edit button events Array.from(document.querySelectorAll('.edit-highlight')).forEach(button => { button.addEventListener('click', () => { const highlightName = button.getAttribute('data-name'); const highlight = savedHighlights[highlightName]; // Pre-fill the form with the highlight values document.getElementById('highlight-name').value = highlightName; document.getElementById('highlight-currentUrl').value = highlight.currentUrl || ''; document.getElementById('highlight-title').value = highlight.title || ''; document.getElementById('highlight-url').value = highlight.url || ''; document.getElementById('highlight-content').value = highlight.content || ''; document.getElementById('highlight-text').value = highlight.text || ''; document.getElementById('highlight-case').checked = highlight.caseInsensitive || false; editingHighlightName = highlightName; // Update the form heading for editing document.querySelector('#highlight-edit-title').innerText = 'Edit Existing Highlight Rule'; document.querySelector('#fkh-save').innerText = 'Update'; }); }); // Re-register the highlight delete button events Array.from(document.querySelectorAll('.delete-highlight')).forEach(button => { button.addEventListener('click', () => { const highlightName = button.getAttribute('data-name'); delete savedHighlights[highlightName]; GM_setValue('highlights', savedHighlights); updateHighlightList(); applyAllHighlights(); }); }); } // Display highlight settings function showSettings() { const settingsHTML = ` <h2>Keyword Highlight Rule Settings</h2> <h4>Saved Highlight Rules</h4> <div id="highlight-list"></div> <br> <hr> <h4 id="highlight-edit-title">Create New Highlight Rule</h4> <div id="highlight-edit"> <div><label>Highlight Name</label><input type="text" id="highlight-name"></div> <div><label>FreshRSS Feed List URL</label><input type="text" id="highlight-currentUrl"></div> <div><label>Title</label><input type="text" id="highlight-title"></div> <div><label>Content URL</label><input type="text" id="highlight-url"></div> <div><label class="highlight-info-label">Content<div title="article.flux_content.innerText" class="highlight-info">i</div></label><input type="text" id="highlight-content"></div> <div><label class="highlight-info-label">Text<div title="div.text.innerHTML" class="highlight-info">i</div></label><input type="text" id="highlight-text"></div> <div><label>Case insensitive?</label><div><input type="checkbox" id="highlight-case"></div></div> <br> </div> <button id="fkh-save">Save</button> <button id="fkh-clear">Clear</button> <button id="fkh-close">Close</button> `; const settingsDiv = document.createElement('div'); settingsDiv.id = 'freshrss-keyword-highlight'; settingsDiv.innerHTML = settingsHTML; document.body.appendChild(settingsDiv); // Initial render of saved highlight list updateHighlightList(); // Make settikeywords panel draggable makeDraggable(settingsDiv); // Save or update button event document.getElementById('fkh-save').addEventListener('click', () => { const highlightName = document.getElementById('highlight-name').value; const highlightCurrentUrl = document.getElementById('highlight-currentUrl').value; const highlightTitle = document.getElementById('highlight-title').value; const highlightUrl = document.getElementById('highlight-url').value; const highlightContent = document.getElementById('highlight-content').value; const highlightText = document.getElementById('highlight-text').value; const caseInsensitive = document.getElementById('highlight-case').checked; if (!highlightName) { alert('Please enter a highlight name'); return; } // Save or update the highlight savedHighlights[highlightName] = { currentUrl: highlightCurrentUrl, title: highlightTitle, url: highlightUrl, content: highlightContent, text: highlightText, caseInsensitive: caseInsensitive }; // If the highlight name was changed during editing, delete the old highlight if (editingHighlightName && editingHighlightName !== highlightName) { delete savedHighlights[editingHighlightName]; } GM_setValue('highlights', savedHighlights); showTooltip('Saved'); initEdit(); // Update highlight list updateHighlightList(); // Apply highlights immediately after saving applyAllHighlights(); }); // Clear button event document.getElementById('fkh-clear').addEventListener('click', () => { initEdit(); }); // Close button event document.getElementById('fkh-close').addEventListener('click', () => { document.body.removeChild(settingsDiv); }); } function initEdit() { editingHighlightName = null; document.getElementById('highlight-name').value = ''; document.getElementById('highlight-currentUrl').value = ''; document.getElementById('highlight-title').value = ''; document.getElementById('highlight-url').value = ''; document.getElementById('highlight-content').value = ''; document.getElementById('highlight-text').value = ''; document.getElementById('highlight-case').checked = false; // Update the form heading for creating a new highlight document.querySelector('#highlight-edit-title').innerText = 'Create New Highlight Rule'; document.querySelector('#fkh-save').innerText = 'Save'; } // Function to display the tooltip function showTooltip(message) { // Create the tooltip element const tooltip = document.createElement('div'); tooltip.textContent = message; tooltip.style.position = 'fixed'; tooltip.style.top = '50%'; tooltip.style.left = '50%'; tooltip.style.transform = 'translate(-50%, -50%)'; tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.75)'; tooltip.style.color = 'white'; tooltip.style.padding = '10px 20px'; tooltip.style.borderRadius = '5px'; tooltip.style.zIndex = '10000'; tooltip.style.fontSize = '16px'; tooltip.style.textAlign = 'center'; // Add the tooltip to the page document.body.appendChild(tooltip); // Automatically remove the tooltip after 1 second setTimeout(() => { document.body.removeChild(tooltip); }, 1000); } // Make element draggable function makeDraggable(elmnt) { const header = elmnt.querySelector('h2'); let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; header.onmousedown = dragMouseDown; function dragMouseDown(e) { e = e || window.event; e.preventDefault(); // Get the mouse cursor position at startup: pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; e.preventDefault(); // Calculate the new cursor position: pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; // Set the element's new position: elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } // Apply all highlights automatically function applyAllHighlights() { const articles = Array.from(document.querySelectorAll('#stream > .flux')); const currentPageUrl = window.location.href; articles.forEach(article => { const title = article.querySelector('a.item-element.title')?.innerText || ''; const url = article.querySelector('a.item-element.title')?.href || ''; const content = article.querySelector('.flux_content')?.innerText || ''; const text = article.querySelector('div.text')?.innerHTML || ''; let matchesAnyHighlight = false; // Check all saved highlights for (let highlightName in savedHighlights) { const highlight = savedHighlights[highlightName]; const regexFlags = highlight.caseInsensitive ? 'i' : ''; const currentUrlMatch = !highlight.currentUrl || new RegExp(highlight.currentUrl, regexFlags).test(currentPageUrl); const titleMatch = !highlight.title || new RegExp(highlight.title, regexFlags).test(title); const urlMatch = !highlight.url || new RegExp(highlight.url, regexFlags).test(url); const contentMatch = !highlight.content || new RegExp(highlight.content, regexFlags).test(content); const textMatch = !highlight.text || new RegExp(highlight.text, regexFlags).test(text); // console.log('titleMatch(' + titleMatch + '): ' + highlight.title + ' = ' + title + '\n' + // 'urlMatch(' + urlMatch + '): ' + highlight.url + ' = ' + url + '\n' + // 'contentMatch(' + contentMatch + '): ' + highlight.content + ' = ' + content + '\n' + // 'textMatch(' + textMatch + '): ' + highlight.text + ' = ' + text + '\n'); // Check if all highlight conditions are met (AND condition) if (currentUrlMatch && titleMatch && urlMatch && contentMatch && textMatch) { matchesAnyHighlight = true; break; } } // Add ng class to articles matching the highlight if (matchesAnyHighlight) { article.classList.add('highlight'); article.style.backgroundColor = highlightColor; } else { article.classList.remove('highlight'); article.style.backgroundColor = null; } }); } // Setup MutationObserver function setupObserver() { const targetNode = document.querySelector('#stream'); if (targetNode) { const observer = new MutationObserver(applyAllHighlights); observer.observe(targetNode, { childList: true, subtree: true }); // Initial highlight application applyAllHighlights(); } else { // Retry if #stream is not found setTimeout(setupObserver, 1000); } } // Register settings screen GM_registerMenuCommand('Settings', showSettings); // Start setupObserver when the script starts setupObserver(); })();