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);
})();