// ==UserScript== // @name SOOP 검색 제목 필터링 // @namespace http://tampermonkey.net/ // @version 5.5 // @description 숲 UI 변경 반영 // @author Dell-nong // @match *://www.sooplive.com/search* // @icon https://res.sooplive.com/favicon.ico // @run-at document-idle // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @downloadURL https://update.greasyfork.icu/scripts/569988/SOOP%20%EA%B2%80%EC%83%89%20%EC%A0%9C%EB%AA%A9%20%ED%95%84%ED%84%B0%EB%A7%81.user.js // @updateURL https://update.greasyfork.icu/scripts/569988/SOOP%20%EA%B2%80%EC%83%89%20%EC%A0%9C%EB%AA%A9%20%ED%95%84%ED%84%B0%EB%A7%81.meta.js // ==/UserScript== (function() { 'use strict'; if (window.top !== window.self) return; // ── URL 변경 감지 (SPA 대응) ────────────────────────────── function hookHistory() { const _push = history.pushState.bind(history); const _replace = history.replaceState.bind(history); history.pushState = function(...args) { _push(...args); window.dispatchEvent(new Event('soop-urlchange')); }; history.replaceState = function(...args) { _replace(...args); window.dispatchEvent(new Event('soop-urlchange')); }; window.addEventListener('popstate', () => { window.dispatchEvent(new Event('soop-urlchange')); }); } hookHistory(); function isSearchPage() { return window.location.href.includes('/search'); } // ── 이하 기존 로직 동일 ─────────────────────────────────── const T = { menuOn: GM_getValue('t_menuOn', '✅ 시청 완료 표시 중 (끄기)'), menuOff: GM_getValue('t_menuOff', '❌ 시청 완료 표시 안함 (켜기)'), filter: GM_getValue('t_filter', '필터링'), all: GM_getValue('t_all', '전체'), title: GM_getValue('t_title', '제목'), exclude: GM_getValue('t_exclude', '제외'), placeholder: GM_getValue('t_ph', '제외 키워드 (공백 구분)'), watched: GM_getValue('t_watched', '[시청완료]'), }; let showWatchedTag = GM_getValue('showWatchedTag', true); try { GM_registerMenuCommand(showWatchedTag ? T.menuOn : T.menuOff, () => { GM_setValue('showWatchedTag', !showWatchedTag); location.reload(); }); } catch (e) {} let isFilterActive = false; let excludeKeywords = []; const STORAGE_KEY = 'soop_watched_v3'; const rowId = 'custom_filter_row'; const getWatched = () => { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch(e) { return []; } }; const saveWatched = (id) => { if (!id) return; try { let watched = getWatched(); if (!watched.includes(id)) { watched.push(id); if (watched.length > 2000) watched.shift(); localStorage.setItem(STORAGE_KEY, JSON.stringify(watched)); } } catch(e) {} }; function matchesFilter(title) { try { const excludeMatch = excludeKeywords.every(k => !title.includes(k)); if (!isFilterActive) return excludeMatch; const raw = new URLSearchParams(window.location.search).get('szKeyword') || ''; const keyword = decodeURIComponent(raw).toLowerCase(); const tokens = keyword.split(' ').filter(k => k.length > 0); const includeMatch = tokens.every(k => title.includes(k)); return includeMatch && excludeMatch; } catch(e) { return true; } } function injectStyle() { if (document.getElementById('soop-style-v6')) return; const style = document.createElement('style'); style.id = 'soop-style-v6'; style.textContent = [ '#' + rowId + '{border-top:1px solid rgba(128,128,128,0.2)!important;padding:15px 0!important;margin-top:10px!important;display:flex!important;align-items:center;flex-wrap:wrap;gap:8px;}', '.f-btn{cursor:pointer!important;margin-right:25px!important;font-size:14px!important;color:#888!important;list-style:none!important;}', '.f-btn.active{color:#00c853!important;font-weight:bold!important;}', '.is-watched{opacity:' + (showWatchedTag ? '0.5' : '1') + '!important;}', '.watched-text{color:#ff4444!important;font-size:12px!important;font-weight:bold!important;margin-left:8px!important;}', '#soop-ex-input{font-size:13px;padding:3px 8px;border:1px solid #666;border-radius:4px;width:160px;margin-left:8px;background-color:#fff !important;color:#333 !important;}', '#soop-ex-btn{font-size:13px;padding:3px 8px;cursor:pointer;border:1px solid #aaa;border-radius:4px;background:#f5f5f5 !important;color:#333 !important;margin-left:4px;}' ].join(''); document.head.appendChild(style); } function applyFilterToAll() { document.querySelectorAll('.cBox-list ul li, li[data-type="cBox"]').forEach(item => { try { const t = item.querySelector('.title a, h3.title a'); if (!t) return; item.style.display = matchesFilter(t.innerText.toLowerCase()) ? '' : 'none'; } catch(e) {} }); } function processItem(item) { try { const titleLink = item.querySelector('.title a, h3.title a'); if (!titleLink) return; const href = titleLink.getAttribute('href') || ''; const clipId = (href.match(/\d{8,}/) || [href])[0]; const watched = getWatched(); if (watched.includes(clipId)) { item.classList.add('is-watched'); if (showWatchedTag && !item.querySelector('.watched-text')) { const span = document.createElement('span'); span.className = 'watched-text'; span.textContent = T.watched; titleLink.parentNode.appendChild(span); } } if (!item.dataset.hooked) { const thumbLink = item.querySelector('.thumbs-box a'); [thumbLink, titleLink].forEach(el => { if (!el) return; el.addEventListener('mousedown', (e) => { if (e.button === 2) return; saveWatched(clipId); item.classList.add('is-watched'); }); }); item.dataset.hooked = 'true'; if (isFilterActive || excludeKeywords.length > 0) { item.style.display = matchesFilter(titleLink.innerText.toLowerCase()) ? '' : 'none'; } } } catch(e) {} } function run() { document.querySelectorAll('.cBox-list ul li, li[data-type="cBox"]').forEach(processItem); } function injectUI() { if (document.getElementById(rowId)) return; // 여러 셀렉터를 순서대로 시도 const target = document.querySelector('.select_filter_list') || document.querySelector('.filter_wrap') || document.querySelector('.search_filter') || (() => { // dl 태그들의 공통 부모를 찾음 const dls = document.querySelectorAll('dl'); if (dls.length > 0) return dls[dls.length - 1].parentElement; return null; })(); if (!target) return; const row = document.createElement('dl'); row.id = rowId; const dt = document.createElement('dt'); dt.style.cssText = 'color:#888;font-weight:bold;width:80px;'; dt.textContent = T.filter; row.appendChild(dt); const dd = document.createElement('dd'); const ul = document.createElement('ul'); ul.style.cssText = 'display:flex;list-style:none;margin:0;padding:0;align-items:center;flex-wrap:wrap;gap:4px;'; const btnAll = document.createElement('li'); btnAll.id = 'btn-all'; btnAll.className = 'f-btn active'; btnAll.textContent = T.all; const btnTitle = document.createElement('li'); btnTitle.id = 'btn-title'; btnTitle.className = 'f-btn'; btnTitle.textContent = T.title; const input = document.createElement('input'); input.id = 'soop-ex-input'; input.type = 'text'; input.placeholder = T.placeholder; const btn = document.createElement('button'); btn.id = 'soop-ex-btn'; btn.textContent = T.exclude; ul.appendChild(btnAll); ul.appendChild(btnTitle); ul.appendChild(input); ul.appendChild(btn); dd.appendChild(ul); row.appendChild(dd); target.appendChild(row); // appendChild로 맨 끝에 추가 btnAll.onclick = () => { isFilterActive = false; applyFilterToAll(); btnAll.classList.add('active'); btnTitle.classList.remove('active'); }; btnTitle.onclick = () => { isFilterActive = true; applyFilterToAll(); btnTitle.classList.add('active'); btnAll.classList.remove('active'); }; btn.onclick = () => { excludeKeywords = input.value.toLowerCase().trim().split(' ').filter(k => k.length > 0); applyFilterToAll(); }; input.addEventListener('keydown', (e) => { if (e.key === 'Enter') btn.click(); }); } let observer = null; let intervalId = null; function cleanup() { if (observer) { observer.disconnect(); observer = null; } if (intervalId) { clearInterval(intervalId); intervalId = null; } // 이전에 삽입한 UI 제거 (페이지 이동 시 초기화) const old = document.getElementById(rowId); if (old) old.remove(); const oldStyle = document.getElementById('soop-style-v6'); if (oldStyle) oldStyle.remove(); // 필터 상태 초기화 isFilterActive = false; excludeKeywords = []; } function init() { if (!isSearchPage()) return; // 검색 페이지 아닐 때는 실행 안 함 cleanup(); // 혹시 이전 잔재 정리 injectStyle(); injectUI(); run(); observer = new MutationObserver((mutations) => { injectUI(); mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType !== 1) return; if (node.matches('li[data-type="cBox"], .cBox-list ul li')) { processItem(node); } node.querySelectorAll('li[data-type="cBox"], .cBox-list ul li').forEach(processItem); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); intervalId = setInterval(() => { injectStyle(); injectUI(); }, 2000); } // ── 진입점 ──────────────────────────────────────────────── // 최초 페이지 로드 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // SPA 페이지 이동 감지 window.addEventListener('soop-urlchange', () => { // DOM 교체 대기 후 재초기화 setTimeout(init, 300); }); })();