LewdCorner Highlight Helper

Basic modifications to Lewdcorner, especially latest updates

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         LewdCorner Highlight Helper
// @namespace    http://scizzer12.me
// @version      2026-07-28.1
// @description  Basic modifications to Lewdcorner, especially latest updates
// @author       scizzer12
// @match        https://lewdcorner.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=lewdcorner.com
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    // Styles
    GM_addStyle(`
.lcCardMediaAction.is-active[title] {
  background: #f29bf9;
  color: #000;
  border-color: #f49def;
}

.lcCardMedia:hover .lcCardOverlay {
    opacity: 1 !important;
}

.hated:not(:hover) {
    opacity: 25%;
}

.lcCard--abandoned {
    border: 1px solid red;
}

.lcCard--abandoned:hover {
    opacity: 1;
}

.lcCardHoverDetails .lcCardHoverTags span {
    background-color: black;
}`);

    // Break closure and inject script changes
    if (window.location.href.includes('latest-updates')) {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(m => m.addedNodes.forEach(node => {
                if ( node.tagName === "SCRIPT" && node.textContent.substring(18,26) == 'var boot') {
                    node.textContent = node.textContent.replace( // Expose data
                        'var settings = readSettings();',
                        'var settings = readSettings();\n' +
                        'window.lcCtx = (prop) => eval(prop);'
                    ).replace( // Bookmark note
                        "class=\"lcCardMediaAction' + (item.is_bookmarked",
                        "' + (item.bookmark_note ? 'title=\"' + item.bookmark_note + '\" ' : '') + 'class=\"lcCardMediaAction' + (item.is_bookmarked"
                    ).replace( // Update image cache
                        "function enableManualMode()",
                        "card.refreshImages = () => { images = decodeImages(image.getAttribute('data-images')) }\n\n" +
                        "function enableManualMode()"
                    ).replace( // Real image fallback
                        /image\.addEventListener\('error'[\s\S]+?}\);/, ""
                    )/*.replace( // pill click
                        /state\.tagsInc = state\.tagsInc\.concat\(\[tag\]\)\.slice\(0, 10\);\s*renderSelectedTags\('inc'\);\s*renderTagList\('inc'\);/,
                        "const type = button.classList.contains('negative') != event.shiftKey ? 'exc' : 'inc';" +
                        "const tagList = 'tags' + (type == 'inc' ? 'Inc' : 'Exc');" +
                        "state[tagList] = state[tagList].concat([tag]).slice(0, 10);" +
                        "renderSelectedTags(type);" +
                        "renderTagList(type);"
                    )*/;
                    // Options menu
                    dqs('#lcLuTabSaved').insertAdjacentHTML('beforebegin', '<button type="button" class="lcModalNavBtn" id="lcLuTabHighlight" data-pane="highlight" role="tab" aria-selected="false" aria-controls="lcLuPaneHighlight" tabindex="-1">Tag Highlights</button>');
                    dqs('#lcLuPaneSaved').insertAdjacentHTML(
                        'beforebegin',
                        '<div class="lcModalPane" id="lcLuPaneHighlight" data-pane-content="highlight" role="tabpanel" aria-labelledby="lcLuTabHighlight">' +
                        '  <div class="lcModalSection">' +
                        '    <div class="lcModalSectionTitle">Tag Highlights</div>' +
                        '    <p class="lcModalSectionSub">Control how important tags are displayed.</p>' +
                        '    <div class="lcSettingsList">' +
                        '      <div class="lcSettingsRow"><div class="lcSettingsRowLabel">Liked<span class="lcSettingsRowHint">ordered,comma separated,list</span></div><input type="text" name="likedTags" class="lcClassicInput" placeholder="Positive tags" autocomplete="off"></div>' +
                        '      <div class="lcSettingsRow"><div class="lcSettingsRowLabel">Disliked<span class="lcSettingsRowHint">ordered,comma separated,list</span></div><input type="text" name="dislikedTags" class="lcClassicInput" placeholder="Negative tags" autocomplete="off"></div>' +
                        '      <div class="lcSettingsRow"><div class="lcSettingsRowLabel">Hated<span class="lcSettingsRowHint">ordered,comma separated,list</span></div><input type="text" name="hatedTags" class="lcClassicInput" placeholder="Dealbreaker tags" autocomplete="off"></div>' +
                        '    </div>' +
                        '  </div>' +
                        '</div>'
                    );
                    observer.disconnect();
                }
            }));
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
    }

    // Tooling
    const dqs = document.querySelector.bind(document),
          dqsa = document.querySelectorAll.bind(document),
          dqsaA = (a) => [...document.querySelectorAll(a)];
    function live(sel, event, callback, context = document) {
        context.addEventListener(event, (e) => {
            const el = e.target || e.srcElement,
                  found = el.closest?.(sel);
            if (found) callback.call(found, e);
        });
    }
    async function lazyLoad(test, maxTries = 100) {
        return new Promise((res, rej) => {
            if (typeof test != 'function')
                return (new MutationObserver((m,o) => { o.disconnect(); return res(m);}).observe(test, {childList: true}));
            let result = test();
            if (!result) {
                let tries = 0;
                const poll = setInterval(() => {
                    result = test();
                    if (result) {
                        clearInterval(poll);
                        return res(result);
                    }
                    else if (tries > maxTries) {
                        clearInterval(poll);
                        return rej();
                    }
                    else tries++;
                }, 200);
            }
            else res(result);
        });
    }

    let likedTags = GM_getValue('likedTags', []);
    let dislikedTags = GM_getValue('dislikedTags', []);
    let hatedTags = GM_getValue('hatedTags', []);

    // Backup image thumbnails
    const needScrape = new Set();
    let scrapeCache = localStorage.getItem('scrapeCache');
    let fullRefresh = false;
    if (scrapeCache) scrapeCache = new Map(JSON.parse(scrapeCache).map(m => [m[0], new Map(m[1])]));
    else scrapeCache = new Map();
    let scrapeRunning = false;
    function queueScrape(link) {
        needScrape.add(link);
        if (!scrapeRunning) { scrapeRunning = true; scrape(); }
    }
    async function scrape() {
        const url = [...needScrape][0];
        let matches,
            queued = scrapeCache.has(url);
        const result = lcCtx('result').items.find(r => r.link === url),
              img = dqs(`.lcCard[data-card-id="${result.id}"] .lcCardImage`);
        if (queued) matches = scrapeCache.get(url);
        else {
            const res = await fetch(url);
            const text = await res.text();
            matches = new Map([...text.matchAll(/<a href="([^"]+)"\s*target="_blank" class="js-lbImage"><img src="([^"]+)"/g)].map(m => m.slice(1)));
            scrapeCache.set(url, matches);
        }
        if (!matches.size) {
            if (result) Object.assign(result, { dead: true, images: '', image: '' });
            fullRefresh = true;
        }
        else if (result) {
            if (result.images.length) result.images = result.images.map(i => matches.get(i)).filter(i => i);
            else result.images = [...matches.values()];
            result.image = matches.get(result.image) || result.images[0] || "";
            if (img) {
                img.src = result.image;
                img.dataset.images = escape(JSON.stringify(result.images));
            }
            else fullRefresh = true;
            const card = dqs(`.lcCard[data-card-id="${result.id}"]`);
            card.refreshImages?.();
            card.style.border = "1px solid pink";
        }
        needScrape.delete(url);
        if (needScrape.size) scrape();
        else {
            scrapeRunning = false;
            localStorage.setItem('scrapeCache', JSON.stringify([...scrapeCache.entries()].map(m => [m[0], [...m[1].entries()]])));
            if (fullRefresh) {
                fullRefresh = false;
                lcCtx('render')();
            }
        }
    }

    function pageLoad(e) {
        const location = e?.destination?.url || document.location.href;
        if (location.includes('/threads/')) {
            // Tag highlights in threads
            dqsa('.p-description .js-tagList a').forEach(t => {
                const text = t.textContent.trim();
                if (likedTags.includes(text)) t.style.background = 'darkgreen';
                else if (dislikedTags.includes(text)) t.setAttribute('style', 'background: thistle; color: black;');
                else if (hatedTags.includes(text)) t.style.background = 'darkred';
            });
        }
        else if (location.includes('latest-updates')) {
            // Clickable tags, shift click to invert
            live('.lcCardHoverTags span', 'click', e => {
                var tag = e.target.textContent.trim() || '';
                if (tag && lcCtx('state').tagsInc.indexOf(tag) === -1) {
                    const type = e.target.classList.contains('negative') != event.shiftKey ? 'exc' : 'inc';
                    const tagList = 'tags' + (type == 'inc' ? 'Inc' : 'Exc');
                    lcCtx('state')[tagList] = lcCtx('state')[tagList].concat([tag]).slice(0, 10);
                    lcCtx('renderSelectedTags')(type);
                    lcCtx('renderTagList')(type);
                    lcCtx('refresh')();
                }
            });
            // Option menu
            dqs('#lcLuPaneHighlight').querySelectorAll('input').forEach(i => {
                const name = i.name.replace(/\W/g,'');
                if (name === 'likedTags') i.value = likedTags.join(', ');
                else if (name === 'dislikedTags') i.value = dislikedTags.join(', ');
                else if (name === 'hatedTags') i.value = hatedTags.join(', ');
                i.addEventListener('change', e => {
                    const values = i.value.trim().toLowerCase().split(/\s*,\s*/);
                    GM_setValue(name, values);
                    eval(`${name} = values`);
                    i.value = values.join(', ');
                    lcCtx('refresh')();
                });
            });
            const processGrid = () => {
                const firstCard = dqs('.lcClassicGrid article.lcCard'),
                      items = lcCtx('result').items;
                if (!firstCard || firstCard.filtered) return;
                else firstCard.filtered = true;
                dqsa('.lcClassicGrid article.lcCard').forEach((c, i) => {
                    const res = items[i] || items.find(i => i.id == c.dataset['card-id']);
                    const bookmarked = c.querySelector('.lcCardTagOverflowWrap+.lcCardMediaAction.is-active');
                    if (bookmarked) c.style.border = "1px solid " + getComputedStyle(bookmarked).backgroundColor;
                    const shortTagBox = c.querySelector('.lcCardRestingTagList');
                    const shortTags = [...c.querySelectorAll('.lcCardRestingTagList>button.lcCardRestingTag')];
                    let allTags = c.querySelectorAll('button.lcCardRestingTag');
                    const previewTags = new Map([...c.querySelectorAll('.lcCardHoverTags span')].map(s => [s.textContent.trim(), s]));
                    if (!allTags.length) allTags = shortTags;
                    const imgContainer = c.querySelector('.lcCardImage');
                    const link = c.querySelector('.lcCardHoverMediaLink').href;
                    if (scrapeCache.get(link)) c.style.border = "1px solid pink";
                    if (!imgContainer && !res.dead) queueScrape(link);
                    else imgContainer?.addEventListener('error', e => {e.preventDefault(); e.stopPropagation(); queueScrape(link); }, { once: true });
                    let score = 0;
                    // Tag processing
                    allTags.forEach(t => {
                        const text = t.textContent.trim().replace(/-/g, ' ');
                        const preview = previewTags.get(t.textContent.trim());
                        if (likedTags.includes(text)) t.style.background = 'darkgreen';
                        else if (dislikedTags.includes(text)) { t.setAttribute('style', 'background: thistle; color: black;'); t.classList.add('negative'); }
                        else if (hatedTags.includes(text)) { t.style.background = 'darkred'; t.classList.add('negative'); }
                        else return;
                        const exists = shortTags.find(s => s.textContent.trim().replace(/-/g, ' ') === text);
                        if (exists) { exists.style.background = t.style.background; exists.style.color = t.style.color; }
                        else {
                            const normalTag = shortTags.findLastIndex(s => !s.style.background);
                            if (normalTag >= 0) shortTags.splice(normalTag, 1)[0].remove();
                        }
                        shortTags[0].insertAdjacentElement('beforebegin', exists ? exists : t);
                        shortTags.unshift(t);
                        if (t.style.background == 'darkred') c.classList.add('hated');
                        preview.setAttribute('style', shortTags[0].getAttribute('style'));
                        if (t.classList.contains('negative')) preview.classList.add('negative');
                    });
                });
                lazyLoad(dqs('#lcLuGrid')).then(processGrid);
            };
            processGrid();
        }
    }
    navigation.addEventListener('navigate', pageLoad);
    document.addEventListener('DOMContentLoaded', pageLoad);
})();