您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
コメントの少ないところだけ自動で早送りする、忙しい人のためのZenzaWatch拡張
当前为
// ==UserScript== // @name HeatSync // @namespace https://github.com/segabito/ // @description コメントの少ないところだけ自動で早送りする、忙しい人のためのZenzaWatch拡張 // @match http://www.nicovideo.jp/* // @match http://ext.nicovideo.jp/ // @match http://ext.nicovideo.jp/#* // @match http://ch.nicovideo.jp/* // @match http://com.nicovideo.jp/* // @match http://commons.nicovideo.jp/* // @match http://dic.nicovideo.jp/* // @exclude http://ads*.nicovideo.jp/* // @exclude http://www.upload.nicovideo.jp/* // @exclude http://www.nicovideo.jp/watch/*?edit=* // @exclude http://ch.nicovideo.jp/tool/* // @exclude http://flapi.nicovideo.jp/* // @exclude http://dic.nicovideo.jp/p/* // @version 0.0.5 // @grant none // @author segabito macmoto // @license public domain // @noframes // ==/UserScript== (function() { const PRODUCT = 'HeatSync'; const monkey = function(PRODUCT) { const console = window.console; let ZenzaWatch = null; //const $ = window.jQuery; console.log(`exec ${PRODUCT}..`); const CONSTANT = { BASE_Z_INDEX: 150000 }; const product = {debug: {_const: CONSTANT}}; window[PRODUCT] = product; const {util, Emitter} = (function() { const util = {}; class Emitter { constructor() { } on(name, callback) { if (!this._events) { this._events = {}; } name = name.toLowerCase(); if (!this._events[name]) { this._events[name] = []; } this._events[name].push(callback); } clear(name) { if (!this._events) { this._events = {}; } if (name) { this._events[name] = []; } else { this._events = {}; } } emit(name) { if (!this._events) { this._events = {}; } name = name.toLowerCase(); if (!this._events.hasOwnProperty(name)) { return; } const e = this._events[name]; const arg = Array.prototype.slice.call(arguments, 1); for (let i =0, len = e.length; i < len; i++) { e[i].apply(null, arg); } } emitAsync(...args) { window.setTimeout(() => { this.emit(...args); }, 0); } } util.emitter = new Emitter(); util.addStyle = function(styles, id) { var elm = document.createElement('style'); elm.type = 'text/css'; if (id) { elm.id = id; } var text = styles.toString(); text = document.createTextNode(text); elm.appendChild(text); var head = document.getElementsByTagName('head'); head = head[0]; head.appendChild(elm); return elm; }; util.mixin = function(self, o) { _.each(Object.keys(o), f => { if (!_.isFunction(o[f])) { return; } if (_.isFunction(self[f])) { return; } self[f] = o[f].bind(o); }); }; util.attachShadowDom = function({host, tpl, mode = 'open'}) { const root = host.attachShadow ? host.attachShadow({mode}) : host.createShadowRoot(); const node = document.importNode(tpl.content, true); root.appendChild(node); return root; }; util.getWatchId = function(url) { /\/?watch\/([a-z0-9]+)/.test(url || location.pathname); return RegExp.$1; }; util.isLogin = function() { return document.getElementsByClassName('siteHeaderLogin').length < 1; }; util.escapeHtml = function(text) { var map = { '&': '&', '\x27': ''', '"': '"', '<': '<', '>': '>' }; return text.replace(/[&"'<>]/g, char => { return map[char]; }); }; util.unescapeHtml = function(text) { var map = { '&' : '&' , ''' : '\x27', '"' : '"', '<' : '<', '>' : '>' }; return text.replace(/(&|'|"|<|>)/g, char => { return map[char]; }); }; util.escapeRegs = function(text) { const map = { '\\': '\\\\', '*': '\\*', '+': '\\+', '.': '\\.', '?': '\\?', '{': '\\{', '}': '\\}', '(': '\\(', ')': '\\)', '[': '\\[', ']': '\\]', '^': '\\^', '$': '\\$', '-': '\\-', '|': '\\|', '/': '\\/', }; return text.replace(/[\\\*\+\.\?\{\}\(\)\[\]\^\$\-\|\/]/g, char => { return map[char]; }); }; util.hasLargeThumbnail = function(videoId) { // return true; // 大サムネが存在する最初の動画ID。 ソースはちゆ12歳 // ※この数字以降でもごく稀に例外はある。 var threthold = 16371888; var cid = videoId.substr(0, 2); if (cid !== 'sm') { return false; } var fid = videoId.substr(2) * 1; if (fid < threthold) { return false; } return true; }; const videoIdReg = /^[a-z]{2}\d+$/; util.getThumbnailUrlByVideoId = function(videoId) { if (!videoIdReg.test(videoId)) { return null; } const fileId = parseInt(videoId.substr(2), 10); const num = (fileId % 4) + 1; const large = util.hasLargeThumbnail(videoId) ? '.L' : ''; return '//tn-skr' + num + '.smilevideo.jp/smile?i=' + fileId + large; }; util.isFirefox = function() { return navigator.userAgent.toLowerCase().indexOf('firefox') >= 0; }; return {util, Emitter}; })(PRODUCT); product.util = util; const ZenzaDetector = (function() { let isReady = false; const emitter = new Emitter(); const onZenzaReady = () => { isReady = true; ZenzaWatch = window.ZenzaWatch; emitter.emit('ready', window.ZenzaWatch); }; if (window.ZenzaWatch && window.ZenzaWatch.ready) { window.console.log('ZenzaWatch is Ready'); isReady = true; } else { document.body.addEventListener('ZenzaWatchInitialize', () => { window.console.log('ZenzaWatchInitialize'); onZenzaReady(); }); } const detect = function() { return new Promise(res => { if (isReady) { return res(window.ZenzaWatch); } emitter.on('ready', () => { res(window.ZenzaWatch); }); }); }; return {detect}; })(); const broadcast = (() => { const bc = new window.BroadcastChannel(PRODUCT); const onMessage = (e) => { const packet = e.data; //console.log('%creceive message', 'background: cyan;', packet); util.emitter.emit('broadcast', packet); }; const send = (packet) => { //console.log('%csend message', 'background: cyan;', packet); bc.postMessage(packet); }; bc.addEventListener('message', onMessage); return { send }; })(); const config = (function() { const prefix = PRODUCT + '_config_'; const emitter = new Emitter(); const defaultConfig = { debug: false, 'turbo.enabled': true, 'turbo.red': 1, 'turbo.smile-blue': 2, 'turbo.dmc-blue': 1.7, 'turbo.minDuration': 30, }; const config = {}; let noEmit = false; emitter.refresh = (emitChange = false) => { Object.keys(defaultConfig).forEach(key => { var storageKey = prefix + key; if (localStorage.hasOwnProperty(storageKey)) { try { let lastValue = config[key]; let newValue = JSON.parse(localStorage.getItem(storageKey)); if (lastValue !== newValue) { config[key] = newValue; if (emitChange) { emitter.emit('key', newValue); emitter.emit('@update', {key, value: newValue}); } } } catch (e) { window.console.error('config parse error key:"%s" value:"%s" ', key, localStorage.getItem(storageKey), e); config[key] = defaultConfig[key]; } } else { config[key] = defaultConfig[key]; } }); }; emitter.refresh(); emitter.getValue = function(key, refresh) { if (refresh) { emitter.refreshValue(key); } return config[key]; }; emitter.setValue = function(key, value) { if (config[key] !== value && arguments.length >= 2) { var storageKey = prefix + key; localStorage.setItem(storageKey, value); config[key] = value; emitter.emit(key, value); emitter.emit('@update', {key, value}); broadcast.send('configUpdate'); //console.log('%cconfig update "%s" = "%s"', 'background: cyan', key, value); } }; emitter.clearConfig = function() { noEmit = true; Object.keys(defaultConfig).forEach(key => { if (_.contains(['message', 'debug'], key)) { return; } var storageKey = prefix + key; try { if (localStorage.hasOwnProperty(storageKey)) { localStorage.removeItem(storageKey); } config[key] = defaultConfig[key]; } catch (e) {} }); noEmit = false; }; emitter.getKeys = function() { return Object.keys(defaultConfig); }; emitter.namespace = function(name) { return { getValue: (key) => { return emitter.getValue(name + '.' + key); }, setValue: (key, value) => { emitter.setValue(name + '.' + key, value); }, refresh: () => { emitter.refresh(); }, on: (key, func) => { if (key === '@update') { emitter.on('@update', ({key, value}) => { const pre = name + '.'; //console.log('@update', key, value, pre); if (key.startsWith(pre)) { func({key: key.replace(pre, ''), value}); } }); } else { emitter.on(name + '.' + key, func); } } }; }; util.emitter.on('broadcast', (type) => { console.log('%cbroadcast "%s"', 'background: cyan', type); //if (type !== 'configUpdate') { return; } emitter.refresh(false); emitter.emit('refresh'); }); return emitter; })(); product.config = config; class Syncer extends Emitter { constructor() { super(); this._timer = null; this._videoElement = null; this._rate = 1.0; this._config = config.namespace('turbo'); util.emitter.on('heatMapUpdate', this._onHeatMapUpdate.bind(this)); util.emitter.on('zenzaClose', this._onZenzaClose.bind(this)); } enable() { if (this._timer) { return; } console.info('start timer', this._timer, this._rate, this._map); this._timer = setInterval(this._onTimer.bind(this), 500); } disable() { clearInterval(this._timer); this._rate = config.getValue('turbo.red'); window.ZenzaWatch.config.setValue('playbackRate', this._rate); this._timer = null; } _onZenzaClose() { this.disable(); } _onHeatMapUpdate({map, duration}) { this._map = map; this._duration = duration; this._rate = config.getValue('turbo.red'); if (duration < config.getValue('turbo.minDuration')) { return this.disable(); } this.enable(); } _onTimer() { if (!this._videoElement) { this._videoElement = window.ZenzaWatch.external.getVideoElement(); if (!this._videoElement) { return; } } const video = this._videoElement; if (video.paused || !config.getValue('turbo.enabled')) { return; } const duration = video.duration; const current = video.currentTime; const per = current / duration; const isDmc = /dmc\.nico/.test(video.src); const map = this._map; const pos = Math.floor(map.length * per); const blue = parseFloat(isDmc ? config.getValue('turbo.dmc-blue') : config.getValue('turbo.smile-blue')); const red = parseFloat(config.getValue('turbo.red')); const pt = map[pos]; let ratePer = (256 - pt) / 256; if (ratePer > 0.95) { ratePer = 1; } if (ratePer < 0.4) { ratePer = 0; } let rate = red + (blue - red) * ratePer; rate = Math.round(rate * 100) / 100; //console.info('onTimer', pt, pt / 255, Math.round(ratePer * 100) / 100, rate); if (Math.abs(rate - this._rate) < 0.05) { return; } this._rate = rate; // ユーザーが自分でスロー再生してるっぽい時は何もしない if (video.playbackRate < red) { return; } window.ZenzaWatch.config.setValue('playbackRate', this._rate); } } class BaseViewComponent extends Emitter { constructor({parentNode = null, name = '', template = '', shadow = '', css = ''}) { super(); this._params = {parentNode, name, template, shadow, css}; this._bound = {}; this._state = {}; this._props = {}; this._elm = {}; this._initDom({ parentNode, name, template, shadow, css }); } _initDom({parentNode, name, template, css = '', shadow = ''}) { let tplId = `${PRODUCT}${name}Template`; let tpl = document.getElementById(tplId); if (!tpl) { if (css) { util.addStyle(css, `${name}Style`); } tpl = document.createElement('template'); tpl.innerHTML = template; tpl.id = tplId; document.body.appendChild(tpl); } const onClick = this._bound.onClick = this._onClick.bind(this); const view = document.importNode(tpl.content, true); this._view = view.querySelector('*') || document.createDocumentFragment(); if (this._view) { this._view.addEventListener('click', onClick); } this.appendTo(parentNode); if (shadow) { this._attachShadow({host: this._view, name, shadow}); if (!this._isDummyShadow) { this._shadow.addEventListener('click', onClick); } } } _attachShadow ({host, shadow, name, mode = 'open'}) { let tplId = `${PRODUCT}${name}Shadow`; let tpl = document.getElementById(tplId); if (!tpl) { tpl = document.createElement('template'); tpl.innerHTML = shadow; tpl.id = tplId; document.body.appendChild(tpl); } if (!host.attachShadow && !host.createShadowRoot) { return this._fallbackNoneShadowDom({host, tpl, name}); } const root = host.attachShadow ? host.attachShadow({mode}) : host.createShadowRoot(); const node = document.importNode(tpl.content, true); root.appendChild(node); this._shadowRoot = root; this._shadow = root.querySelector('.root'); this._isDummyShadow = false; } _fallbackNoneShadowDom({host, tpl, name}) { const node = document.importNode(tpl.content, true); const style = node.querySelector('style'); style.remove(); util.addStyle(style.innerHTML, `${name}Shadow`); host.appendChild(node); this._shadow = this._shadowRoot = host.querySelector('.root'); this._isDummyShadow = true; } setState(key, val) { if (typeof key === 'string') { this._setState(key, val); } Object.keys(key).forEach(k => { this._setState(k, key[k]); }); } _setState(key, val) { if (this._state[key] !== val) { this._state[key] = val; if (/^is(.*)$/.test(key)) { this.toggleClass(`is-${RegExp.$1}`, !!val); } this.emit('update', {key, val}); } } _onClick(e) { const target = e.target.classList.contains('command') ? e.target : e.target.closest('.command'); if (!target) { return; } const command = target.getAttribute('data-command'); if (!command) { return; } const type = target.getAttribute('data-type') || 'string'; let param = target.getAttribute('data-param'); e.stopPropagation(); e.preventDefault(); param = this._parseParam(param, type); this._onCommand(command, param); } _parseParam(param, type) { switch (type) { case 'json': case 'bool': case 'number': param = JSON.parse(param); break; } return param; } appendTo(parentNode) { if (!parentNode) { return; } this._parentNode = parentNode; parentNode.appendChild(this._view); } _onCommand(command, param) { this.emit('command', command, param); } toggleClass(className, v) { (className || '').split(/ +/).forEach((c) => { if (this._view && this._view.classList) { this._view.classList.toggle(c, v); } if (this._shadow && this._shadow.classList) { this._shadow.classList.toggle(c, this._view.classList.contains(c)); } }); } addClass(name) { this.toggleClass(name, true); } removeClass(name) { this.toggleClass(name, false); } } class ConfigPanel extends BaseViewComponent { constructor({parentNode}) { super({ parentNode, name: 'HeatSyncConfigPanel', shadow: ConfigPanel.__shadow__, template: '<div class="HeatSyncConfigPanelContainer"></div>', css: '' }); this._state = { isOpen: false, isVisible: false }; config.on('refresh', this._onBeforeShow.bind(this)); } _initDom(...args) { super._initDom(...args); const v = this._shadow; this._elm.red = v.querySelector('*[data-config-name="turbo.red"]'); this._elm.dmc = v.querySelector('*[data-config-name="turbo.dmc-blue"]'); this._elm.smile = v.querySelector('*[data-config-name="turbo.smile-blue"]'); this._elm.minDur = v.querySelector('*[data-config-name="turbo.minDuration"]'); this._elm.enabled = v.querySelector('*[data-config-name="turbo.enabled"]'); const onChange = (e) => { const target = e.target, name = target.getAttribute('data-config-name'); switch (target.tagName) { case 'INPUT': case 'SELECT': const type = target.getAttribute('data-type'); const value = this._parseParam(target.value, type); config.setValue(name, value); break; default: //console.info('target', e, target, name, target.checked); config.setValue(name, !!target.checked); break; } }; this._elm.red .addEventListener('change', onChange); this._elm.dmc .addEventListener('change', onChange); this._elm.smile .addEventListener('change', onChange); this._elm.minDur .addEventListener('change', onChange); this._elm.enabled.addEventListener('change', onChange); v.querySelector('.closeButton') .addEventListener('click', this.hide.bind(this)); } _onClick(e) { super._onClick(e); } _onMouseDown(e) { this.hide(); this._onClick(e); } show() { document.body.addEventListener('click', this._bound.onBodyClick); this._onBeforeShow(); this.setState({isOpen: true}); window.setTimeout(() => { this.setState({isVisible: true}); }, 100); } hide() { document.body.removeEventListener('click', this._bound.onBodyClick); this.setState({isVisible: false}); window.setTimeout(() => { this.setState({isOpen: false}); }, 2100); } toggle() { if (this._state.isOpen) { this.hide(); } else { this.show(); } } _onBeforeShow() { this._elm.red.value = '' + config.getValue('turbo.red'); this._elm.dmc.value = '' + config.getValue('turbo.dmc-blue'); this._elm.smile.value = '' + config.getValue('turbo.smile-blue'); this._elm.minDur.value = '' + config.getValue('turbo.minDuration'); this._elm.enabled.checked = !!config.getValue('turbo.enabled'); } } ConfigPanel.__shadow__ = (` <style> .HeatSyncConfigPanel { display: none; position: fixed; z-index: ${CONSTANT.BASE_Z_INDEX}; top: 50vh; left: 50vw; padding: 8px; border: 2px outset; box-shadow: 0 0 8px #000; background: #ccc; transform: translate(-50%, -50%); /*transform: translate(-50%, -50%) perspective(200px) rotateX(90deg);*/ transition: opacity 0.5s; transform-origin: center bottom; animation-timing-function: steps(10); perspective-origin: center bottom; user-select: none; -webkit-user-select: none; -moz-user-select: none; } .HeatSyncConfigPanel.is-Open { display: block; opacity: 0; /*animation-name: dokahide;*/ } .HeatSyncConfigPanel.is-Open.is-Visible { opacity: 1; /*animation-name: dokashow;*/ /*transform: translate(-50%, -50%) perspective(200px) rotateX(0deg);*/ } @keyframes dokashow { 0% { opacity: 1; transform: translate(-50%, -50%) perspective(200px) rotateX(90deg); } 100% { opacity: 1; transform: translate(-50%, -50%) perspective(200px) rotateX(0deg); } } @keyframes dokahide { 0% { opacity: 1; transform: translate(-50%, -50%) perspective(200px) rotateX(0deg); } 99% { opacity: 1; transform: translate(-50%, -50%) perspective(200px) rotateX(90deg); } 100% { opacity: 0; } } .title { margin: 0; font-weight: bolder; font-size: 120%; } .speedSelect { margin: 8px; } .minDuration { margin: 8px; } .enableSelect { margin: 8px; } .closeButton { display: block; text-align: center; } .closeButton { display: block; pading: 8px; cursor: pointer; margin: auto; } label { cursor: pointer; } input[type="number"] { width: 50px; } </style> <div class="root HeatSyncConfigPanel"> <p class="title">†HeatSync†</p> <div class="speedSelect dmc"> <span>最高倍率(新仕様サーバー)</span> <select data-config-name="turbo.dmc-blue" data-type="number"> <option value="3">3.0</option> <option>2.9</option> <option>2.8</option> <option>2.7</option> <option>2.6</option> <option>2.5</option> <option>2.4</option> <option>2.3</option> <option>2.2</option> <option>2.1</option> <option value="2">2.0</option> <option>1.9</option> <option>1.8</option> <option>1.7</option> <option>1.6</option> <option>1.5</option> <option>1.4</option> <option>1.3</option> <option>1.2</option> <option>1.1</option> <option value="1">1</option> </select> </div> <div class="speedSelect smile"> <span>最高倍率(旧仕様サーバー)</span> <select data-config-name="turbo.smile-blue" data-type="number"> <option value="3">3.0</option> <option>2.9</option> <option>2.8</option> <option>2.7</option> <option>2.6</option> <option>2.5</option> <option>2.4</option> <option>2.3</option> <option>2.2</option> <option>2.1</option> <option value="2">2.0</option> <option>1.9</option> <option>1.8</option> <option>1.7</option> <option>1.6</option> <option>1.5</option> <option>1.4</option> <option>1.3</option> <option>1.2</option> <option>1.1</option> <option value="1">1</option> </select> </div> <div class="speedSelect minimum"> <span>最低倍率</span> <select data-config-name="turbo.red" data-type="number"> <option value="3">3.0</option> <option>2.9</option> <option>2.8</option> <option>2.7</option> <option>2.6</option> <option>2.5</option> <option>2.4</option> <option>2.3</option> <option>2.2</option> <option>2.1</option> <option value="2">2.0</option> <option>1.9</option> <option>1.8</option> <option>1.7</option> <option>1.6</option> <option>1.5</option> <option>1.4</option> <option>1.3</option> <option>1.2</option> <option>1.1</option> <option value="1">1.0</option> </select> </div> <div class="minDuration"> <label> <input type="number" data-config-name="turbo.minDuration" data-type="number"> 秒未満の動画には適用しない </label> </div> <div class="enableSelect"> <label> <input type="checkbox" data-config-name="turbo.enabled" data-type="bool"> HeatSyncを有効にする </label> </div> <div class="closeButtonContainer"> <button class="closeButton" type="button"> 閉じる </button> </div> </div> `).trim(); const initExternal = (syncer) => { product.external = { syncer }; product.isReady = true; const ev = new CustomEvent(`${PRODUCT}Initialized`, { detail: { product } }); document.body.dispatchEvent(ev); }; const initDom = () => { let configPanel; const li = document.createElement('li'); li.innerHTML = `<a href="javascript:;">†HeatSync†設定</a>`; li.addEventListener('click', () => { if (!configPanel) { configPanel = new ConfigPanel({parentNode: document.body}); } configPanel.toggle(); }); document.querySelector('#siteHeaderRightMenuContainer').appendChild(li); }; const init = () => { let syncer; ZenzaDetector.detect().then(() => { window.ZenzaWatch.emitter.on('DialogPlayerClose', () => { util.emitter.emit('zenzaClose'); }); window.ZenzaWatch.emitter.on('heatMapUpdate', (p) => { util.emitter.emit('heatMapUpdate', p); }); initDom(); //console.info('detect zenzawatch...'); syncer = new Syncer(); initExternal(syncer); }); }; init(); }; (() => { const script = document.createElement('script'); script.id = `${PRODUCT}Loader`; script.setAttribute('type', 'text/javascript'); script.setAttribute('charset', 'UTF-8'); script.appendChild(document.createTextNode( '(' + monkey + ')("' + PRODUCT + '");' )); document.body.appendChild(script); })(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址