// ==UserScript== // @name 加强糯米! // @namespace https://www.nuomill.com // @version 1.3 // @description 糯米洛洛网站增强 // @author Sunse666 // @match https://www.nuomill.com/* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; // Workaround: /user/* pages crash on full page load — APIs return 200 but // reactive data rendering triggers a Nuxt error page. Client-side navigation // (e.g. clicking avatar from a video page) works fine, so redirect to home // and navigate back via Vue Router. (function () { var match = location.pathname.match(/^\/user\/(\d+)/); if (match) { try { sessionStorage.setItem('nml_user_redirect', match[1]); } catch (e) {} location.replace('/'); return; } if (location.pathname === '/' || location.pathname === '') { var userId; try { userId = sessionStorage.getItem('nml_user_redirect'); } catch (e) {} if (userId) { try { sessionStorage.removeItem('nml_user_redirect'); } catch (e) {} var tries = 0; (function tryNav() { var router; try { var app = document.getElementById('__nuxt').__vue_app__; router = app.config.globalProperties.$router; } catch (e) {} if (router) { router.push('/user/' + userId); } else if (tries++ < 50) { setTimeout(tryNav, 200); } })(); } } })(); const SPEEDS = [0.25, 0.5, 1.0, 1.25, 1.5, 2.0]; const DBLCLICK_THRESHOLD = 320; const VOLUME_KEY = 'nml_saved_volume'; const SPEED_KEY = 'nml_saved_speed'; let savedSpeed = parseFloat(localStorage.getItem(SPEED_KEY)); if (isNaN(savedSpeed) || !SPEEDS.includes(savedSpeed)) savedSpeed = 1.0; let currentSpeed = savedSpeed; let savedVolume = parseFloat(localStorage.getItem(VOLUME_KEY)); if (isNaN(savedVolume) || savedVolume < 0 || savedVolume > 1) savedVolume = null; let videoEl = null; let speedBtn = null; let speedMenu = null; let toastEl = null; let mounted = false; // Double-click fullscreen // Intercept clicks in capture phase to avoid pause/resume flicker. // Single click: manually toggle play/pause after a short delay. // Double click: toggle fullscreen, keep play state unchanged. let clickTimer = null; let lastClickTime = 0; function setupDoubleClick(video) { if (!video || video.dataset.nmlDblclick) return; video.dataset.nmlDblclick = '1'; video.addEventListener('click', e => { const now = Date.now(); const elapsed = now - lastClickTime; lastClickTime = now; // Block ALL clicks from reaching the player — we'll handle them ourselves e.stopPropagation(); e.stopImmediatePropagation(); if (elapsed < DBLCLICK_THRESHOLD && elapsed > 0) { // Second click within threshold → double-click clearTimeout(clickTimer); lastClickTime = 0; toggleFullscreen(); return; } // First click — wait to see if a second click follows clearTimeout(clickTimer); clickTimer = setTimeout(() => { // No second click → it was a single click → toggle play/pause lastClickTime = 0; const v = findVideo(); if (v) { v.paused ? v.play() : v.pause(); } }, DBLCLICK_THRESHOLD); }, true); // capture phase — fires BEFORE the player's own click handler } function toggleFullscreen() { // Use the same element the player's fullscreen button uses: .player-wrapper const wrapper = document.querySelector('.player-wrapper'); if (!wrapper) return; if (document.fullscreenElement || document.webkitFullscreenElement) { (document.exitFullscreen || document.webkitExitFullscreen).call(document); } else { (wrapper.requestFullscreen || wrapper.webkitRequestFullscreen).call(wrapper); } showToast(currentSpeed); // brief feedback that fullscreen toggled } // Volume memory function saveVolume(vol) { savedVolume = vol; try { localStorage.setItem(VOLUME_KEY, String(vol)); } catch (e) {} } function applySavedVolume(video) { if (savedVolume == null) return; video.volume = savedVolume; video.muted = savedVolume === 0; // Also sync the slider const slider = document.querySelector('.custom-video-player .volume-slider'); if (slider) { slider.value = savedVolume; slider.dispatchEvent(new Event('input', { bubbles: true })); } } // Video tracking function findVideo() { const player = document.querySelector('.custom-video-player'); if (!player) return null; const v = player.querySelector('video'); if (v && v !== videoEl) { videoEl = v; v.playbackRate = currentSpeed; applySavedVolume(v); setupDoubleClick(v); watchVideo(v); } return v; } function watchVideo(video) { // Apply saved volume at the earliest opportunity (loadstart fires before loadedmetadata) video.addEventListener('loadstart', () => { applySavedVolume(video); setTimeout(() => { applySavedVolume(video); if (video.playbackRate !== currentSpeed) { video.playbackRate = currentSpeed; } }, 60); }); video.addEventListener('loadedmetadata', () => { applySavedVolume(video); if (video.playbackRate !== currentSpeed) { video.playbackRate = currentSpeed; } }); // Save volume whenever user changes it via the player UI video.addEventListener('volumechange', () => { if (!video.muted && video.volume > 0) { saveVolume(video.volume); } }); } // Observe DOM for video element const videoObserver = new MutationObserver(() => { findVideo(); }); function startVideoObserver() { const player = document.querySelector('.custom-video-player'); if (player) { videoObserver.observe(player, { childList: true, subtree: true }); } findVideo(); } // Speed control function applySpeed(speed) { currentSpeed = speed; const v = findVideo() || document.querySelector('video'); if (v) { v.playbackRate = speed; requestAnimationFrame(() => { if (v.playbackRate !== speed) v.playbackRate = speed; }); } syncDanmaku(speed); updateUI(); showToast(speed); savedSpeed = speed; try { localStorage.setItem(SPEED_KEY, String(speed)); } catch (e) {} } function syncDanmaku(speed) { try { const app = document.getElementById('__nuxt'); if (!app?.__vue_app__) return; walkVue(app.__vue_app__._instance?.vnode, comp => { if (comp.props && 'speed' in comp.props) { comp.props.speed = 100 * speed; } }); } catch (e) {} } function walkVue(vnode, fn) { if (!vnode) return; if (vnode.component) { fn(vnode.component); if (vnode.component.subTree) walkVue(vnode.component.subTree, fn); } if (Array.isArray(vnode.children)) vnode.children.forEach(c => walkVue(c, fn)); if (vnode.dynamicChildren) vnode.dynamicChildren.forEach(c => walkVue(c, fn)); } // Toast let toastTimer = null; function showToast(speed) { if (!toastEl) { toastEl = document.createElement('div'); toastEl.style.cssText = 'position:absolute;top:12px;left:50%;transform:translateX(-50%);' + 'z-index:10000;background:rgba(0,0,0,0.85);color:#fff;' + 'font-size:15px;font-weight:600;padding:6px 16px;border-radius:6px;' + 'pointer-events:none;opacity:0;transition:opacity 0.2s;' + 'font-family:-apple-system,BlinkMacSystemFont,sans-serif;' + 'white-space:nowrap;letter-spacing:0.5px;'; } if (!toastEl.parentNode) { const vc = document.querySelector('.custom-video-player'); if (vc) vc.appendChild(toastEl); } if (!toastEl.parentNode) return; toastEl.textContent = speed === 1.0 ? '1×' : speed + '×'; toastEl.style.opacity = '1'; clearTimeout(toastTimer); toastTimer = setTimeout(() => { toastEl.style.opacity = '0'; }, 700); } // Update UI function updateUI() { if (speedBtn) { speedBtn.textContent = currentSpeed === 1.0 ? '倍速' : currentSpeed + '×'; } if (speedMenu) { const resOpt = document.querySelector('.resolution-option'); const defaultColor = resOpt ? getComputedStyle(resOpt).color : '#ccc'; speedMenu.querySelectorAll('.nml-speed-opt').forEach(opt => { const s = parseFloat(opt.textContent); opt.style.color = s === currentSpeed ? '#ff6fae' : defaultColor; opt.style.fontWeight = s === currentSpeed ? '600' : ''; }); } } // Build UI function createSpeedSelector() { const controlBtn = document.querySelector('.custom-video-player .control-btn'); if (!controlBtn) return null; const wrapper = document.createElement('div'); wrapper.setAttribute('data-v-65d53e0f', ''); wrapper.style.cssText = 'position:relative;display:inline-flex;align-items:center;'; speedBtn = controlBtn.cloneNode(true); while (speedBtn.firstChild) speedBtn.removeChild(speedBtn.firstChild); speedBtn.textContent = '倍速'; speedBtn.removeAttribute('title'); speedBtn.removeAttribute('aria-label'); speedBtn.addEventListener('mousedown', handleSpeedBtnMouseDown); speedBtn.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); }); wrapper.appendChild(speedBtn); // Menu speedMenu = document.createElement('div'); speedMenu.setAttribute('data-v-65d53e0f', ''); speedMenu.style.cssText = 'display:none;position:absolute;bottom:100%;right:0;margin-bottom:6px;' + 'background:rgba(28,28,28,0.96);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);' + 'border:1px solid rgba(255,255,255,0.12);border-radius:8px;' + 'padding:4px;min-width:82px;z-index:10001;' + 'box-shadow:0 4px 24px rgba(0,0,0,0.5);'; const resMenu = document.querySelector('.resolution-menu'); const resOpt = resMenu?.querySelector('.resolution-option'); SPEEDS.forEach(s => { const opt = document.createElement('div'); opt.className = 'nml-speed-opt'; opt.setAttribute('data-v-65d53e0f', ''); if (resOpt) { const cs = getComputedStyle(resOpt); opt.style.cssText = cs.cssText; } else { opt.style.cssText = 'padding:8px 14px;border-radius:6px;font-size:13px;' + 'text-align:center;cursor:pointer;white-space:nowrap;' + 'transition:background 0.15s,color 0.15s;color:#ccc;'; } if (s === currentSpeed) { opt.style.color = '#ff6fae'; opt.style.fontWeight = '600'; } opt.textContent = s + '×'; opt.addEventListener('mousedown', e => { e.stopPropagation(); e.preventDefault(); applySpeed(s); speedMenu.style.display = 'none'; }); opt.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); }); opt.addEventListener('mouseenter', function () { this.style.background = 'rgba(255,255,255,0.1)'; this.style.color = '#fff'; }); opt.addEventListener('mouseleave', function () { this.style.background = ''; const defColor = resOpt ? getComputedStyle(resOpt).color : '#ccc'; this.style.color = parseFloat(this.textContent) === currentSpeed ? '#ff6fae' : defColor; }); speedMenu.appendChild(opt); }); wrapper.appendChild(speedMenu); // Combined outside-click-to-close for both menus document.addEventListener('mousedown', closeMenusOnOutside, true); document.addEventListener('touchstart', closeMenusOnOutside, true); return wrapper; } function closeMenusOnOutside(e) { // Close speed menu if (speedMenu && speedMenu.style.display !== 'none') { const sw = speedMenu.parentNode; if (sw && !sw.contains(e.target)) { speedMenu.style.display = 'none'; } } // Close resolution menu — click its button to toggle Vue's M.value const resMenu = document.querySelector('.resolution-menu'); if (resMenu) { const resBtn = document.querySelector('.resolution-selector .control-btn'); if (resBtn && !resBtn.contains(e.target) && !resMenu.contains(e.target)) { resBtn.click(); } } } function handleSpeedBtnMouseDown(e) { e.stopPropagation(); e.preventDefault(); if (!speedMenu) return; // Close resolution menu before opening speed menu const resBtn = document.querySelector('.resolution-selector .control-btn'); const resMenu = document.querySelector('.resolution-menu'); if (resMenu && resBtn) { resBtn.click(); } const isHidden = speedMenu.style.display === 'none'; speedMenu.style.display = isHidden ? '' : 'none'; } // Mount function mountUI() { if (mounted) return true; const resSelector = document.querySelector('.resolution-selector'); const danmakuToggle = document.querySelector('.danmaku-toggle-btn'); const volControl = document.querySelector('.volume-control'); let insertAfter = resSelector; if (!insertAfter) insertAfter = danmakuToggle; if (!insertAfter && volControl) { insertAfter = volControl.previousElementSibling; } if (!insertAfter || !insertAfter.parentNode) return false; const speedSelector = createSpeedSelector(); if (!speedSelector) return false; insertAfter.parentNode.insertBefore(speedSelector, insertAfter.nextSibling); updateUI(); startVideoObserver(); mounted = true; return true; } // Keyboard shortcuts document.addEventListener('keydown', e => { const tag = document.activeElement?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; // Only intercept keys when video is present on the page if (!findVideo() && !document.querySelector('.custom-video-player video')) return; // Vim-style scrolling: j k h l if (e.key === 'j') { window.scrollBy({ top: 120, behavior: 'smooth' }); return; } if (e.key === 'k') { window.scrollBy({ top: -120, behavior: 'smooth' }); return; } if (e.key === 'h') { window.scrollBy({ left: -120, behavior: 'smooth' }); return; } if (e.key === 'l') { window.scrollBy({ left: 120, behavior: 'smooth' }); return; } // Speed: > < if (e.key === '>' || (e.key === '.' && e.shiftKey)) { e.preventDefault(); const idx = SPEEDS.indexOf(currentSpeed); if (idx < SPEEDS.length - 1) applySpeed(SPEEDS[idx + 1]); return; } if (e.key === '<' || (e.key === ',' && e.shiftKey)) { e.preventDefault(); const idx = SPEEDS.indexOf(currentSpeed); if (idx > 0) applySpeed(SPEEDS[idx - 1]); return; } // Seek: ← → (10s) if (e.key === 'ArrowLeft') { e.preventDefault(); seekRelative(-10); return; } if (e.key === 'ArrowRight') { e.preventDefault(); seekRelative(10); return; } // Volume: ↑ ↓ if (e.key === 'ArrowUp') { e.preventDefault(); adjustVolume(0.05); return; } if (e.key === 'ArrowDown') { e.preventDefault(); adjustVolume(-0.05); return; } // Play/Pause: Space if (e.key === ' ' || e.code === 'Space') { e.preventDefault(); const v = findVideo() || document.querySelector('video'); if (v) { v.paused ? v.play() : v.pause(); } return; } // Fullscreen: F if (e.key === 'f' || e.key === 'F') { e.preventDefault(); toggleFullscreen(); return; } }); function seekRelative(delta) { const v = findVideo() || document.querySelector('video'); if (!v) return; const newTime = Math.max(0, Math.min(v.currentTime + delta, v.duration || Infinity)); v.currentTime = newTime; } function adjustVolume(delta) { const v = findVideo() || document.querySelector('video'); if (!v) return; const newVol = Math.max(0, Math.min(1, Math.round((v.volume + delta) * 100) / 100)); v.volume = newVol; v.muted = newVol === 0; // Sync the volume slider so the player's Vue state stays consistent const slider = document.querySelector('.custom-video-player .volume-slider'); if (slider) { slider.value = v.muted ? 0 : newVol; slider.dispatchEvent(new Event('input', { bubbles: true })); } saveVolume(newVol); showToastVolume(newVol); } function showToastVolume(vol) { if (!toastEl) { toastEl = document.createElement('div'); toastEl.style.cssText = 'position:absolute;top:12px;left:50%;transform:translateX(-50%);' + 'z-index:10000;background:rgba(0,0,0,0.85);color:#fff;' + 'font-size:15px;font-weight:600;padding:6px 16px;border-radius:6px;' + 'pointer-events:none;opacity:0;transition:opacity 0.2s;' + 'font-family:-apple-system,BlinkMacSystemFont,sans-serif;' + 'white-space:nowrap;letter-spacing:0.5px;'; } if (!toastEl.parentNode) { const vc = document.querySelector('.custom-video-player'); if (vc) vc.appendChild(toastEl); } if (!toastEl.parentNode) return; var pct = Math.round(vol * 100); toastEl.textContent = 'Vol ' + pct; toastEl.style.opacity = '1'; clearTimeout(toastTimer); toastTimer = setTimeout(function () { toastEl.style.opacity = '0'; }, 700); } // Channel panel: hover logo to show category nav const CATEGORIES = [ { name: '动漫', slug: 'anime' }, { name: '游戏', slug: 'game' }, { name: '音乐', slug: 'music' }, { name: '舞蹈', slug: 'dance' }, { name: '科技', slug: 'tech' }, { name: '吐槽', slug: 'commentary' }, { name: '漫画', slug: 'manga' }, { name: '鬼畜', slug: 'kichiku' }, { name: 'mmd', slug: 'mmd' }, { name: '日常', slug: 'daily' }, { name: 'AI-娱乐', slug: 'ai-entertainment' }, { name: '音mad', slug: 'otomad' } ]; let channelPanel = null; let channelTimer = null; function buildChannelPanel() { if (channelPanel) return; channelPanel = document.createElement('div'); channelPanel.className = 'nml-channel-panel'; channelPanel.setAttribute('data-v-e737bc8a', ''); channelPanel.innerHTML = '
' + '' + '' + '
' + '
' + CATEGORIES.map(function (c) { return ''; }).join('') + '
'; // Click: store target channel in sessionStorage, then navigate to homepage channelPanel.addEventListener('click', function (e) { var link = e.target.closest('.nml-chan-link'); if (!link) return; var slug = link.getAttribute('data-slug'); if (slug) { try { sessionStorage.setItem('nml_pending_channel', slug); } catch (e) {} } window.location.href = '/'; }); } function setupChannelPanel() { // Don't create if native header-channel already exists (e.g. on homepage) if (document.querySelector('.header-channel')) return; var logo = document.querySelector('.entry-title'); if (!logo) return; buildChannelPanel(); // Insert panel right after the logo logo.style.position = 'relative'; if (!channelPanel.parentNode) { logo.appendChild(channelPanel); } logo.addEventListener('mouseenter', function () { clearTimeout(channelTimer); channelPanel.classList.add('show'); }); logo.addEventListener('mouseleave', function () { channelTimer = setTimeout(function () { channelPanel.classList.remove('show'); }, 200); }); channelPanel.addEventListener('mouseenter', function () { clearTimeout(channelTimer); }); channelPanel.addEventListener('mouseleave', function () { channelPanel.classList.remove('show'); }); } // Channel panel styles (function injectChannelStyles() { var s = document.createElement('style'); s.textContent = '.nml-channel-panel{' + 'display:none;position:absolute;top:100%;left:0;margin-top:4px;' + 'background:var(--bg1,#fff);border:1px solid var(--line_regular,#e5e5e5);' + 'border-radius:10px;padding:12px 16px;z-index:1003;' + 'box-shadow:0 8px 32px rgba(0,0,0,0.12);white-space:nowrap;' + 'min-width:360px;' + '}' + '.nml-channel-panel.show{display:flex;gap:24px;}' + '.nml-channel-panel .channel-icons{display:flex;gap:16px;flex-shrink:0;}' + '.nml-channel-panel .channel-icons__item{display:flex;flex-direction:column;align-items:center;gap:6px;cursor:pointer;}' + '.nml-channel-panel .icon-bg{width:42px;height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;transition:transform 0.2s;}' + '.nml-channel-panel .icon-bg__dynamic{background:#ff6fae;color:#fff;}' + '.nml-channel-panel .icon-bg__popular{background:#ff9500;color:#fff;}' + '.nml-channel-panel .icon-title{font-size:12px;color:var(--text2,#666);}' + '.nml-channel-panel .channel-icons__item:hover .icon-bg{transform:scale(1.08);}' + '.nml-channel-panel .right-channel-container{display:flex;}' + '.nml-channel-panel .channel-items__left{display:grid;grid-template-columns:repeat(3,1fr);gap:4px 8px;}' + '.nml-channel-panel .channel-link{font-size:13px;color:var(--text2,#666);cursor:pointer;padding:4px 8px;border-radius:6px;transition:all 0.15s;}' + '.nml-channel-panel .channel-link:hover{background:var(--graph_bg_thin,rgba(0,0,0,0.04));color:var(--brand_pink,#ff6fae);}'; document.head.appendChild(s); })(); // Homepage: auto-select pending channel function trySelectChannel() { var slug = null; try { slug = sessionStorage.getItem('nml_pending_channel'); } catch (e) {} if (!slug) return; // Map slug to display name var nameMap = {}; CATEGORIES.forEach(function (c) { nameMap[c.slug] = c.name; }); if (slug === 'dynamic') nameMap.dynamic = '动态'; if (slug === 'popular') nameMap.popular = '热门'; var targetName = nameMap[slug]; if (!targetName) { clearPending(); return; } // Try to find a clickable channel element by text content var candidates = document.querySelectorAll( '[class*="channel"] a, [class*="channel"] button, [class*="channel"] span,' + '[class*="category"] a, [class*="category"] button, [class*="category"] span,' + '.channel-link, .category-item, .tab-btn,' + '[class*="ChannelBar"] a, [class*="ChannelBar"] button' ); for (var i = 0; i < candidates.length; i++) { var el = candidates[i]; if ((el.textContent || '').trim() === targetName) { el.click(); clearPending(); return; } } // Loose match for (var j = 0; j < candidates.length; j++) { var el2 = candidates[j]; if ((el2.textContent || '').indexOf(targetName) !== -1) { el2.click(); clearPending(); return; } } } function clearPending() { try { sessionStorage.removeItem('nml_pending_channel'); } catch (e) {} } // Kaomoji panel const KAOMOJI = [ { c: '开心', list: [ '╰(*°▽°*)╯', '(^▽^)', '(◕‿◕)', '✧(≖ ◡ ≖✿)', '(。•̀ᴗ-✿)', '(◠‿◠)', '(๑˃̵ᴗ˂̵)و', '(≧∇≦)', '(◍•ᴗ•◍)', '(。・ω・。)', '(ノ◕ヮ◕)ノ', '(●\'◡\'●)', '(◕ᴗ◕✿)', '♪(^∇^*)', '☆*:.。.o(≧▽≦)o.。.:*☆', 'ヽ(>∀<☆)ノ', '♪(๑ᴖ◡ᴖ๑)♪', '(▰˘◡˘▰)', '╭(●`∀´●)╯', 'ヾ(@^∇^@)ノ', 'o(*≧▽≦)ツ', '(ᗒᗨᗕ)', '٩(◕‿◕。)۶', '(★ω★)', '╰(▔∀▔)╯', '(✪ω✪)', 'o(≧∇≦o)', '(๑•̀ㅂ•́)و✧', '٩(ˊᗜˋ*)و', 'ヽ(✿゚▽゚)ノ', '( ^ω^)', '( ̄︶ ̄)', '( ´∀`)', '(o゚ω゚o)', '⌒(。・.・。)⌒','♪(╯^-^)╯♪' ]}, { c: '大笑', list: [ '(σ゚д゚)σ', 'ヾ(@>▽<@)ノ', '♪ヽ(^^ヽ)♪', '(・∀・)', '(●´∀`)●', 'Ψ( ̄∀ ̄)Ψ', 'ヽ(´▽`)/', '(๑´ㅂ`๑)', '哈哈哈哈', '恍恍惚惚', '红红火火恍恍惚惚', '啊哈哈哈哈(((o(*゚▽゚*)o)))', '笑死(≧∀≦)', '(σ゚∀゚)σ', '(((o(*゚▽゚*)o)))', 'ヽ(〃∀〃)ノ', '(ノ≧∀≦)ノ', '٩( ᐛ )و', '└(^o^)┘', '(๑>ᴗ<๑)', '(◡‿◡✿)', '∩(︶▽︶)∩', '( ゚∀゚)', '(●`∀´●)', '╭(●`∀´●)╯', '( ゜∀゜)' ]}, { c: '喜欢', list: [ '(。♥‿♥。)', '(❤ω❤)', '(´▽`ʃ♡ƪ)', '♡(◡‿◡✿)', '(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)', '(。・//ε//・。)', '(♡°▽°♡)', '(✿◠‿◠)', '♥(ノ´∀`)', '(´ ε ` )♡', '(。・ω・。)ノ♡', '(*^3^)/~☆', '(◕‿◕✿)', '(っ˘з(˘⌣˘ )', '(*^3^)', 'ヽ(愛´∀`愛)ノ', '(。・ω・。)ノ♡', 'チュ~(´ε`*)', '(๑ơ ₃ ơ)♥', '(●♡∀♡)', '(. ❛ ᴗ ❛.)', '(⁄ ⁄•⁄ω⁄•⁄ ⁄)' ]}, { c: '难过', list: [ '(╥_╥)', '(╯︵╰,)', '(。•́︿•̀。)', '(╥﹏╥)', 'ಥ_ಥ', '(个_个)', '(╯_╰)', '(。ŏ﹏ŏ)', '(╥ω╥)', '(´;︵;`)', '。゚(゚´Д`゚)゚。', '(´°̥̥̥̥̥̥̥̥ω°̥̥̥̥̥̥̥̥`)', '(;´༎ຶД༎ຶ`)', '(´;ω;`)', '(༎ຶ ෴ ༎ຶ)', '。・゚゚・(>д<)・゚゚・。', '(;_;)', '(ノД`)', '(ノ_<。)','。・゜・(ノД`)・゜・。', '(iДi)', '( >﹏< )', 'ヽ(*。>Д<)o゜' ]}, { c: '生气', list: [ '(╬ Ò﹏Ó)', '(#`д´)ノ', '(╯°□°)╯︵ ┻━┻', '(≖_≖ )', '(¬_¬)', '(`皿´#)', '(⋋▂⋌)', 'ヽ(`⌒´)ノ', '(╬゚◥益◤゚)', '(◣_◢)', '(`Δ´)!', '(#`皿´)', '(╬ ̄皿 ̄)', '凸(`0´)凸', '━━━╋══◥◣_◢◤', '╋══◥◣_◢◤', '(≧m≦)', 'ヽ(●-`Д´-)ノ', 's(・`ヘ´・;)ゞ', '(; ̄Д ̄)', '(#°Д°)', 'ヽ(・ω・´メ)', '(。・`ω´・)' ]}, { c: '惊讶', list: [ '(⊙_⊙)', '(º ロ º)', '(°ロ°)', '(⊙﹏⊙)', '(〇o〇;)', '( Д ) ゚ ゚', '∑(O_O;)', '(;° ロ°)', '(⊙.☉)', '∑(゚Д゚)', '(((;꒪ꈊ꒪;)))', '(゚ロ゚)', 'щ(゚Д゚щ)', '(。ӧ◡ӧ。)', '!!!∑(゚Д゚ノ)ノ', '‼(•\'╻\'• )', '〣( ºΔº )〣', 'щ(゜ロ゜щ)', 'Σ(°△°|||)', '!!!w(゚Д゚)w', '!!!(ʘ言ʘ╬)', '(゚Д゚≡゚д゚)', 'щ(゚Д゚щ)' ]}, { c: '害羞', list: [ '(〃ω〃)', '(。・//ω//・。)', '(〃∀〃)', '(。-人-。)', '(๑•́ ₃ •̀๑)', '(*/ω\*)', '(〃⌒ー⌒〃)', '(❁´◡`❁)', '(。•́︿•̀。)', '( ̄▽ ̄*)ゞ', '(>/////<)', '(*/∇\*)', '(*ノωノ)', '(〃▽〃)','(´,,•ω•,,)♡', '(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)', '(◞‸◟)', '(:3[▓▓]', '(*ノ▽ノ)', '(´_っ`)' ]}, { c: '眨眼', list: [ '(◕‿↼)', '(。•̀ᴗ-)✧', '◔ ⌣ ◔', '(・ω<)', '(。•ᴗ•。)♡', '( ̄ε ̄")', '(。・ω・。)ノ♡', '(^◡^)っ', '(๑¯◡¯๑)', '(✿◠‿◠)', '(≖‿≖)✧', '(・ω<)☆', '(●´ω`●)', '(・`ω´・)', '(^_-)','(. ❛ ᴗ ❛.)', '☆~(ゝ。∂)', '(=∀=*)', '(ΦωΦ)', '(ΦωΦ)' ]}, { c: '困惑', list: [ '( ̄ω ̄;)', '(@_@)', '(・_・;)', '(¯ . ¯;)', '(;¬_¬)', '(´・ω・`)?', '┐( ̄ヘ ̄")┌', '╮( ̄▽ ̄")╭', '( ̄~ ̄;)', '(´-ω-`)', '(。-ω-)zzz', '( ̄▽ ̄*)ゞ', '( ̄□ ̄;)', '(´-﹏-`;)', '(;一_一)', '(´゚ω゚`)', 'щ(ºДºщ)', '(O_o)', '( ・◇・)?','( ̄. ̄)', '(´・ω・`)' ]}, { c: '无语', list: [ '( ̄▽ ̄")', '(;´Д`)', '(´-ι_-`)', '( ̄ω ̄)', '(=_=;)', '(´・_・`)', '( ˘・з・)', '( ̄. ̄)', '┐(´-`)┌', '╮( ̄▽ ̄)╭', '( ̄ェ ̄;)', '(눈_눈)', '≖_≖', 'ー( ̄~ ̄)ξ', '( ̄^ ̄)', 'ヘ(´o`)ヘ', '(´_ゝ`)', '( ̄へ ̄)', '(›´ω`‹ )', '(  ̄  ̄ )', '(。_。)' ]}, { c: '傲娇', list: [ '哼!( ̄^ ̄)ゞ', '( ̄ε ̄")', '(,-`д´-)', '( `ー´)', '(-`ω´-)', '( ̄へ  ̄ )', '╭(╯^╰)╮', '(。-`ω´-)', '(`・ω・´)', '( ̄~ ̄)', '(≧へ≦)╯', '(-`ェ´-╬)', '(`へ´*)ノ', '( ・`⌓´・)', '(〃` 3´〃)', '(`ε´)', '(,,Ծ‸Ծ,,)', 'ヽ(`⌒´)ノ', '(◣_◢)', '(`・ω・´)', '(`・ω・´)', '(。-`ω´-)' ]}, { c: '舞蹈', list: [ '┗(^0^)┓', '♪(┌・。・)┌', '♪ヽ(^^ヽ)♪', 'ƪ(˘⌣˘)ʃ', 'ヾ(⌐■_■)ノ♪', '♬♩♫♪☻(●´∀`●)☺♪♫♩♬', '┏(^0^)┛', 'ヘ(^_^ヘ)', 'ヾ(´〇`)ノ', '♪♪♪ ヽ(ˇ∀ˇ )ゞ', '♫꒰・‿・๑꒱', '♪(o・ω・)ノ', '(〜^∇^)〜', '♪ヽ( ⌒o⌒)人(⌒-⌒ )v ♪', 'ヘ( ̄ω ̄ヘ)', 'ƪ(‾ε‾")ʃ', '┏(^0^)┛┗(^0^)┓', 'ヘ(^_^ヘ) ヘ(^o^ヘ)', '♪ ヾ(⌒ε⌒*)ゞ', 'ヾ(´・ω・`)ノ' ]}, { c: '摊手', list: [ '¯\\_(ツ)_/¯', '┐(´д`)┌', '╮(╯_╰)╭', '(;-_-)︵', '(ツ)', 'ヽ(。_°)ノ', '┐( ̄ヮ ̄)┌', '(´-ι_-`)', '~( ̄▽ ̄~)', '┐(´~`)┌', '╮( ̄~ ̄)╭', '┐( ̄ー ̄)┌', '( ̄ο ̄)', '(。-ω-)', '┐(´∀`)┌', '~(´ー`~)', '╮(╯∀╰)╭', '┐(シ)┌', '( ´_ゝ`)', '┐( ̄∀ ̄)┌' ]}, { c: '动物', list: [ '(=\\^・ω・\\^=)', '(◕ᴥ◕)', '(=\'◉ω◉\'=)', 'V●ᴥ●V', '(=^ ◡ ^=)', '(=\\^._.\\^=)∫', '( ̄(00) ̄)', '(°(oo)°)', 'くコ:彡', '/(=;x;=)\', '(=▼ω▼=)', '(=ФωФ=)', '(^=˃ᆺ˂)', '(=\'×\'=)', 'U•ᴥ•U', '(ᵔᴥᵔ)', 'ฅ(^ω^ฅ)', '/(・×・)\', '/(˃ᆺ˂)\', '(=`ェ´=)' ]}, { c: '颜艺', list: [ '( ͡° ͜ʖ ͡°)', '( ͡~ ͜ʖ ͡~)', '( ͠° ͟ʖ ͡°)', 'ヽ(͡◕ ͜ʖ ͡◕)ノ', '( ͡°╭ͮ ͟ʖ╮ ͡° )', '(☞゚ヮ゚)☞', '☜(゚ヮ゚☜)', '(⌐■_■)', '(•̀ᴗ•́)و ̑̑', '(◕ᴥ◕ʋ)', '( ͡ಠ ʖ̯ ͡ಠ)', '凸(¬‿¬)凸', 't(-_-t)', '(╭ರ_⊙)', '(♯`∧´)', '(ز ͡° ͜ʖ ͡°)ز', '┌∩┐(◣_◢)┌∩┐', '( ͡° ͜ʖ ͡°)=ε/̵͇̿̿/\'̿̿ ̿ ̿̿', '(ง\'̀-\'́)ง', 'щ(゚Д゚щ)', '( ゚∀。)' ]}, { c: '经典', list: [ '(づ。◕‿‿◕。)づ', '(づ ̄ ³ ̄)づ', '(◡ ω ◡)', '(ノ´ヮ`)ノ*: ・゚', '(>^ω^<)', '*:・゚✧(ꈍᴗꈍ)✧・゚:*', '☆*。★゚*♪ヾ(☆ゝз・)ノ', '✧*:・゚ヾ(●´∀`●)', '°˖✧◝(⁰▿⁰)◜✧˖°', '(✿⊙‿⊙)', '(ノ◕ヮ◕)ノ*:・゚✧', '(ノ>ω<)ノ :。・:*:・゚\'', '★,。・:*:・゚☆ ヽ(〃・ω・)ノ', ':・゚✧゚・: *ヽ(◕ヮ◕ヽ)', '~(^з^)-☆', '(>ω^<)ノ彡☆', '☆⌒(≧▽° )', '(*´▽`*)ノシ', '*・゜゚・*:.。..。.:*・\'(*゚▽゚*)\'・*:.。. .。.:*・゜゚・*' ]}, { c: '卖萌', list: [ '(◕‿◕✿)', '✿(。◕‿◕。)✿', '(◕ω◕✿)', '(◕ᴗ◕✿)', '(◡ ω ◡)', '(★^ー^★)', '✿♥‿♥✿', '(。・ω・。)', '(^ω^)', '(≧ω≦)', '٩(ˊᗜˋ*)و', 'ヾ(@⌒ー⌒@)ノ', '(っ´▽`)っ', '\(^ω^\)', '∪・ω・∪', '(๑•̀ㅂ•́)و✧', '(ᗒᗨᗕ)', '(●´艸`)', '(´。• ᵕ •。`)', '( ˘•ω•˘ )', '(。・ω・。)' ]}, { c: '躺平', list: [ '_(:3 」∠)_', '_(:3⌒゙)_', '_(´ཀ`」 ∠)_', '(:3[▓▓]', '(:3[」_]', '(:3_ヽ)_', '_ノ乙(、ン、)_', '(๑•́ ₃ •̀๑)エー', '(:3[▓▓▓]', '(。-ω-)zzz', '( ̄ρ ̄)..zzZZ', '(◎−◎;)', '(´。`)', '(ˇ▽ˇ)ZZz', '∪・ω・∪zzZ', '(´〜`*) zzz', 'ZZzz(。-_-。)ZZzz', '(。-ω-)' ]}, { c: '战斗', list: [ '(╯°□°)╯︵ ┻━┻', '┬─┬ノ( º _ ºノ)', '(ノಠ益ಠ)ノ', '(ง •̀_•́)ง', '(╯°益°)╯彡┻━┻', '┬─┬ ノ( ゜-゜ノ)', '(ʘ言ʘ╬)', '(ノ`Д´)ノ彡┻━┻', '┬──┬ ノ(ò_óノ)', '(ノ°Д°)ノ︵ ┻━┻', '┻━┻ ︵ヽ(´Д`ヽ)', '(╯ಠ_ಠ)╯︵ ┻━┻', '(ノ`□´)ノ⌒┻━┻', '(╯°□°)╯︵(\\.o.)\\', '┬─┬⃰͡ (ᵔᵕᵔ͜ )', '┻━┻︵ \\(°□°)/ ︵ ┻━┻', '(ノ-_-)ノ~┻━┻', 'ヽ(゚Д゚)ノ', '┗(`Д゚┗(`゚Д゚)┓゚Д´)┛' ]} ]; let kaoPanel = null; let kaoActiveInput = null; let kaoHideTimer = null; let kaoActiveCat = 0; function buildKaoPanel() { if (kaoPanel) return; kaoPanel = document.createElement('div'); kaoPanel.className = 'nml-kao-panel'; kaoPanel.addEventListener('mousedown', function (e) { e.preventDefault(); }); // Category tabs var tabs = document.createElement('div'); tabs.className = 'nml-kao-tabs'; KAOMOJI.forEach(function (cat, i) { var tab = document.createElement('span'); tab.className = 'nml-kao-tab' + (i === 0 ? ' active' : ''); tab.textContent = cat.c; tab.addEventListener('mousedown', function (e) { e.stopPropagation(); e.preventDefault(); switchKaoCat(i); }); tabs.appendChild(tab); }); kaoPanel.appendChild(tabs); // Kaomoji grid var grid = document.createElement('div'); grid.className = 'nml-kao-grid'; renderKaoGrid(grid, 0); kaoPanel.appendChild(grid); document.body.appendChild(kaoPanel); // Styles var s = document.createElement('style'); s.textContent = '.nml-kao-panel{' + 'display:none;position:fixed;z-index:10010;' + 'background:var(--bg1,#1a1a1a);border:2px solid var(--brand_pink,#ff6fae);' + 'border-radius:10px;padding:10px;min-width:400px;max-width:680px;' + 'box-shadow:0 8px 32px rgba(0,0,0,0.5),0 0 0 1px rgba(255,111,174,0.3) inset;' + '}' + '.nml-kao-panel.show{display:block;}' + '.nml-kao-tabs{' + 'display:flex;flex-wrap:wrap;gap:3px;margin-bottom:10px;' + 'border-bottom:2px dashed var(--brand_pink,rgba(255,111,174,0.4));padding-bottom:8px;' + '}' + '.nml-kao-tab{' + 'font-size:12px;padding:4px 10px;border-radius:12px;cursor:pointer;' + 'color:var(--text2,#999);transition:all 0.15s;user-select:none;' + 'border:1px solid transparent;' + '}' + '.nml-kao-tab:hover{background:var(--graph_bg_thin,rgba(255,255,255,0.06));color:var(--text1,#ddd);border-color:var(--brand_pink,rgba(255,111,174,0.3));}' + '.nml-kao-tab.active{background:var(--brand_pink,#ff6fae);color:#fff;border-color:var(--brand_pink,#ff6fae);}' + '.nml-kao-grid{' + 'display:flex;flex-wrap:wrap;gap:4px;max-height:280px;overflow-y:auto;' + 'scrollbar-width:thin;scrollbar-color:var(--brand_pink,#ff6fae) transparent;' + '}' + '.nml-kao-grid::-webkit-scrollbar{width:4px;}' + '.nml-kao-grid::-webkit-scrollbar-thumb{background:var(--brand_pink,#ff6fae);border-radius:4px;}' + '.nml-kao-item{' + 'font-size:14px;padding:5px 10px;border-radius:6px;cursor:pointer;' + 'color:var(--text1,#ddd);transition:all 0.12s;user-select:none;' + 'white-space:nowrap;line-height:1.7;border:1px solid transparent;' + '}' + '.nml-kao-item:hover{background:var(--brand_pink,#ff6fae);color:#fff;border-color:rgba(255,255,255,0.2);}'; document.head.appendChild(s); } function renderKaoGrid(grid, catIdx) { grid.innerHTML = ''; var list = KAOMOJI[catIdx].list; list.forEach(function (kao) { var item = document.createElement('span'); item.className = 'nml-kao-item'; item.textContent = kao; item.addEventListener('mousedown', function (e) { e.stopPropagation(); e.preventDefault(); insertKaomoji(kao); }); grid.appendChild(item); }); } function switchKaoCat(idx) { kaoActiveCat = idx; var tabs = kaoPanel.querySelectorAll('.nml-kao-tab'); tabs.forEach(function (t, i) { t.classList.toggle('active', i === idx); }); var grid = kaoPanel.querySelector('.nml-kao-grid'); renderKaoGrid(grid, idx); } function insertKaomoji(text) { var el = kaoActiveInput; if (!el) return; el.focus(); if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { var s = el.selectionStart; var e = el.selectionEnd; el.value = el.value.substring(0, s) + text + el.value.substring(e); el.selectionStart = el.selectionEnd = s + text.length; } else if (el.isContentEditable) { var sel = window.getSelection(); if (sel.rangeCount) { var r = sel.getRangeAt(0); r.deleteContents(); var tn = document.createTextNode(text); r.insertNode(tn); r.setStartAfter(tn); r.collapse(true); sel.removeAllRanges(); sel.addRange(r); } } // Dispatch input event so Vue/Element Plus picks up the change el.dispatchEvent(new Event('input', { bubbles: true })); } function positionKaoPanel(input) { var rect = input.getBoundingClientRect(); var top = rect.bottom + 6; var left = rect.left; // Keep panel within viewport (panel is up to 680px wide) if (left + 420 > window.innerWidth) left = window.innerWidth - 430; if (left < 4) left = 4; if (top + 320 > window.innerHeight) top = rect.top - 330; if (top < 4) top = 4; kaoPanel.style.top = top + 'px'; kaoPanel.style.left = left + 'px'; kaoPanel.classList.add('show'); } function hideKaoPanel() { if (kaoPanel) kaoPanel.classList.remove('show'); kaoActiveInput = null; } function onCommentFocus(e) { buildKaoPanel(); kaoActiveInput = e.target; positionKaoPanel(e.target); } function onCommentBlur() { clearTimeout(kaoHideTimer); var active = document.activeElement; kaoHideTimer = setTimeout(function () { if (kaoPanel && !kaoPanel.contains(active)) { hideKaoPanel(); } }, 150); } // Check if an element is a comment/chat/post input we should show kaomoji for function isCommentLike(el) { if (el.tagName !== 'TEXTAREA' && !el.isContentEditable) return false; // Skip search bars, login forms, etc. if (el.closest('[class*=search], [class*=login], [class*=sign], input[type=search]')) return false; // Match: comment-input, comment-editor, reply-*, chat-*, post-comment, message-input, editor-* if (el.closest('.comment-input, .comment-editor, [class*=comment], [class*=reply], [class*=chat-input], [class*=message-input], [class*=post-comment], [class*=editor], [class*=rich-text]')) return true; // Also match any textarea with a comment-like placeholder if (el.tagName === 'TEXTAREA') { var ph = (el.placeholder || '').toLowerCase(); if (/评论|回复|留言|说点什么|发(表|布|送)|聊|输入|看法|弹幕|吐槽|写(下|点)|参与|讨论/.test(ph)) return true; } return false; } // Use click delegation (capture phase) — most reliable trigger document.addEventListener('click', function (e) { var el = e.target; if (isCommentLike(el)) { onCommentFocus({ target: el }); } }, true); // Also hook focus for keyboard-only users (Tab navigation) document.addEventListener('focusin', function (e) { var el = e.target; if (isCommentLike(el)) { onCommentFocus({ target: el }); } }); // Blur — hide after short delay document.addEventListener('focusout', function (e) { if (isCommentLike(e.target)) { onCommentBlur(); } }); // Danmaku modes (zero-width char encoding in content) let danmakuMode = 0; // 0=scroll, 1=top, 2=bottom, 3=reverse, 4=advanced // Advanced params: start/end position + rotation + easing let advSX = 0, advSY = 50, advEX = 100, advEY = 50, advSR = 0, advER = 0, advEasing = 0; var ZW = ['​','‌','‍','‎','‏','','⁠','⁡','⁢','⁣','⁤']; // 11 zero-width chars var EASINGS = ['linear','ease','ease-in','ease-out','ease-in-out', 'cubic-bezier(.68,-.55,.27,1.55)','cubic-bezier(.36,0,.66,-.56)', 'cubic-bezier(.34,1.56,.64,1)','steps(3,end)','cubic-bezier(.45,0,.55,1)', 'cubic-bezier(.22,.61,.36,1)']; var EASING_NAMES = ['直线','缓动','渐入','渐出','渐入出','回弹','弹力','弹入','阶梯','平滑','标准']; function zwVal(ch) { var i = ZW.indexOf(ch); return i >= 0 ? i : 0; } function zwChar(v) { return ZW[v] || ZW[0]; } function b11(a,b) { return a*11 + b; } // base-11 decode helper function encodeContent(text, mode) { if (mode === 0 || !text) return text; if (mode >= 1 && mode <= 3) return ZW[mode - 1] + text; if (mode === 4) { // U+2064 prefix + 14 base-11 digits: SX,SX,SY,SY,EX,EX,EY,EY,SR,SR,ER,ER,Easing,Easing var p = [advSX, advSY, advEX, advEY, Math.round(advSR / 3), Math.round(advER / 3), advEasing]; var s = ZW[10]; // U+2064 prefix (NOT U+FEFF — BOM is stripped by browser) p.forEach(function (v) { v = Math.round(Math.max(0, Math.min(120, v))); s += zwChar(Math.floor(v / 11)) + zwChar(v % 11); }); return s + text; } return text; } function decodeContent(el) { var text = el.textContent || ''; if (!text) return { mode: 0 }; var c0 = text.charCodeAt(0); if (c0 === 0x200B) return { mode: 1 }; if (c0 === 0x200C) return { mode: 2 }; if (c0 === 0x200D) return { mode: 3 }; // Advanced: U+2064 prefix (NOT U+FEFF — BOM gets stripped by browser) if (c0 === 0x2064 && text.length >= 16) { var ch = text, i = 1; var sx = b11(zwVal(ch[i]), zwVal(ch[i+1])); i += 2; var sy = b11(zwVal(ch[i]), zwVal(ch[i+1])); i += 2; var ex = b11(zwVal(ch[i]), zwVal(ch[i+1])); i += 2; var ey = b11(zwVal(ch[i]), zwVal(ch[i+1])); i += 2; var sr = b11(zwVal(ch[i]), zwVal(ch[i+1])) * 3; i += 2; var er = b11(zwVal(ch[i]), zwVal(ch[i+1])) * 3; i += 2; var ease = b11(zwVal(ch[i]), zwVal(ch[i+1])); return { mode: 4, sx: sx, sy: sy, ex: ex, ey: ey, sr: sr, er: er, easing: Math.min(ease, 10) }; } // Old 6-char format fallback (U+FEFF prefix — may be stripped) if ((c0 === 0xFEFF || c0 === 0x2064) && text.length >= 6) { var x = b11(zwVal(text[1]), zwVal(text[2])); var y = b11(zwVal(text[3]), zwVal(text[4])); var r = zwVal(text[5]) * 33; // old format: single-char rotation, 0-330° return { mode: 4, sx: x, sy: y, ex: x, ey: y, sr: r, er: r, easing: 0 }; } return { mode: 0 }; } function injectContentMarker() { if (danmakuMode === 0) return; var input = document.querySelector('.danmaku-input'); if (!input) return; var text = input.value; if (!text) return; var first = text.charCodeAt(0); if (first >= 0x200B && first <= 0x2064 && first !== 0x20 && first !== 0x0A) return; var encoded = encodeContent(text, danmakuMode); console.log('[nml-dm] SEND mode=' + danmakuMode + (danmakuMode === 4 ? ' sx=' + advSX + ' sy=' + advSY + ' ex=' + advEX + ' ey=' + advEY : '') + ' text=' + text.slice(0, 15)); input.value = encoded; input.dispatchEvent(new Event('input', { bubbles: true })); setTimeout(function () { input.value = text; input.dispatchEvent(new Event('input', { bubbles: true })); }, 60); } document.addEventListener('click', function (e) { if (e.target.closest('.send-btn')) injectContentMarker(); }, true); document.addEventListener('keydown', function (e) { if (e.key === 'Enter' && e.target.classList.contains('danmaku-input')) injectContentMarker(); }, true); // Rendering var _advDmId = 0; (function () { var kf = document.createElement('style'); kf.id = 'nml-dm-keyframes'; kf.textContent = '@keyframes nml-reverse-dm{from{left:-50%;}to{left:110%;}}'; document.head.appendChild(kf); })(); function applyDanmakuStyle(el, data) { var text = el.textContent || ''; var first = text.charCodeAt(0); var stripLen = 0; if (first === 0x200B || first === 0x200C || first === 0x200D) stripLen = 1; else if (first === 0x2064 || first === 0xFEFF) stripLen = text.length >= 16 ? 15 : (text.length >= 6 ? 6 : 0); if (stripLen > 0) { text = text.slice(stripLen); el.textContent = text; } var clone = el.cloneNode(true); var fs = el.style.fontSize || '24px'; var col = el.style.color || '#fff'; var base = 'position:absolute!important;z-index:999!important;' + 'white-space:nowrap!important;pointer-events:none!important;' + 'font-size:' + fs + '!important;color:' + col + '!important;' + 'text-shadow:1px 0 2px #000,-1px 0 2px #000,0 1px 2px #000,0 -1px 2px #000;'; var player = document.querySelector('.player-wrapper'); var video = document.querySelector('.custom-video-player video'); if (data.mode === 1 || data.mode === 2) { clone.style.cssText = base + 'left:50%!important;top:' + (data.mode === 1 ? '6%' : '88%') + '!important;' + 'transform:translateX(-50%)!important;'; } else if (data.mode === 3) { var ss = document.querySelector('.danmaku-input-wrapper input[type=range]'); var dur = 720 / (parseFloat(ss?.value) || 120); clone.style.cssText = base + 'top:50%!important;animation:nml-reverse-dm ' + dur + 's linear forwards!important;'; } else if (data.mode === 4) { // Dynamic keyframes for this specific danmaku var id = 'nml-adv-' + (++_advDmId); var kf = document.createElement('style'); kf.textContent = '@keyframes ' + id + '{' + 'from{left:' + data.sx + '%;top:' + data.sy + '%;' + 'transform:translate(-50%,-50%) rotate(' + data.sr + 'deg);}' + 'to{left:' + data.ex + '%;top:' + data.ey + '%;' + 'transform:translate(-50%,-50%) rotate(' + data.er + 'deg);}}'; document.head.appendChild(kf); var easing = EASINGS[data.easing] || 'linear'; var advDur = 5; clone.style.cssText = base + 'animation:' + id + ' ' + advDur + 's ' + easing + ' forwards!important;'; // Clean up keyframes after animation clone.addEventListener('animationend', function () { setTimeout(function () { if (kf.parentNode) kf.remove(); }, 100); }); } if (player) player.appendChild(clone); el.style.display = 'none'; if (data.mode === 1 || data.mode === 2) { if (video) { var startTime = video.currentTime; (function tick() { if (!clone.parentNode) return; if (video.paused) { startTime = video.currentTime; requestAnimationFrame(tick); return; } if (video.currentTime - startTime >= 4) { clone.style.transition = 'opacity 0.6s'; clone.style.opacity = '0'; setTimeout(function () { if (clone.parentNode) clone.remove(); if (el.parentNode) el.remove(); }, 600); return; } requestAnimationFrame(tick); })(); } } else if (data.mode === 3 || data.mode === 4) { if (video) { (function sync() { if (!clone.parentNode) return; clone.style.setProperty('animation-play-state', video.paused ? 'paused' : 'running', 'important'); requestAnimationFrame(sync); })(); } clone.addEventListener('animationend', function () { clone.style.transition = 'opacity 0.3s'; clone.style.opacity = '0'; setTimeout(function () { if (clone.parentNode) clone.remove(); if (el.parentNode) el.remove(); }, 300); }); } } var danmakuObserveCount = 0; var danmakuLayerObserver = new MutationObserver(function (mutations) { mutations.forEach(function (m) { m.addedNodes.forEach(function (node) { if (node.nodeType !== 1 || !node.style) return; var data = decodeContent(node); danmakuObserveCount++; // Extract visible text (strip zero-width prefix) var visibleText = node.textContent || ''; var first = visibleText.charCodeAt(0); if (first === 0x200B || first === 0x200C || first === 0x200D) visibleText = visibleText.slice(1); else if (first === 0x2064 || first === 0xFEFF) visibleText = visibleText.slice(visibleText.length >= 16 ? 15 : 6); console.log('[nml-dm] OBSERVE #' + danmakuObserveCount + ' mode=' + data.mode + (data.mode === 4 ? ' sx=' + data.sx + ' sy=' + data.sy + ' ex=' + data.ex + ' ey=' + data.ey : '') + ' text=' + visibleText.slice(0, 25)); addDanmakuEntry(data, visibleText); if (data.mode > 0) applyDanmakuStyle(node, data); }); }); }); function hookDanmakuLayer() { danmakuLayerObserver.disconnect(); var layer = null; // Strategy 1: find container inside player-wrapper with pointer-events:none and position:absolute var candidates = document.querySelectorAll('.player-wrapper > div, .player-wrapper > * > div'); for (var i = 0; i < candidates.length; i++) { try { var cs = getComputedStyle(candidates[i]); if (cs.pointerEvents === 'none' && (cs.position === 'absolute' || cs.position === 'fixed')) { // Check if it contains or could contain danmaku text if (candidates[i].childElementCount > 0 || candidates[i].offsetHeight > 100) { layer = candidates[i]; break; } } } catch (e) {} } // Strategy 2: fallback to [class*=danmaku] (excluding input wrapper) if (!layer) { var dmEls = document.querySelectorAll('[class*=danmaku]'); for (var j = 0; j < dmEls.length; j++) { if (!dmEls[j].classList.contains('danmaku-input-wrapper') && !dmEls[j].closest('.danmaku-input-wrapper')) { layer = dmEls[j]; break; } } } // Strategy 3: watch entire player-wrapper if (!layer) layer = document.querySelector('.player-wrapper'); if (layer) { danmakuLayerObserver.observe(layer, { childList: true, subtree: true }); } else { setTimeout(hookDanmakuLayer, 1000); } } // Danmaku list panel var dmList = []; // collected danmaku entries var dmListPanel = null; // panel DOM var dmListExpanded = false; // collapsed by default var MODE_LABELS = ['','顶部','底部','逆向','高级']; var MODE_CLASS = ['','nml-dm-tag-top','nml-dm-tag-bottom','nml-dm-tag-rev','nml-dm-tag-adv']; function addDanmakuEntry(data, text) { var video = document.querySelector('.custom-video-player video'); dmList.push({ time: video ? video.currentTime : 0, mode: data.mode, text: text, params: data.mode === 4 ? { sx: data.sx, sy: data.sy, ex: data.ex, ey: data.ey, sr: data.sr, er: data.er, easing: data.easing } : null }); updateDanmakuListCount(); if (dmListPanel && dmListExpanded) renderDanmakuList(); } function renderDanmakuList() { if (!dmListPanel) return; var body = dmListPanel.querySelector('.nml-dm-list-body'); if (!body || !dmListExpanded) return; var html = ''; for (var i = dmList.length - 1; i >= 0; i--) { var d = dmList[i]; var t = formatTime(d.time); var tagCls = MODE_CLASS[d.mode] || ''; var tagLabel = MODE_LABELS[d.mode] || '滚动'; var textEscaped = escapeHtml(d.text.length > 30 ? d.text.slice(0, 30) + '...' : d.text); html += '
' + '' + t + '' + '' + tagLabel + '' + '' + textEscaped + ''; if (d.params) { html += '
起点(' + d.params.sx + '%,' + d.params.sy + '%) ' + '→终点(' + d.params.ex + '%,' + d.params.ey + '%) ' + '旋转' + d.params.sr + '°→' + d.params.er + '° ' + '缓动:' + (EASING_NAMES[d.params.easing] || '?') + '
'; } html += '
'; } body.innerHTML = html; } function formatTime(sec) { var m = Math.floor(sec / 60); var s = Math.floor(sec % 60); return m + ':' + (s < 10 ? '0' : '') + s; } function escapeHtml(str) { return str.replace(/&/g, '&').replace(//g, '>'); } function buildDanmakuListPanel() { if (dmListPanel) return; dmListPanel = document.createElement('div'); dmListPanel.className = 'nml-dm-list-panel'; dmListPanel.style.cssText = 'display:block!important;visibility:visible!important;opacity:1!important;'; dmListPanel.innerHTML = '
' + '弹幕列表 (' + dmList.length + ')' + '' + '
' + ''; dmListPanel.querySelector('.nml-dm-list-header').addEventListener('click', function () { dmListExpanded = !dmListExpanded; var body = dmListPanel.querySelector('.nml-dm-list-body'); var arrow = dmListPanel.querySelector('.nml-dm-list-arrow'); if (dmListExpanded) { body.style.display = ''; arrow.textContent = '▾'; } else { body.style.display = 'none'; arrow.textContent = '▸'; } dmListPanel.querySelector('.nml-dm-list-title').textContent = '弹幕列表 (' + dmList.length + ')'; renderDanmakuList(); }); // Styles var ds = document.createElement('style'); ds.textContent = '.nml-dm-list-panel{margin-bottom:16px;border:1px solid var(--line_regular,#e5e5e5);border-radius:8px;overflow:hidden;}' + '.nml-dm-list-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;' + 'cursor:pointer;user-select:none;background:var(--graph_bg_thin,rgba(0,0,0,0.02));transition:background 0.15s;}' + '.nml-dm-list-header:hover{background:var(--graph_bg_thin,rgba(0,0,0,0.05));}' + '.nml-dm-list-title{font-size:14px;font-weight:600;color:var(--text1,#333);}' + '.nml-dm-list-arrow{font-size:14px;color:var(--text2,#999);}' + '.nml-dm-list-body{max-height:320px;overflow-y:auto;padding:8px 14px;' + 'scrollbar-width:thin;scrollbar-color:var(--brand_pink,#ff6fae) transparent;}' + '.nml-dm-list-body::-webkit-scrollbar{width:4px;}' + '.nml-dm-list-body::-webkit-scrollbar-thumb{background:var(--brand_pink,#ff6fae);border-radius:4px;}' + '.nml-dm-entry{padding:6px 0;border-bottom:1px solid var(--line_regular,rgba(0,0,0,0.05));font-size:13px;}' + '.nml-dm-entry:last-child{border-bottom:none;}' + '.nml-dm-time{color:var(--text2,#999);margin-right:8px;font-family:monospace;}' + '.nml-dm-tag{display:inline-block;padding:1px 6px;border-radius:3px;font-size:11px;margin-right:6px;}' + '.nml-dm-tag-top{background:rgba(0,200,100,0.15);color:#00c864;}' + '.nml-dm-tag-bottom{background:rgba(0,150,255,0.15);color:#0096ff;}' + '.nml-dm-tag-rev{background:rgba(255,150,0,0.15);color:#ff9600;}' + '.nml-dm-tag-adv{background:rgba(255,111,174,0.15);color:#ff6fae;}' + '.nml-dm-text{color:var(--text1,#333);}' + '.nml-dm-params{font-size:11px;color:var(--text2,#999);margin-top:2px;padding-left:60px;}'; document.head.appendChild(ds); } function findRightAd() { // Find .ad-slot near related/recommend section (right sidebar) var related = document.querySelector('[class*=related], [class*=recommend]'); if (related) { // Search siblings and ancestors for .ad-slot var ad = related.previousElementSibling; while (ad) { if (ad.matches('.ad-slot')) return ad; ad = ad.previousElementSibling; } } // Fallback: last .ad-slot on the page (likely the right one) var ads = document.querySelectorAll('.ad-slot'); return ads.length > 0 ? ads[ads.length - 1] : null; } function injectDanmakuListPanel() { buildDanmakuListPanel(); if (document.contains(dmListPanel)) { updateDanmakuListCount(); return; } var ad = findRightAd(); if (!ad) { console.log('[nml-dm-list] no right .ad-slot found'); return; } ad.parentNode.insertBefore(dmListPanel, ad.nextSibling); ad.style.display = 'none'; updateDanmakuListCount(); console.log('[nml-dm-list] injected, entries=' + dmList.length); } function updateDanmakuListCount() { if (!dmListPanel) return; var title = dmListPanel.querySelector('.nml-dm-list-title'); if (title) title.textContent = '弹幕列表 (' + dmList.length + ')'; } // Guard setInterval(function () { if (!location.href.includes('/video/')) return; var ad = findRightAd(); if (ad && ad.style.display !== 'none') ad.style.display = 'none'; if (!dmListPanel || !document.contains(dmListPanel)) { console.log('[nml-dm-list] re-injecting'); dmListPanel = null; injectDanmakuListPanel(); } }, 1500); // Mode selector UI — injected into settings panel let dmModeBar = null; function buildDanmakuModeBar() { if (dmModeBar) return; dmModeBar = document.createElement('div'); dmModeBar.className = 'setting-row nml-dm-mode-row'; dmModeBar.setAttribute('data-v-65d53e0f', ''); dmModeBar.innerHTML = '模式'; var btnGroup = document.createElement('div'); btnGroup.className = 'nml-dm-mode-btns'; var modes = [ { id: 0, label: '滚动' }, { id: 1, label: '顶部' }, { id: 2, label: '底部' }, { id: 3, label: '逆向' }, { id: 4, label: '高级' } ]; modes.forEach(function (m) { var btn = document.createElement('button'); btn.setAttribute('data-v-65d53e0f', ''); btn.className = 'nml-dm-mode-btn' + (m.id === danmakuMode ? ' active' : ''); btn.textContent = m.label; btn.addEventListener('mousedown', function (e) { e.stopPropagation(); e.preventDefault(); danmakuMode = m.id; btnGroup.querySelectorAll('.nml-dm-mode-btn').forEach(function (b) { b.classList.remove('active'); }); btn.classList.add('active'); toggleAdvControls(); }); btnGroup.appendChild(btn); }); dmModeBar.appendChild(btnGroup); // Advanced controls — shown only when mode 4 (高级) is active var advControls = document.createElement('div'); advControls.className = 'nml-adv-controls'; advControls.setAttribute('data-v-65d53e0f', ''); advControls.style.display = danmakuMode === 4 ? '' : 'none'; var advCfg = [ { ref: function() { return advSX; }, set: function(v) { advSX = v; }, label: '起点X', min: 0, max: 100, val: advSX, unit: '%' }, { ref: function() { return advSY; }, set: function(v) { advSY = v; }, label: '起点Y', min: 0, max: 100, val: advSY, unit: '%' }, { ref: function() { return advEX; }, set: function(v) { advEX = v; }, label: '终点X', min: 0, max: 100, val: advEX, unit: '%' }, { ref: function() { return advEY; }, set: function(v) { advEY = v; }, label: '终点Y', min: 0, max: 100, val: advEY, unit: '%' }, { ref: function() { return advSR; }, set: function(v) { advSR = v; }, label: '起始角度', min: 0, max: 360, step: 3, val: advSR, unit: '°' }, { ref: function() { return advER; }, set: function(v) { advER = v; }, label: '终点角度', min: 0, max: 360, step: 3, val: advER, unit: '°' } ]; advCfg.forEach(function (cfg) { var d = document.createElement('div'); d.className = 'setting-row'; d.setAttribute('data-v-65d53e0f', ''); d.innerHTML = '' + cfg.label + '' + '' + '' + cfg.val + cfg.unit + ''; var slider = d.querySelector('input'); var valSpan = d.querySelector('.setting-val'); slider.addEventListener('input', function () { var v = parseInt(slider.value); valSpan.textContent = v + cfg.unit; cfg.set(v); }); advControls.appendChild(d); }); // Easing selector var easeRow = document.createElement('div'); easeRow.className = 'setting-row'; easeRow.setAttribute('data-v-65d53e0f', ''); easeRow.innerHTML = '缓动'; var easeBtns = document.createElement('div'); easeBtns.className = 'nml-easing-btns'; EASING_NAMES.forEach(function (name, i) { var eb = document.createElement('button'); eb.setAttribute('data-v-65d53e0f', ''); eb.className = 'nml-easing-btn' + (i === advEasing ? ' active' : ''); eb.textContent = name; eb.addEventListener('mousedown', function (e) { e.stopPropagation(); e.preventDefault(); advEasing = i; easeBtns.querySelectorAll('.nml-easing-btn').forEach(function (b) { b.classList.remove('active'); }); eb.classList.add('active'); }); easeBtns.appendChild(eb); }); easeRow.appendChild(easeBtns); advControls.appendChild(easeRow); dmModeBar.appendChild(advControls); // Easing button styles if (!document.getElementById('nml-easing-style')) { var es = document.createElement('style'); es.id = 'nml-easing-style'; es.textContent = '.nml-easing-btns{display:flex;flex-wrap:wrap;gap:3px;}' + '.nml-easing-btn{font-size:11px;padding:2px 6px;border-radius:3px;cursor:pointer;' + 'color:var(--text2,#999);background:transparent;border:1px solid var(--line_regular,rgba(255,255,255,0.1));transition:all 0.15s;outline:none;}' + '.nml-easing-btn:hover{color:var(--text1,#fff);border-color:var(--brand_pink,#ff6fae);}' + '.nml-easing-btn.active{background:var(--brand_pink,#ff6fae);color:#fff;border-color:var(--brand_pink,#ff6fae);}'; document.head.appendChild(es); } // Style if (!document.getElementById('nml-dm-mode-style')) { var s = document.createElement('style'); s.id = 'nml-dm-mode-style'; s.textContent = '.nml-dm-mode-btns{display:flex;gap:4px;}' + '.nml-dm-mode-btn{' + 'font-size:12px;padding:3px 10px;border-radius:4px;cursor:pointer;' + 'color:var(--text2,#999);background:transparent;border:1px solid var(--line_regular,rgba(255,255,255,0.12));' + 'transition:all 0.15s;outline:none;' + '}' + '.nml-dm-mode-btn:hover{color:var(--text1,#fff);border-color:var(--brand_pink,#ff6fae);}' + '.nml-dm-mode-btn.active{background:var(--brand_pink,#ff6fae);color:#fff;border-color:var(--brand_pink,#ff6fae);}'; document.head.appendChild(s); } } function toggleAdvControls() { var ctrls = document.querySelector('.nml-adv-controls'); if (ctrls) ctrls.style.display = danmakuMode === 4 ? '' : 'none'; } function injectDanmakuModeBar() { var panel = document.querySelector('.danmaku-input-wrapper .settings-panel'); if (!panel || panel.querySelector('.nml-dm-mode-row')) return; buildDanmakuModeBar(); panel.appendChild(dmModeBar); toggleAdvControls(); } // Watch for settings panel being opened (Vue re-creates it each time) var settingsPanelObserver = new MutationObserver(function (mutations) { mutations.forEach(function (m) { m.addedNodes.forEach(function (node) { if (node.nodeType === 1 && node.classList && node.classList.contains('settings-panel')) { injectDanmakuModeBar(); } }); }); }); function hookSettingsPanelObserver() { settingsPanelObserver.disconnect(); var wrapper = document.querySelector('.danmaku-input-wrapper'); if (wrapper) { settingsPanelObserver.observe(wrapper, { childList: true, subtree: true }); // Check if panel is already open if (wrapper.querySelector('.settings-panel')) injectDanmakuModeBar(); } else { setTimeout(hookSettingsPanelObserver, 800); } } // Init let initAttempts = 0; function doInit() { setupChannelPanel(); if (location.pathname === '/' || location.pathname === '') { var channelAttempts = 0; function tryChannel() { trySelectChannel(); channelAttempts++; if (channelAttempts < 15) setTimeout(tryChannel, 400); } tryChannel(); } if (location.pathname.indexOf('/video/') === 0) { hookDanmakuLayer(); hookSettingsPanelObserver(); injectDanmakuListPanel(); if (mountUI()) return; initAttempts++; if (initAttempts < 50) setTimeout(doInit, 200); } } doInit(); // SPA navigation // Intercept history.pushState / replaceState to detect client-side navigation function onNavigate() { var url = location.href; if (url.includes('/video/')) { // Clean up previous video page state var old = document.querySelector('.speed-selector'); if (old) old.remove(); videoObserver.disconnect(); speedBtn = null; speedMenu = null; videoEl = null; mounted = false; initAttempts = 0; danmakuMode = 0; dmList = []; danmakuLayerObserver.disconnect(); currentSpeed = savedSpeed; // Re-initialize after Vue renders the new page setTimeout(doInit, 800); } } var _pushState = history.pushState; history.pushState = function () { _pushState.apply(this, arguments); onNavigate(); }; var _replaceState = history.replaceState; history.replaceState = function () { _replaceState.apply(this, arguments); onNavigate(); }; window.addEventListener('popstate', onNavigate); // Start if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { setTimeout(doInit, 600); }); } else { setTimeout(doInit, 600); } })();