// ==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 '
' + c.name + '
';
}).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));
}
})();