// ==UserScript==
// @name 加强糯米!
// @namespace https://www.nuomill.com
// @version 1.4
// @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.className = 'nml-speed-wrapper';
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;
// Defensive: remove any stale duplicates before creating a new one
var stale = document.querySelectorAll('.nml-speed-wrapper');
for (var si = 0; si < stale.length; si++) stale[si].remove();
speedBtn = null;
speedMenu = null;
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();
}
});
// 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, advMX = 50, advMY = 50, advEX = 100, advEY = 50;
let advSR = 0, advMR = 0, advER = 0;
let advEasing1 = 0, advEasing2 = 0;
let advTimeSM = 25, advTimeME = 25;
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 + 26 base-11 digits:
// SX,SY,MX,MY,EX,EY, SR,MR,ER, Easing1,Easing2, TimeSM,TimeME
var p = [advSX, advSY, advMX, advMY, advEX, advEY,
Math.round(advSR / 3), Math.round(advMR / 3), Math.round(advER / 3),
advEasing1, advEasing2, advTimeSM, advTimeME];
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)
// New extended format: 26 zero-width chars (13 values)
if (c0 === 0x2064 && text.length >= 27) {
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 mx = b11(zwVal(ch[i]), zwVal(ch[i+1])); i += 2;
var my = 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 mr = 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 e1 = b11(zwVal(ch[i]), zwVal(ch[i+1])); i += 2;
var e2 = b11(zwVal(ch[i]), zwVal(ch[i+1])); i += 2;
var ts = b11(zwVal(ch[i]), zwVal(ch[i+1])); i += 2;
var te = b11(zwVal(ch[i]), zwVal(ch[i+1]));
return { mode: 4, sx: sx, sy: sy, mx: mx, my: my, ex: ex, ey: ey,
sr: sr, mr: mr, er: er,
easing1: Math.min(e1, 10), easing2: Math.min(e2, 10),
timeSM: Math.min(ts, 100), timeME: Math.min(te, 100) };
}
// Old 14-char format (backward compatible — fills defaults for new fields)
if (c0 === 0x2064 && text.length >= 15) {
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]));
var avgX = Math.round((sx + ex) / 2);
var avgY = Math.round((sy + ey) / 2);
var avgR = Math.round((sr + er) / 2);
return { mode: 4, sx: sx, sy: sy, mx: avgX, my: avgY, ex: ex, ey: ey,
sr: sr, mr: avgR, er: er,
easing1: Math.min(ease, 10), easing2: Math.min(ease, 10),
timeSM: 25, timeME: 25 };
}
// 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, mx: x, my: y, ex: x, ey: y,
sr: r, mr: r, er: r,
easing1: 0, easing2: 0, timeSM: 25, timeME: 25 };
}
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 + ' mx=' + advMX + ' my=' + advMY +
' ex=' + advEX + ' ey=' + advEY + ' t1=' + (advTimeSM/10).toFixed(1) + 's t2=' + (advTimeME/10).toFixed(1) + 's' : '') +
' 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);
})();
// Lane tracking — prevent top/bottom/reverse danmaku overlap
var activeDmLanes = { 1: [], 2: [], 3: [] };
var DM_LANE_TOPS = {
1: [2,7,12,17,22,27,32,37,42],
2: [68,73,78,83,88,93],
3: [8,13,18,23,28,33,38,43,48,53,58,63,68,73,78,83,88]
};
function assignDmLane(mode, clone) {
var lanes = activeDmLanes[mode];
var tops = DM_LANE_TOPS[mode];
// Purge clones that have been removed from DOM
for (var i = lanes.length - 1; i >= 0; i--) {
if (!lanes[i].parentNode) lanes.splice(i, 1);
}
// Find first free top value
var used = {};
for (var j = 0; j < lanes.length; j++) used[lanes[j]._nmlLaneTop] = true;
var freeTop = null;
for (var k = 0; k < tops.length; k++) {
if (!used[tops[k]]) { freeTop = tops[k]; break; }
}
if (freeTop == null) {
// All occupied — evict oldest
var oldest = lanes.shift();
if (oldest.parentNode) oldest.remove();
freeTop = oldest._nmlLaneTop;
}
clone._nmlLaneTop = freeTop;
lanes.push(clone);
return freeTop;
}
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 >= 27 ? 27 : (text.length >= 15 ? 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) {
var laneTop = assignDmLane(data.mode, clone);
clone.style.cssText = base +
'left:50%!important;top:' + laneTop + '%!important;' +
'transform:translateX(-50%)!important;';
} else if (data.mode === 3) {
var laneTop3 = assignDmLane(3, clone);
var ss = document.querySelector('.danmaku-input-wrapper input[type=range]');
var dur = 720 / (parseFloat(ss?.value) || 120);
clone.style.cssText = base +
'top:' + laneTop3 + '%!important;animation:nml-reverse-dm ' + dur + 's linear forwards!important;';
} else if (data.mode === 4) {
var t1 = (data.timeSM || 25) / 10;
var t2 = (data.timeME || 25) / 10;
var totalMs = (t1 + t2) * 1000;
var midPct = t1 / (t1 + t2);
var mx = data.mx != null ? data.mx : (data.sx + data.ex) / 2;
var my = data.my != null ? data.my : (data.sy + data.ey) / 2;
var mr = data.mr != null ? data.mr : (data.sr + data.er) / 2;
var e1 = EASINGS[data.easing1 != null ? data.easing1 : (data.easing || 0)] || 'linear';
var e2 = EASINGS[data.easing2 != null ? data.easing2 : (data.easing || 0)] || 'linear';
clone.style.cssText = base;
try {
var anim = clone.animate([
{ left: data.sx + '%', top: data.sy + '%', transform: 'translate(-50%,-50%) rotate(' + data.sr + 'deg)', offset: 0 },
{ left: mx + '%', top: my + '%', transform: 'translate(-50%,-50%) rotate(' + mr + 'deg)', offset: midPct, easing: e1 },
{ left: data.ex + '%', top: data.ey + '%', transform: 'translate(-50%,-50%) rotate(' + data.er + 'deg)', offset: 1, easing: e2 }
], { duration: totalMs, fill: 'forwards' });
if (video) {
var syncFn = function sync() {
if (!clone.parentNode) return;
try {
if (video.paused) { if (anim.playState === 'running') anim.pause(); }
else if (anim.playState === 'paused') anim.play();
} catch (e) {}
requestAnimationFrame(sync);
};
requestAnimationFrame(syncFn);
}
anim.onfinish = function () {
clone.style.transition = 'opacity 0.3s';
clone.style.opacity = '0';
setTimeout(function () { if (clone.parentNode) clone.remove(); if (el.parentNode) el.remove(); }, 300);
};
} catch (e) {
el.style.display = '';
}
}
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) {
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 >= 27 ? 27 : (visibleText.length >= 15 ? 15 : 6));
console.log('[nml-dm] OBSERVE #' + danmakuObserveCount + ' mode=' + data.mode +
(data.mode === 4 ? ' sx=' + data.sx + ' sy=' + data.sy + ' mx=' + (data.mx||'?') + ' my=' + (data.my||'?') +
' 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 = [];
var dmListPanel = null;
var dmListExpanded = false;
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, mx: data.mx, my: data.my, ex: data.ex, ey: data.ey, sr: data.sr, mr: data.mr, er: data.er, easing1: data.easing1, easing2: data.easing2, timeSM: data.timeSM, timeME: data.timeME } : 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.mx != null ? d.params.mx : '?') + '%,' + (d.params.my != null ? d.params.my : '?') + '%) ' +
'→终点(' + d.params.ex + '%,' + d.params.ey + '%) ' +
'旋转' + d.params.sr + '°→' + (d.params.mr != null ? d.params.mr : '?') + '°→' + d.params.er + '° ' +
'缓1:' + (EASING_NAMES[d.params.easing1 != null ? d.params.easing1 : d.params.easing] || '?') + ' ' +
'缓2:' + (EASING_NAMES[d.params.easing2 != null ? d.params.easing2 : d.params.easing] || '?') + ' ' +
(d.params.timeSM != null ? (d.params.timeSM/10).toFixed(1) + 's' : '?') + '/' + (d.params.timeME != null ? (d.params.timeME/10).toFixed(1) + 's' : '?') + '
';
}
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 =
'' +
'';
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';
// Compact slider builder
function buildAdvSlider(cfg) {
var row = document.createElement('div');
row.className = 'nml-adv-col-row';
row.setAttribute('data-v-65d53e0f', '');
var disp = cfg.fmt ? cfg.fmt(cfg.val) : cfg.val + (cfg.unit || '');
row.innerHTML = '' + cfg.label + '' +
'' +
'' + disp + '';
var slider = row.querySelector('input');
var valSpan = row.querySelector('.setting-val');
slider.addEventListener('input', function () {
var v = parseInt(slider.value);
valSpan.textContent = cfg.fmt ? cfg.fmt(v) : v + (cfg.unit || '');
cfg.set(v);
});
return row;
}
// Three-column waypoint layout
var columns = document.createElement('div');
columns.className = 'nml-adv-columns';
var waypoints = [
{ title: '起点', cfgs: [
{ 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 advSR; }, set: function(v) { advSR = v; }, label: '∠', min: 0, max: 360, step: 3, val: advSR, unit: '°' }
]},
{ title: '中间', cfgs: [
{ ref: function() { return advMX; }, set: function(v) { advMX = v; }, label: 'X', min: 0, max: 100, val: advMX, unit: '%' },
{ ref: function() { return advMY; }, set: function(v) { advMY = v; }, label: 'Y', min: 0, max: 100, val: advMY, unit: '%' },
{ ref: function() { return advMR; }, set: function(v) { advMR = v; }, label: '∠', min: 0, max: 360, step: 3, val: advMR, unit: '°' }
]},
{ title: '终点', cfgs: [
{ 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 advER; }, set: function(v) { advER = v; }, label: '∠', min: 0, max: 360, step: 3, val: advER, unit: '°' }
]}
];
waypoints.forEach(function (wp) {
var col = document.createElement('div');
col.className = 'nml-adv-col';
var title = document.createElement('div');
title.className = 'nml-adv-col-title';
title.textContent = wp.title;
col.appendChild(title);
wp.cfgs.forEach(function (cfg) { col.appendChild(buildAdvSlider(cfg)); });
columns.appendChild(col);
});
advControls.appendChild(columns);
// Timing row (full width)
var timeRow = document.createElement('div');
timeRow.className = 'nml-adv-time-row';
var timeCfgs = [
{ ref: function() { return advTimeSM; }, set: function(v) { advTimeSM = v; }, label: '起→中', min: 5, max: 100, val: advTimeSM, fmt: function(v) { return (v/10).toFixed(1) + 's'; } },
{ ref: function() { return advTimeME; }, set: function(v) { advTimeME = v; }, label: '中→终', min: 5, max: 100, val: advTimeME, fmt: function(v) { return (v/10).toFixed(1) + 's'; } }
];
timeCfgs.forEach(function (cfg) {
var d = document.createElement('div');
d.className = 'setting-row';
d.setAttribute('data-v-65d53e0f', '');
var disp = cfg.fmt ? cfg.fmt(cfg.val) : cfg.val + (cfg.unit || '');
d.innerHTML = '' + cfg.label + '' +
'' +
'' + disp + '';
var slider = d.querySelector('input');
var valSpan = d.querySelector('.setting-val');
slider.addEventListener('input', function () {
var v = parseInt(slider.value);
valSpan.textContent = cfg.fmt ? cfg.fmt(v) : v + (cfg.unit || '');
cfg.set(v);
});
timeRow.appendChild(d);
});
advControls.appendChild(timeRow);
// Easing selectors — two rows for start→middle and middle→end segments
function buildEasingRow(label, getter, setter) {
var row = document.createElement('div');
row.className = 'setting-row';
row.setAttribute('data-v-65d53e0f', '');
row.innerHTML = '' + label + '';
var btns = document.createElement('div');
btns.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 === getter() ? ' active' : '');
eb.textContent = name;
eb.addEventListener('mousedown', function (e) {
e.stopPropagation(); e.preventDefault();
setter(i);
btns.querySelectorAll('.nml-easing-btn').forEach(function (b) { b.classList.remove('active'); });
eb.classList.add('active');
});
btns.appendChild(eb);
});
row.appendChild(btns);
return row;
}
advControls.appendChild(buildEasingRow('缓动1', function() { return advEasing1; }, function(v) { advEasing1 = v; }));
advControls.appendChild(buildEasingRow('缓动2', function() { return advEasing2; }, function(v) { advEasing2 = v; }));
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);}' +
'.nml-adv-columns{display:flex;gap:6px;margin-top:2px;margin-bottom:4px;}' +
'.nml-adv-col{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}' +
'.nml-adv-col-title{text-align:center;font-size:11px;font-weight:700;color:var(--brand_pink,#ff6fae);margin-bottom:1px;padding:2px 0;border-bottom:1px solid var(--brand_pink,rgba(255,111,174,0.25));}' +
'.nml-adv-col-row{display:flex;align-items:center;gap:2px;}' +
'.nml-adv-col-row>span:first-child{font-size:10px;color:var(--text2,#999);width:14px;flex-shrink:0;text-align:center;}' +
'.nml-adv-col-row input[type=range]{flex:1;min-width:0;height:3px;}' +
'.nml-adv-col-row .setting-val{font-size:10px;color:var(--text2,#999);width:26px;flex-shrink:0;text-align:right;}' +
'.nml-adv-time-row{display:flex;gap:8px;margin-top:4px;}' +
'.nml-adv-time-row .setting-row{flex:1;}';
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('.nml-speed-wrapper');
if (old) old.remove();
videoObserver.disconnect();
speedBtn = null;
speedMenu = null;
videoEl = null;
mounted = false;
initAttempts = 0;
danmakuMode = 0;
dmList = [];
activeDmLanes = { 1: [], 2: [], 3: [] };
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);
}
})();