// ==UserScript== // @name SOOP (숲) - 목록 탐색 자동 PIP // @namespace http://tampermonkey.net/ // @version 40.3 // @description 재생중인 화면을 PIP로 전환하고 탐색 // @author tamszero1, Gemini, Claude // @license MIT // @match https://play.sooplive.com/* // @match https://vod.sooplive.com/* // @match https://www.sooplive.com/* // @grant GM_addStyle // @grant unsafeWindow // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/556481/SOOP%20%28%EC%88%B2%29%20-%20%EB%AA%A9%EB%A1%9D%20%ED%83%90%EC%83%89%20%EC%9E%90%EB%8F%99%20PIP.user.js // @updateURL https://update.greasyfork.icu/scripts/556481/SOOP%20%28%EC%88%B2%29%20-%20%EB%AA%A9%EB%A1%9D%20%ED%83%90%EC%83%89%20%EC%9E%90%EB%8F%99%20PIP.meta.js // ==/UserScript== (function () { 'use strict'; const EXIT_REDIRECT_KEY = 'soop_exit_redirect_url'; const EXIT_REDIRECT_ARMED = 'soop_exit_redirect_armed'; const KEEP_MS = 30000; const CLICK_TTL = 1500; const VOD_RE = /^\/(player|vod|catchstory|catch|view)\//i; const PIP_PATHS = new Set(['/', '/live/all', '/my/favorite', '/search', '/directory/category']); const DROPDOWN_SEL = '#areaSuggest li, #areaHistory li, #areaRealtime li, #areaRecommend li'; const P_ID = '#player_area,#playerArea,#playerWrap,#player_wrap,#vodPlayer,#webPlayer,#player,#afreecaPlayer,#ap_player,#vodWrap,#vod_player'; const P_CLS = '.player_area,.webplayer_area,.vod_player,.player_wrap,.player-wrap,.player_box,.video_box,.vod_area,.catch_player'; const MENU_IGNORE_SEL = '#userArea, #logArea, #soop-gnb, .loginUserMenu, .profileWrap, .serviceUtil'; const originalOpen = window.open; const uWin = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const originalTargetOpen = uWin.open; const isPlayerDomain = location.hostname === 'play.sooplive.com' || location.hostname === 'vod.sooplive.com'; const FAKE_WIN = Object.freeze({ closed: false, close() {}, focus() {}, blur() {}, postMessage() {}, document: { write() {}, close() {} }, location: { href: '' } }); function isCatchVodPlayer(el) { return !!(el && el.matches?.('.vod_player') && el.closest('section.catch_webplayer_wrap')); } function rememberCatchPlayerSize(player) { if (!isCatchVodPlayer(player)) return; if (player.dataset.origWidth == null) player.dataset.origWidth = player.style.width || ''; if (player.dataset.origHeight == null) player.dataset.origHeight = player.style.height || ''; } function restoreCatchPlayerSize(player) { if (!isCatchVodPlayer(player)) return; player.style.width = player.dataset.origWidth || ''; player.style.height = player.dataset.origHeight || ''; delete player.dataset.origWidth; delete player.dataset.origHeight; } function refreshPipPlayerSize() { const player = document.querySelector('.pip-player'); if (!player || !pipActive || pageOverlayHidden) return; const oldRect = player.getBoundingClientRect(); applyPipPlayerSize(player); const { width, height } = getScaledPipSize(); if (savedTop == null || savedLeft == null) { player.style.top = Math.max(0, window.innerHeight - height - 30) + 'px'; player.style.left = Math.max(0, window.innerWidth - width - 20) + 'px'; return; } const maxLeft = Math.max(0, window.innerWidth - width); const maxTop = Math.max(0, window.innerHeight - height); player.style.left = Math.min(Math.max(0, oldRect.left), maxLeft) + 'px'; player.style.top = Math.min(Math.max(0, oldRect.top), maxTop) + 'px'; } function norm(url) { try { return new URL(url, location.href).href; } catch { return url || ''; } } function same(a, b) { return !!(a && b) && norm(a) === norm(b); } function isPlayerUrl(url) { try { const u = new URL(url, location.href); if (u.protocol !== 'https:') return false; if (u.hostname === 'play.sooplive.com' || u.hostname === 'vod.sooplive.com') return true; return u.hostname === 'www.sooplive.com' && VOD_RE.test(u.pathname); } catch { return false; } } function shouldPipUrl(url) { try { const u = new URL(url, location.href); if (u.protocol !== 'https:' || u.hostname !== 'www.sooplive.com') return false; if (isPlayerUrl(u.href)) return false; if (u.hash && (u.hash === '#' || u.hash.startsWith('#javascript'))) return false; return PIP_PATHS.has(u.pathname) || u.pathname.startsWith('/directory/category/'); } catch { return false; } } function canReflectPanelUrl(url) { try { const u = new URL(url, location.href); if (u.protocol !== 'https:') return false; if (u.hostname !== 'www.sooplive.com') return false; if (isPlayerUrl(u.href)) return false; if (u.hash && (u.hash === '#' || u.hash.startsWith('#javascript'))) return false; return true; } catch { return false; } } function withReload(url) { try { const u = new URL(url, location.href); u.searchParams.set('_r', Date.now().toString(36)); return u.href; } catch { return url; } } function isMenuActionAnchor(a) { return !!a?.closest?.(MENU_IGNORE_SEL); } function getPlainLeftClickAnchor(e) { if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return null; return e.target.closest?.('a[href]') || null; } function isHandledPlayerAnchor(a) { return !!a && isPlayerUrl(a.href) && !a.hasAttribute('download') && !isMenuActionAnchor(a); } function saveExitRedirectUrl(url) { try { if (url && canReflectPanelUrl(url)) { sessionStorage.setItem(EXIT_REDIRECT_KEY, url); } } catch {} } function loadExitRedirectUrl() { try { const url = sessionStorage.getItem(EXIT_REDIRECT_KEY); return (url && canReflectPanelUrl(url)) ? url : null; } catch { return null; } } function armExitRedirect() { try { sessionStorage.setItem(EXIT_REDIRECT_ARMED, '1'); } catch {} } function disarmExitRedirect() { try { sessionStorage.removeItem(EXIT_REDIRECT_ARMED); } catch {} } function isExitRedirectArmed() { try { return sessionStorage.getItem(EXIT_REDIRECT_ARMED) === '1'; } catch { return false; } } function clearExitRedirectState() { try { sessionStorage.removeItem(EXIT_REDIRECT_KEY); sessionStorage.removeItem(EXIT_REDIRECT_ARMED); } catch {} } function nudgeBodyClick() { try { document.body?.click(); } catch {} setTimeout(() => { try { document.body?.click(); } catch {} }, 60); } function createPlayerGuardState() { let pendingClick = null; let pendingBlank = null; function markClick(anchor) { if (!anchor?.href || !isPlayerUrl(anchor.href)) return; if (isMenuActionAnchor(anchor)) return; pendingClick = { url: norm(anchor.href), ts: Date.now(), targetBlank: anchor.target === '_blank' }; } function clearClick() { pendingClick = null; } function getClick() { if (!pendingClick) return null; if (Date.now() - pendingClick.ts > CLICK_TTL) { pendingClick = null; return null; } return pendingClick; } function armBlank(anchor) { if (!anchor?.href || !isPlayerUrl(anchor.href) || anchor.target !== '_blank') return; if (isMenuActionAnchor(anchor)) return; pendingBlank = { url: norm(anchor.href), ts: Date.now(), handled: false }; } function getBlank() { if (!pendingBlank) return null; if (Date.now() - pendingBlank.ts > CLICK_TTL) { pendingBlank = null; return null; } return pendingBlank; } function clearBlank() { pendingBlank = null; } return { markClick, clearClick, getClick, armBlank, getBlank, clearBlank }; } let externalClickWatch = null; function beginExternalClickWatch(e, href, fallback, opts = {}) { const waitMs = opts.waitMs ?? 90; if (externalClickWatch) { clearTimeout(externalClickWatch.timer); externalClickWatch = null; } const startHref = location.href; const watch = { href: norm(href), startHref, handled: false, preventedInitially: !!e?.defaultPrevented, timer: null }; watch.timer = setTimeout(() => { if (externalClickWatch !== watch) return; externalClickWatch = null; if (watch.handled) return; if (e?.defaultPrevented && !watch.preventedInitially) return; if (location.href !== startHref) return; fallback(); }, waitMs); externalClickWatch = watch; } function markExternalClickHandled() { if (!externalClickWatch) return; externalClickWatch.handled = true; } function bindPlayerClickMarker(state, onAfterMark) { document.addEventListener('click', function (e) { const a = getPlainLeftClickAnchor(e); if (!a || !isPlayerUrl(a.href) || isMenuActionAnchor(a)) return; state.markClick(a); setTimeout(() => { const p = state.getClick(); if (p && same(p.url, a.href)) state.clearClick(); }, CLICK_TTL + 50); onAfterMark?.(a); }, true); } function bindBlankGuard(state, navigateFn) { document.addEventListener('click', function (e) { const a = getPlainLeftClickAnchor(e); if (!a || !isHandledPlayerAnchor(a) || a.target !== '_blank') return; state.armBlank(a); e.preventDefault(); setTimeout(() => { const g = state.getBlank(); if (!g) return; if (!same(g.url, a.href)) return; if (g.handled) { state.clearBlank(); return; } state.clearBlank(); state.clearClick(); navigateFn(a.href); }, 0); }, true); } function installOpenHook(w, realOpen, consumeFn, key = '__soopOpenHookInstalled') { if (!w || w[key]) return; w[key] = true; let _real = realOpen || w.open; function hooked(url, name, specs) { if (typeof url === 'string') { try { const consumed = consumeFn(url, name, specs); if (consumed) return FAKE_WIN; } catch {} } return _real.call(w, url, name, specs); } try { Object.defineProperty(w, 'open', { get() { return hooked; }, set(v) { _real = v; }, configurable: true, enumerable: true }); } catch { w.open = hooked; } } function installTopWwwPlayerGuards() { if (window.__soopTopWwwGuardsInstalled) return; window.__soopTopWwwGuardsInstalled = true; const state = createPlayerGuardState(); installOpenHook(window, originalOpen, (url) => { const pending = state.getClick(); if (!pending || !isPlayerUrl(url) || !same(pending.url, url)) return false; state.clearClick(); const g = state.getBlank(); if (g && same(g.url, url)) g.handled = true; location.href = norm(url); return true; }, '__soopTopWwwOpen_window'); if (uWin !== window) { installOpenHook(uWin, originalTargetOpen, (url) => { const pending = state.getClick(); if (!pending || !isPlayerUrl(url) || !same(pending.url, url)) return false; state.clearClick(); const g = state.getBlank(); if (g && same(g.url, url)) g.handled = true; location.href = norm(url); return true; }, '__soopTopWwwOpen_uwin'); } bindPlayerClickMarker(state); bindBlankGuard(state, (url) => { location.href = norm(url); }); } function installIframePlayerGuards() { if (window.__soopIframePlayerGuardsInstalled) return; window.__soopIframePlayerGuardsInstalled = true; const state = createPlayerGuardState(); let searchBodyClickTimer = null; let lastSent = ''; installOpenHook(window, window.open, (url) => { const pending = state.getClick(); if (!pending || !isPlayerUrl(url) || !same(pending.url, url)) return false; state.clearClick(); const g = state.getBlank(); if (g && same(g.url, url)) g.handled = true; window.parent.postMessage({ type: 'SOOP_NAV_PLAYER', url }, '*'); return true; }, '__soopIframeOpen_window'); if (uWin !== window) { installOpenHook(uWin, uWin.open, (url) => { const pending = state.getClick(); if (!pending || !isPlayerUrl(url) || !same(pending.url, url)) return false; state.clearClick(); const g = state.getBlank(); if (g && same(g.url, url)) g.handled = true; window.parent.postMessage({ type: 'SOOP_NAV_PLAYER', url }, '*'); return true; }, '__soopIframeOpen_uwin'); } bindPlayerClickMarker(state); bindBlankGuard(state, (url) => { window.parent.postMessage({ type: 'SOOP_NAV_PLAYER', url }, '*'); }); function sendInfo() { const url = location.href; const dark = document.documentElement.classList.contains('dark') || document.documentElement.dataset.theme === 'dark' || document.body?.classList.contains('dark'); window.parent.postMessage({ type: 'SOOP_THEME', dark }, '*'); if (url !== lastSent && canReflectPanelUrl(url)) { lastSent = url; window.parent.postMessage({ type: 'SOOP_PANEL_URL', url }, '*'); try { const u = new URL(url); if (u.hostname === 'www.sooplive.com' && u.pathname === '/search') { clearTimeout(searchBodyClickTimer); searchBodyClickTimer = setTimeout(() => { window.parent.postMessage({ type: 'SOOP_PARENT_BODY_CLICK' }, '*'); }, 220); } } catch {} } try { window.parent.postMessage({ type: 'SOOP_IFRAME_URL', url }, '*'); } catch {} } window.addEventListener('load', sendInfo); window.addEventListener('popstate', () => setTimeout(sendInfo, 0)); ['pushState', 'replaceState'].forEach(method => { const orig = history[method]; history[method] = function () { const r = orig.apply(this, arguments); setTimeout(sendInfo, 0); return r; }; }); if (window.navigation && !window.__soopIframeNavHooked) { window.__soopIframeNavHooked = true; window.navigation.addEventListener('navigate', function (e) { const url = e.destination?.url; if (url && isPlayerUrl(url)) { e.preventDefault(); window.parent.postMessage({ type: 'SOOP_NAV_PLAYER', url }, '*'); } }); } document.addEventListener('mousedown', function (e) { if (!e.target.closest('a, button, input, textarea, select, [role="button"]')) { setTimeout(() => window.parent.postMessage({ type: 'SOOP_PARENT_BODY_CLICK' }, '*'), 0); } }, true); } if (!isPlayerDomain) { if (location.hostname !== 'www.sooplive.com') return; if (window.self === window.top) { installTopWwwPlayerGuards(); return; } { const style = document.createElement('style'); let css = ` *{text-rendering:optimizeSpeed!important} video:not([controls]):not([src^="blob:"]),.thumbs_box .thumb,.broad_thumb,iframe[title*="광고"]{display:none!important} [class*="preview"] video,[class*="Preview"] video,[class*="modal"] video{display:block!important} .thumbs_box{contain:layout paint} body{overflow-x:hidden} `; if (location.href.includes('/my/favorite')) { css += ` div[class*="list_wrap"],ul{ content-visibility:visible!important; contain:none!important } .thumbs_box li,.list_wrap li,.cBox{ content-visibility:auto; contain-intrinsic-size:300px; contain:layout paint style } `; } style.textContent = css; (document.head || document.documentElement).appendChild(style); } (function installIframeNetworkThrottler() { const Q = []; const D = 80; let busy = false; async function run() { if (busy || !Q.length) return; busy = true; while (Q.length) { await Q.shift()(); await new Promise(r => setTimeout(r, D)); } busy = false; } const isImage = u => /\.(jpg|jpeg|png|gif|webp|svg)/i.test(u); const isTargetApi = u => /\/api\/|station|list/.test(u); const origFetch = window.fetch; window.fetch = async function (...args) { const url = args[0]?.toString() || ''; if (isImage(url) || !isTargetApi(url)) return origFetch(...args); return new Promise((resolve, reject) => { Q.push(async () => { try { resolve(await origFetch(...args)); } catch (err) { reject(err); } }); run(); }); }; const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this._url = url; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { const url = this._url || ''; if (isImage(url) || !isTargetApi(url)) return origSend.call(this, body); Q.push(() => new Promise(resolve => { this.addEventListener('loadend', resolve, { once: true }); origSend.call(this, body); setTimeout(resolve, 1200); })); run(); }; })(); installIframePlayerGuards(); return; } if (window.self !== window.top) { let parentIsSoop = false; try { parentIsSoop = window.parent.location.hostname.endsWith('sooplive.com'); } catch { parentIsSoop = false; } if (!parentIsSoop) return; } const bootExitUrl = loadExitRedirectUrl(); const bootExitArmed = isExitRedirectArmed(); let isRealReload = false; try { const navEntry = performance.getEntriesByType('navigation')[0]; isRealReload = navEntry?.type === 'reload'; } catch {} if (!isRealReload) { try { isRealReload = performance.navigation?.type === 1; } catch {} } if (bootExitUrl && bootExitArmed && isRealReload) { clearExitRedirectState(); location.replace(bootExitUrl); return; } clearExitRedirectState(); let panelUrl = ''; let lastPipUrl = null; let pipActive = false; let pipClosedMode = false; let pageOverlayHidden = false; let bypass = false; let savedTop = null; let savedLeft = null; let _pa = null, _paT = 0; let resumeTimer = null; let keepTimer = null; let initObserver = null; let initScheduled = false; let pendingRouteTimer = null; const PIP_PLAYER_RATIO = 0.46; const PIP_MIN_W = 300; const PIP_MAX_W = 520; function getPipLayoutWidth() { const frame = getFrame(); if (frame) { const r = frame.getBoundingClientRect(); if (r.width > 100) return r.width; } return document.documentElement.clientWidth || window.innerWidth || 1280; } function getScaledPipSize() { const baseWidth = getPipLayoutWidth(); let width = Math.round(baseWidth * PIP_PLAYER_RATIO); width = Math.max(PIP_MIN_W, Math.min(PIP_MAX_W, width)); const height = Math.round(width * 9 / 16); return { width, height }; } function applyPipPlayerSize(player) { if (!player) return; const { width, height } = getScaledPipSize(); player.style.width = width + 'px'; player.style.height = height + 'px'; } const playerPageState = createPlayerGuardState(); function getPlayer() { const now = Date.now(); if (_pa && now - _paT < 2000 && _pa.isConnected) return _pa; _paT = now; _pa = document.querySelector(P_ID) || document.querySelector(P_CLS); if (_pa) return _pa; const v = document.querySelector('video'); if (!v) return null; _pa = v.closest('[id*="player" i],[class*="player" i],[id*="vod" i],[class*="vod_" i]'); if (_pa) return _pa; let p = v.parentElement; while (p && p !== document.body) { const s = getComputedStyle(p).position; if (s === 'relative' || s === 'absolute' || s === 'fixed') { _pa = p; return _pa; } p = p.parentElement; } _pa = v.parentElement; return _pa; } function findVideo() { const p = getPlayer(); return p ? p.querySelector('video') : document.querySelector('video'); } function setLastUrl(url) { if (!canReflectPanelUrl(url)) return; lastPipUrl = url; } function getResumeUrl() { if (lastPipUrl && canReflectPanelUrl(lastPipUrl)) return lastPipUrl; return null; } function reflectPanelUrlToHash(url) { try { if (!url || !canReflectPanelUrl(url)) return; const currentBase = location.pathname + location.search; history.replaceState({ soopPanelUrl: url }, '', currentBase + '#pipurl=' + url); } catch {} } function nav(url, skipSameCheck) { if (!url) return; const t = norm(url); if (!skipSameCheck && t === norm(location.href)) return; disarmExitRedirect(); bypass = true; location.href = t; } function getFrame(create) { let f = document.getElementById('soop-pip-frame'); if (f || !create) return f; f = document.createElement('iframe'); f.id = 'soop-pip-frame'; f.loading = 'eager'; f.referrerPolicy = 'strict-origin-when-cross-origin'; document.body.appendChild(f); return f; } function setFramePointer(on) { const f = getFrame(); if (f) f.style.pointerEvents = on ? 'auto' : 'none'; } function destroyFrame() { const f = getFrame(); if (f) f.remove(); } function cancelKeep() { if (keepTimer) { clearTimeout(keepTimer); keepTimer = null; } } function scheduleKeep() { cancelKeep(); keepTimer = setTimeout(() => { if (!pipActive && !pipClosedMode) destroyFrame(); keepTimer = null; }, KEEP_MS); } function navPanel(url, reload) { if (!canReflectPanelUrl(url)) return; const f = getFrame(); if (!f) return; const isSame = same(panelUrl, url); panelUrl = url; setLastUrl(url); reflectPanelUrlToHash(url); f.src = (isSame || reload) ? withReload(url) : url; } function cleanPipDom(player) { if (!player) return; player.querySelector('#pip-bar')?.remove(); player.onmousedown = null; } function applyOverlayHidden() { document.documentElement.classList.toggle('soop-pip-overlay-hidden', !!pageOverlayHidden); document.body?.classList.toggle('soop-pip-overlay-hidden', !!pageOverlayHidden); const player = getPlayer(); if (!player) return; if (pageOverlayHidden) { player.classList.remove('pip-player'); player.classList.add('pip-player-hidden'); cleanPipDom(player); player.style.top = ''; player.style.left = ''; } else if (document.body?.classList.contains('pip-mode')) { player.classList.remove('pip-player-hidden'); player.classList.add('pip-player'); buildControls(player); makeDrag(player); } } function mkBtn(html, title, color, fn) { const b = document.createElement('button'); b.className = 'pip-btn'; b.innerHTML = html; b.title = title; if (color) b.style.color = color; b.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); fn(); }); return b; } function buildControls(player) { if (player.querySelector('#pip-bar')) return; const bar = document.createElement('div'); bar.id = 'pip-bar'; bar.append( mkBtn('⤢', '복귀', null, stopPip), mkBtn('✖', '종료', null, exitPip) ); player.appendChild(bar); } function makeDrag(el) { let sx, sy, il, it, raf; el.onmousedown = e => { if (!pipActive || pageOverlayHidden) return; if (e.target.closest('button,[role="button"],input,.play_control_box')) return; e.preventDefault(); el.classList.add('dragging'); sx = e.clientX; sy = e.clientY; il = el.offsetLeft; it = el.offsetTop; document.onmouseup = () => { document.onmouseup = document.onmousemove = null; cancelAnimationFrame(raf); el.classList.remove('dragging'); }; document.onmousemove = de => { cancelAnimationFrame(raf); raf = requestAnimationFrame(() => { el.style.top = (it + de.clientY - sy) + 'px'; el.style.left = (il + de.clientX - sx) + 'px'; }); }; }; } function stopPlayerMedia() { try { if (location.hostname === 'vod.sooplive.com') { const ctrlBox = document.querySelector('.ctrlBox'); if (ctrlBox) { const ctrlBtn = ctrlBox.querySelector('button.pause'); if (ctrlBtn) { ctrlBtn.click(); return true; } if (ctrlBox.querySelector('button.play')) return false; } const buttons = [...document.querySelectorAll('button.play, button.pause')]; const btn = buttons.find(b => { if (b.classList.contains('prev') || b.classList.contains('next')) return false; const text = b.querySelector('.tooltip span')?.textContent?.trim() || ''; return text === '재생' || text === '일시정지'; }); if (!btn) return false; if (btn.classList.contains('play')) return false; if (btn.classList.contains('pause')) { btn.click(); return true; } return false; } if (location.hostname === 'play.sooplive.com') { const btn = document.querySelector('#play'); if (!btn) return false; if (btn.classList.contains('play')) return false; if (btn.classList.contains('stop')) { btn.click(); return true; } return false; } } catch {} return false; } function startPip(url, reload) { pageOverlayHidden = false; pipClosedMode = false; clearExitRedirectState(); const player = getPlayer(); if (!player) { if (url) nav(url, true); return; } cancelKeep(); pipActive = true; bypass = false; const f = getFrame(true); const target = url || getResumeUrl(); if (target) { const currentSrc = f.src || ''; if (!same(currentSrc, target) || reload) { navPanel(target, reload || same(panelUrl, target)); } else { panelUrl = target; setLastUrl(target); reflectPanelUrlToHash(target); } } document.body.classList.add('pip-mode'); player.classList.remove('pip-player-hidden'); player.classList.add('pip-player'); rememberCatchPlayerSize(player); applyPipPlayerSize(player); const { width, height } = getScaledPipSize(); if (savedTop !== null && savedLeft !== null) { player.style.top = savedTop; player.style.left = savedLeft; } else { player.style.top = Math.max(0, window.innerHeight - height - 30) + 'px'; player.style.left = Math.max(0, window.innerWidth - width - 20) + 'px'; } cleanPipDom(player); buildControls(player); makeDrag(player); setFramePointer(true); applyOverlayHidden(); updateResume(); } function stopPip() { pageOverlayHidden = false; clearExitRedirectState(); const player = document.querySelector('.pip-player, .pip-player-hidden'); if (player) { savedTop = player.style.top; savedLeft = player.style.left; player.classList.remove('pip-player'); player.classList.remove('pip-player-hidden'); player.style.top = ''; player.style.left = ''; if (isCatchVodPlayer(player)) { restoreCatchPlayerSize(player); } else { player.style.width = ''; player.style.height = ''; } cleanPipDom(player); } pipActive = false; document.body.classList.remove('pip-mode'); document.body.classList.remove('soop-pip-overlay-hidden'); document.documentElement.classList.remove('soop-pip-overlay-hidden'); setFramePointer(false); scheduleKeep(); updateResume(); } function exitPip() { if (!pipActive) return; pipActive = false; pipClosedMode = true; pageOverlayHidden = true; setTimeout(() => stopPlayerMedia(), 180); if (panelUrl) { saveExitRedirectUrl(panelUrl); armExitRedirect(); reflectPanelUrlToHash(panelUrl); } applyOverlayHidden(); const f = getFrame(); if (f) { f.style.visibility = 'visible'; f.style.pointerEvents = 'auto'; f.style.zIndex = '100'; } updateResume(); } function route(url, opts = {}) { if (!url || bypass || !shouldPipUrl(url)) return false; if (pipClosedMode) { nav(url, true); return true; } setLastUrl(url); const doPanelRoute = () => { if (pipActive) { pageOverlayHidden = false; applyOverlayHidden(); navPanel(url, opts.reload || same(panelUrl, url)); } else { startPip(url, opts.reload); } }; if (getPlayer()) { doPanelRoute(); return true; } if (pendingRouteTimer) { clearTimeout(pendingRouteTimer); pendingRouteTimer = null; } let tries = 0; const retry = () => { if (getPlayer()) { pendingRouteTimer = null; doPanelRoute(); return; } tries += 1; if (tries < 4) { pendingRouteTimer = setTimeout(retry, 80); return; } pendingRouteTimer = null; nav(url, true); }; pendingRouteTimer = setTimeout(retry, 60); return true; } function getDropdownUrl(li) { const id = li.querySelector('button.thumb img')?.alt?.trim(); if (!id) return null; return li.classList.contains('live') ? 'https://play.sooplive.com/' + id : 'https://www.sooplive.com/station/' + id; } function getDropdownKw(li) { if (li.classList.contains('tag_result')) { const t = li.querySelector('.hash_result')?.textContent?.trim(); if (t) return t; } const span = li.querySelector('span:not(.certify):not(.live_cnt):not([class*="ic"])'); if (span?.textContent?.trim()) return span.textContent.trim(); const a = li.querySelector('a'); if (a) { const c = a.cloneNode(true); c.querySelectorAll('.num,i,.btn_delete,.ic_chain,.related_search,em').forEach(x => x.remove()); if (c.textContent?.trim()) return c.textContent.trim(); } return null; } function searchUrl(kw) { return 'https://www.sooplive.com/search?szLocation=total_search&szSearchType=total' + '&szKeyword=' + encodeURIComponent(kw) + '&szStype=di&szActype=input_field'; } function installPlayerOpenHook(w, realOpen) { installOpenHook(w, realOpen, (url) => { if (externalClickWatch) { markExternalClickHandled(); } if (shouldPipUrl(url)) { if (pipClosedMode) nav(url, true); else route(url, { reload: same(panelUrl, url) }); return true; } const pending = playerPageState.getClick(); if (!pending || !isPlayerUrl(url) || !same(pending.url, url)) return false; playerPageState.clearClick(); const g = playerPageState.getBlank(); if (g && same(g.url, url)) g.handled = true; nav(url, true); return true; }); } installPlayerOpenHook(window, originalOpen); if (uWin !== window) installPlayerOpenHook(uWin, originalTargetOpen); function toggleResume(show) { const b = document.getElementById('pip-resume'); if (!b) return; clearTimeout(resumeTimer); if (show && b.classList.contains('can') && !pipActive) { b.classList.add('show'); resumeTimer = setTimeout(() => b?.classList.remove('show'), 3000); } else { b.classList.remove('show'); } } function updateResume() { const b = document.getElementById('pip-resume'); if (!b) return; const hasFrame = !!getFrame(); const ok = (hasFrame || !!getResumeUrl() || !!panelUrl) && !pipActive && !pipClosedMode; b.classList.toggle('can', ok); if (!ok) b.classList.remove('show'); } function doResume() { const url = getResumeUrl() || panelUrl; if (!url) return; toggleResume(false); if (document.fullscreenElement) { document.exitFullscreen?.(); setTimeout(() => startPip(url, false), 350); } else { startPip(url, false); } } function overVideo(e) { const el = document.elementFromPoint(e.clientX, e.clientY); if (el?.id === 'pip-resume' || el?.closest('#pip-resume')) return true; const v = findVideo(); if (!v) return false; const r = v.getBoundingClientRect(); return r.width > 10 && e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom; } function ensureResume() { if (document.getElementById('pip-resume')) return; const player = getPlayer(); if (!player) return; const pos = getComputedStyle(player).position; if (pos === 'static' || !pos) player.style.position = 'relative'; const b = document.createElement('button'); b.id = 'pip-resume'; b.className = 'pip-btn'; b.innerHTML = '↩'; b.title = '현재 세션의 이전 탐색 페이지로 복귀 (PIP)'; b.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); doResume(); }); b.addEventListener('mouseenter', () => clearTimeout(resumeTimer)); b.addEventListener('mouseleave', () => { if (b.classList.contains('show')) { resumeTimer = setTimeout(() => b.classList.remove('show'), 3000); } }); player.appendChild(b); updateResume(); } function scheduleEnsureResume() { if (initScheduled) return; initScheduled = true; requestAnimationFrame(() => { initScheduled = false; ensureResume(); if (pipActive || pageOverlayHidden) applyOverlayHidden(); }); } bindPlayerClickMarker(playerPageState, () => { disarmExitRedirect(); }); bindBlankGuard(playerPageState, (url) => { nav(url, true); }); document.addEventListener('click', function (e) { if (e.defaultPrevented || bypass) return; if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; const li = e.target.closest(DROPDOWN_SEL); if (li && e.target.closest('button.thumb')) { disarmExitRedirect(); const url = getDropdownUrl(li); if (!url) return; if (li.classList.contains('live') || isPlayerUrl(url)) { e.preventDefault(); e.stopPropagation(); nav(url, true); return; } return; } const a = e.target.closest('a[href]'); if (!a || a.hasAttribute('download')) return; disarmExitRedirect(); const href = a.href; if (!href) return; if (isPlayerUrl(href)) { if (isMenuActionAnchor(a)) return; beginExternalClickWatch(e, href, () => { nav(href, true); }); return; } if (!shouldPipUrl(href)) return; if (a.closest('#hashtag') && a.target === '_blank') { e.preventDefault(); e.stopPropagation(); if (pipClosedMode) { nav(href, true); } else { route(href, { reload: same(panelUrl, href) }); } return; } if (a.closest('#pip-bar,.play_control_box,#hashtag')) return; e.preventDefault(); e.stopPropagation(); if (pipClosedMode) { nav(href, true); return; } route(href, { reload: same(panelUrl, href) }); route(href, { reload: same(panelUrl, href) }); }, true); document.addEventListener('auxclick', function (e) { if (e.button !== 1) return; const li = e.target.closest(DROPDOWN_SEL); if (!li) return; disarmExitRedirect(); e.preventDefault(); e.stopPropagation(); const u = e.target.closest('button.thumb') ? getDropdownUrl(li) : (() => { const kw = getDropdownKw(li); return kw ? searchUrl(kw) : null; })(); if (u) originalOpen.call(window, u, '_blank'); }, true); document.addEventListener('dblclick', function (e) { if (!pipActive || pageOverlayHidden) return; const player = document.querySelector('.pip-player'); if (!player?.contains(e.target)) return; if (e.target.closest('#pip-bar,.play_control_box')) return; e.preventDefault(); e.stopPropagation(); stopPip(); setTimeout(() => { const t = player.querySelector('video') || player; (t.requestFullscreen || t.webkitRequestFullscreen)?.call(t); }, 150); }, true); window.addEventListener('message', e => { if (!e.data) return; const { type, dark, url } = e.data; switch (type) { case 'SOOP_THEME': document.body.classList.toggle('iframe-dark', dark); break; case 'SOOP_PANEL_URL': if (canReflectPanelUrl(url)) { panelUrl = url; setLastUrl(url); reflectPanelUrlToHash(url); } break; case 'SOOP_IFRAME_URL': if (pipClosedMode && url && canReflectPanelUrl(url) && !same(location.href, url)) { nav(url, true); } break; case 'SOOP_NAV_PLAYER': markExternalClickHandled(); if (url) nav(url, true); break; case 'SOOP_PARENT_BODY_CLICK': nudgeBodyClick(); break; } }, { passive: true }); const init = () => { ensureResume(); applyOverlayHidden(); setTimeout(scheduleEnsureResume, 500); setTimeout(scheduleEnsureResume, 1500); if (!document.__soopHover) { document.__soopHover = true; document.addEventListener('mousemove', function (e) { if (!pipActive) overVideo(e) ? toggleResume(true) : toggleResume(false); }, { passive: true }); } if (!window.__soopPipResizeBound) { window.__soopPipResizeBound = true; window.addEventListener('resize', refreshPipPlayerSize, { passive: true }); } if (!initObserver && document.body) { initObserver = new MutationObserver(() => { const resume = document.getElementById('pip-resume'); const player = getPlayer(); if (resume && player && player.contains(resume)) { initObserver.disconnect(); initObserver = null; return; } if (!resume && player) { scheduleEnsureResume(); } }); initObserver.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { initObserver?.disconnect(); initObserver = null; }, 3000); } }; if (document.body) init(); else document.addEventListener('DOMContentLoaded', init); GM_addStyle(` body.pip-mode .pip-player{ position:fixed!important; z-index:999999!important; bottom:auto!important; right:auto!important; border:none!important; box-shadow:0 4px 20px rgba(0,0,0,.7); background:#000; border-radius:8px; overflow:hidden; cursor:move!important; contain:strict!important; } body.pip-mode .pip-player.dragging{ transition:none!important; box-shadow:0 2px 10px rgba(0,0,0,.5); } body.pip-mode .pip-player video{ pointer-events:none!important; object-fit:contain!important; width:100%!important; height:100%!important; position:relative!important; z-index:5!important; } body.pip-mode .pip-player .player_cover{ pointer-events:none!important; width:100%!important; height:100%!important; position:relative!important; z-index:4!important; } body.pip-mode .pip-player .play_control_box{ display:block!important; pointer-events:auto!important; bottom:0!important; position:absolute!important; width:100%!important; background:linear-gradient(to top,rgba(0,0,0,.7),transparent); z-index:20!important; cursor:default!important; } body.pip-mode .pip-player .play_control_box *{ pointer-events:auto!important; } body.pip-mode #web_chatting, body.pip-mode .header_area, body.pip-mode .sidebar_area, body.pip-mode .start_ad_area, body.pip-mode #action_bar, body.pip-mode .btn_chat_open, body.pip-mode .btn_chat_fold, body.pip-mode .btn_expand, body.pip-mode button[class*="chat"], body.pip-mode .chat_layer, body.pip-mode .btn_sidebar, body.pip-mode .chat-icon.trash-icon.trash, body.pip-mode .chat-icon.highlight-icon.highlight{ display:none!important; } #soop-pip-frame{ position:fixed; top:0; left:0; width:100%; height:100%; border:none; background:#fff; visibility:hidden; pointer-events:none; z-index:-1; transform:translateZ(0); } body.pip-mode #soop-pip-frame{ visibility:visible; pointer-events:auto; z-index:100; } body.pip-mode.iframe-dark #soop-pip-frame{ background:#141517; } body:not(.pip-mode) #soop-pip-frame{ visibility:hidden; pointer-events:none; z-index:-1; } #pip-bar{ display:none; position:absolute; top:0; left:0; width:100%; height:48px; background:linear-gradient(to bottom,rgba(0,0,0,.5),transparent); z-index:1000000; justify-content:space-between; align-items:flex-start; padding:5px 8px; opacity:0; transition:opacity .15s; pointer-events:auto!important; cursor:default; } body.pip-mode .pip-player:hover #pip-bar{ opacity:1; } body.pip-mode #pip-bar{ display:flex; } .pip-btn{ background:rgba(0,0,0,.45); border:1px solid rgba(255,255,255,.2); color:#fff; width:40px; height:40px; border-radius:50%; cursor:pointer; font-size:25px; line-height:34px; text-align:center; padding:0; margin:0; flex-shrink:0; } .pip-btn:hover{ background:rgba(255,255,255,.25); } #pip-resume{ position:absolute; top:10px; left:10px; z-index:999999; opacity:0; pointer-events:none; transition:opacity .2s; width:47px; height:47px; font-size:23px; line-height:45px; } #pip-resume.can.show{ opacity:.75; pointer-events:auto; } #pip-resume.can.show:hover{ opacity:1; } body.pip-mode #pip-resume{ display:none!important; } body.soop-pip-overlay-hidden .pip-player, body.soop-pip-overlay-hidden #pip-bar{ display:none!important; } .pip-player-hidden{ display:none!important; } `); })();