您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Highlight and pin threads in the catalog
// ==UserScript== // @name Holotower Catalog Highlights and Pin // @namespace http://holotower.org/ // @version 1.0 // @author anonymous // @license CC0 // @description Highlight and pin threads in the catalog // @icon https://boards.holotower.org/static/emotes/ina/_tehepero.png // @match *://boards.holotower.org/*/catalog.html // @match *://holotower.org/*/catalog.html // @grant none // @run-at document-body // ==/UserScript== (function () { 'use strict'; if (active_page != 'catalog') { return; } const STORAGE_KEY = 'pinnedThreadSettings'; function getSettings() { try { const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY)); if (!parsed || !Array.isArray(parsed.highlights)) throw new Error(); return parsed; } catch { const defaults = { highlights: [{ name: 'Hololive Global', color: '#00bfff' }], pinThreads: true, hideOlderThreads: false }; saveSettings(defaults); return defaults; } } function saveSettings(settings) { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } function createSettingsButtonAndPopup() { // Button $("<button>", { text: 'Pin Settings', css: { 'margin-left': '6px', 'padding': '2px 8px', 'font-size': '13px', } }).click(function () { refreshList(); $("#pin-settings").toggle(); }).appendTo('span.catalog_search'); // Popup const $pinSettings = $("<div>", { id: 'pin-settings', css: { 'position': 'fixed', 'overflow': 'auto', 'max-height': '90vh', 'top': '50%', 'left': '50%', 'transform': 'translate(-50%, -50%)', 'background': '#1c1c1c', 'color': '#eee', 'border': '1px solid #444', 'box-shadow': '0 4px 12px rgba(0,0,0,0.4)', 'padding': '14px', 'z-index': 999, 'width': '330px', 'border-radius': '6px', 'display': 'none' } }).appendTo('body'); // Close Button $("<button>", { text: '✖', css: { 'border': 'none', 'background': 'transparent', 'color': '#ccc', 'fontSize': '16px', 'cursor': 'pointer', 'position': 'absolute', 'top': '8px', 'right': '12px' } }).click(function () { $("#pin-settings").hide(); }).appendTo($pinSettings); // Header $("<h3>", { text: 'Highlight Settings', css: { 'margin': '0', 'padding': '0', 'color': '#fff' } }).appendTo($pinSettings); // List const $list = $("<div>", { css: { 'padding': '0', 'margin-top': '8px' } }).appendTo($pinSettings); const settings = getSettings(); function refreshList() { $list.empty(); settings.highlights.forEach((entry, index) => { const $listItem = $("<div>", { css: { 'margin-bottom': '8px', 'display': 'flex', 'align-items': 'center', 'gap': '4px' } }).appendTo($list); // Subject Input $("<input>", { type: 'text', placeholder: 'Subject', value: entry.name, title: 'Text to search in threads subject. If a thread has no subject, its comment is searched instead. Case insensitive', css: { 'flex': '2', 'background': '#2a2a2a', 'color': '#eee', 'border': '1px solid #555', 'padding': '3px' } }).change(function () { entry.name = this.value; saveSettings(settings); highlightLatestThreads(); }).appendTo($listItem); // Color Input $("<input>", { type: 'color', class: 'color-picker', value: entry.color, css: { 'width': '30px', 'height': '30px', 'border': 'none', 'background': 'transparent' } }).change(function () { entry.color = this.value; $(this).siblings('.hex-color').val(this.value); saveSettings(settings); highlightLatestThreads(); }).appendTo($listItem); // Hex Input $("<input>", { type: 'text', class: 'hex-color', placeholder: 'Hex', value: entry.color, css: { 'width': '54px', 'background': '#2a2a2a', 'color': '#eee', 'border': '1px solid #555', 'padding': '3px' } }).change(function () { if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(this.value)) { entry.color = this.value; $(this).siblings('.color-picker').val(this.value); saveSettings(settings); highlightLatestThreads(); } }).appendTo($listItem); // Remove Button $("<button>", { text: '✖', css: { 'background': '#400', 'color': '#fff', 'border': '1px solid #700', 'cursor': 'pointer' } }).click(function () { settings.highlights.splice(index, 1); saveSettings(settings); refreshList(); highlightLatestThreads(); }).appendTo($listItem); }); } // Add button $("<button>", { text: '+ Add', css: { 'margin-top': '4px', 'background': '#333', 'color': '#eee', 'border': '1px solid #555', 'padding': '4px 8px 4px 5px', 'cursor': 'pointer' } }).click(function () { settings.highlights.push({ name: '', color: '#00bfff' }); saveSettings(settings); refreshList(); setTimeout(() => { const inputs = $list.find('input[placeholder="Subject"]'); if (inputs.length > 0) { inputs[inputs.length - 1].focus(); } }, 0); }).appendTo($pinSettings); // Pin threads checkbox $("<label>", { title: 'Pin the most recently bumped highlighted threads' } ).append( $("<input>", { type: 'checkbox', checked: settings.pinThreads, css: { 'margin-left': '10px' } }).change(function () { settings.pinThreads = this.checked; saveSettings(settings); highlightLatestThreads(); }), "Pin" ).appendTo($pinSettings); // Hide non-pinned threads checkbox $("<label>", { title: 'Hide all the older highlighted threads' } ).append( $("<input>", { type: 'checkbox', checked: settings.hideOlderThreads, css: { 'margin-left': '10px' } }).change(function () { settings.hideOlderThreads = this.checked; saveSettings(settings); highlightLatestThreads(); }), "Hide older threads" ).appendTo($pinSettings); } function highlightLatestThreads() { const settings = getSettings(); if (!settings.highlights || !settings.highlights.length) return; const $grid = $('#Grid'); const highlightedThreads = {}; $("#Grid > div.mix").each(function () { const $mix = $(this); const $thread = $mix.find('.thread'); if ($mix.data('thread-highlighter-hidden') === 'true') { $mix.show(); $mix.data('thread-highlighter-hidden', 'false') } $mix.removeClass('highlighted'); $thread.css('box-shadow', ''); const subjectEl = $mix.find('.subject'); var subjectText = ''; if (subjectEl) subjectText = subjectEl.text().trim().replace(/\s+/g, ' ').toLowerCase(); if (!subjectText) { // if no subject try the post content subjectText = Array.from($mix.find(".replies strong").parent().contents().filter(function () { return this.nodeType == Node.TEXT_NODE; }), (x) => x.textContent).join(' ').toLowerCase(); } for (const setting of settings.highlights) { const matchText = setting.name.trim().replace(/\s+/g, ' ').toLowerCase(); if (!matchText || !subjectText.includes(matchText)) continue; $mix.addClass('highlighted'); $thread.css('box-shadow', 'inset 0 0 2px 2px ' + setting.color); // push thread into array if (!highlightedThreads[matchText]) highlightedThreads[matchText] = []; highlightedThreads[matchText].push($mix); } }); // sort by bump for (const matchText in highlightedThreads) { highlightedThreads[matchText].sort(($a, $b) => $b.data('bump') - $a.data('bump')); } // sort the object by the array with the most recent bump const sortedThreads = Object.entries(highlightedThreads).sort(([, a], [, b]) => a[0].data('bump') - b[0].data('bump')); const sort_by = $("#sort_by").val(); $grid.mixItUp('sort', (sort_by == "random" ? sort_by : "sticky:desc " + sort_by)); // loop through sorted threads for (const [matchText, threads] of sortedThreads) { if (settings.pinThreads) { $grid.find('.mix').first().before(threads[0], ' '); } if (settings.hideOlderThreads) { for (let i = 1; i < threads.length; i++) { threads[i].hide(); threads[i].data('thread-highlighter-hidden', 'true'); } } } } $(window).on('load', function () { createSettingsButtonAndPopup(); highlightLatestThreads(); }); })();