// ==UserScript== // @name 加强糯米! // @namespace https://www.nuomill.com // @version 1.2 // @description 糯米洛洛网站增强 // @author Sunse666 // @match https://www.nuomill.com/* // @grant none // @run-at document-end // ==/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(); } }); // Init let attempts = 0; function init() { setupChannelPanel(); if (location.pathname === '/' || location.pathname === '') { // On homepage, try to select pending channel with retry var channelAttempts = 0; function tryChannel() { trySelectChannel(); channelAttempts++; if (channelAttempts < 15) setTimeout(tryChannel, 400); } tryChannel(); } if (location.pathname.indexOf('/video/') === 0) { if (mountUI()) return; attempts++; if (attempts < 50) setTimeout(init, 200); } } // 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; attempts = 0; currentSpeed = savedSpeed; // Re-initialize after Vue renders the new page setTimeout(init, 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 === 'complete') { setTimeout(init, 600); } else { window.addEventListener('load', () => setTimeout(init, 600)); } })();