コメントの少ないところだけ自動で早送りする、忙しい人のためのZenzaWatch拡張
// ==UserScript== // @name HeatSync // @namespace https://github.com/segabito/ // @description コメントの少ないところだけ自動で早送りする、忙しい人のためのZenzaWatch拡張 // @match *://www.nicovideo.jp/* // @match *://ext.nicovideo.jp/ // @match *://ext.nicovideo.jp/#* // @match *://ch.nicovideo.jp/* // @match *://com.nicovideo.jp/* // @match *://commons.nicovideo.jp/* // @match *://dic.nicovideo.jp/* // @exclude *://ads*.nicovideo.jp/* // @exclude *://www.upload.nicovideo.jp/* // @exclude *://www.nicovideo.jp/watch/*?edit=* // @exclude *://ch.nicovideo.jp/tool/* // @exclude *://flapi.nicovideo.jp/* // @exclude *://dic.nicovideo.jp/p/* // @version 0.0.18 // @grant none // @author segabito macmoto // @license public domain // @noframes // ==/UserScript== /* eslint-disable */ (() => { 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; function EmitterInitFunc() { class Handler { //extends Array { constructor(...args) { this._list = args; } get length() { return this._list.length; } exec(...args) { if (!this._list.length) { return; } else if (this._list.length === 1) { this._list[0](...args); return; } for (let i = this._list.length - 1; i >= 0; i--) { this._list[i](...args); } } execMethod(name, ...args) { if (!this._list.length) { return; } else if (this._list.length === 1) { this._list[0][name](...args); return; } for (let i = this._list.length - 1; i >= 0; i--) { this._list[i][name](...args); } } add(member) { if (this._list.includes(member)) { return this; } this._list.unshift(member); return this; } remove(member) { this._list = this._list.filter(m => m !== member); return this; } clear() { this._list.length = 0; return this; } get isEmpty() { return this._list.length < 1; } *[Symbol.iterator]() { const list = this._list || []; for (const member of list) { yield member; } } next() { return this[Symbol.iterator](); } } Handler.nop = () => {/* ( ˘ω˘ ) スヤァ */}; const PromiseHandler = (() => { const id = function() { return `Promise${this.id++}`; }.bind({id: 0}); class PromiseHandler extends Promise { constructor(callback = () => {}) { const key = new Object({id: id(), callback, status: 'pending'}); const cb = function(res, rej) { const resolve = (...args) => { this.status = 'resolved'; this.value = args; res(...args); }; const reject = (...args) => { this.status = 'rejected'; this.value = args; rej(...args); }; if (this.r###lt) { return this.r###lt.then(resolve, reject); } Object.assign(this, {resolve, reject}); return callback(resolve, reject); }.bind(key); super(cb); this.resolve = this.resolve.bind(this); this.reject = this.reject.bind(this); this.key = key; } resolve(...args) { if (this.key.resolve) { this.key.resolve(...args); } else { this.key.r###lt = Promise.resolve(...args); } return this; } reject(...args) { if (this.key.reject) { this.key.reject(...args); } else { this.key.r###lt = Promise.reject(...args); } return this; } addCallback(callback) { Promise.resolve().then(() => callback(this.resolve, this.reject)); return this; } } return PromiseHandler; })(); const {Emitter} = (() => { let totalCount = 0; let warnings = []; class Emitter { on(name, callback) { if (!this._events) { Emitter.totalCount++; this._events = new Map(); } name = name.toLowerCase(); let e = this._events.get(name); if (!e) { const handler = new Handler(callback); handler.name = name; e = this._events.set(name, handler); } else { e.add(callback); } if (e.length > 10) { console.warn('listener count > 10', name, e, callback); !Emitter.warnings.includes(this) && Emitter.warnings.push(this); } return this; } off(name, callback) { if (!this._events) { return; } name = name.toLowerCase(); const e = this._events.get(name); if (!this._events.has(name)) { return; } else if (!callback) { this._events.delete(name); } else { e.remove(callback); if (e.isEmpty) { this._events.delete(name); } } if (this._events.size < 1) { delete this._events; } return this; } once(name, func) { const wrapper = (...args) => { func(...args); this.off(name, wrapper); wrapper._original = null; }; wrapper._original = func; return this.on(name, wrapper); } clear(name) { if (!this._events) { return; } if (name) { this._events.delete(name); } else { delete this._events; Emitter.totalCount--; } return this; } emit(name, ...args) { if (!this._events) { return; } name = name.toLowerCase(); const e = this._events.get(name); if (!e) { return; } e.exec(...args); return this; } emitAsync(...args) { if (!this._events) { return; } setTimeout(() => this.emit(...args), 0); return this; } promise(name, callback) { if (!this._promise) { this._promise = new Map; } const p = this._promise.get(name); if (p) { return callback ? p.addCallback(callback) : p; } this._promise.set(name, new PromiseHandler(callback)); return this._promise.get(name); } emitResolve(name, ...args) { if (!this._promise) { this._promise = new Map; } if (!this._promise.has(name)) { this._promise.set(name, new PromiseHandler()); } return this._promise.get(name).resolve(...args); } emitReject(name, ...args) { if (!this._promise) { this._promise = new Map; } if (!this._promise.has(name)) { this._promise.set(name, new PromiseHandler); } return this._promise.get(name).reject(...args); } resetPromise(name) { if (!this._promise) { return; } this._promise.delete(name); } hasPromise(name) { return this._promise && this._promise.has(name); } addEventListener(...args) { return this.on(...args); } removeEventListener(...args) { return this.off(...args);} } Emitter.totalCount = totalCount; Emitter.warnings = warnings; return {Emitter}; })(); return {Handler, PromiseHandler, Emitter}; } const {Handler, PromiseHandler, Emitter} = EmitterInitFunc(); const {util} = (function() { const util = {}; 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; }; util.emitter = new Emitter; return {util, Emitter}; })(PRODUCT); product.util = util; const ZenzaDetector = (() => { const promise = (window.ZenzaWatch && window.ZenzaWatch.ready) ? Promise.resolve(window.ZenzaWatch) : new Promise(resolve => { [window, (document.body || document.documentElement)] .forEach(e => e.addEventListener('ZenzaWatchInitialize', () => { resolve(window.ZenzaWatch); })); }); return {detect: () => promise}; })(); const broadcast = (() => { if (!window.BroadcastChannel) { return {send: () => {}}; } 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': 1.7, 'turbo.dmc-blue': 1.7, 'turbo.minDuration': 30, 'turbo.ignoreTags': 'VOCALOID 音楽 作業用BGM 演奏してみた 歌ってみた' }; 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, JSON.stringify(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) => { //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)); util.emitter.on('zenzaOpen', this._onZenzaOpen.bind(this)); util.emitter.on('broadcast', this._onBroadcast.bind(this)); } enable() { if (this._timer) { return; } console.info('start timer', this._timer, this._rate); //, this._map); this._enabled = true; this._timer = setInterval(this._onTimer.bind(this), 500); } disable() { clearInterval(this._timer); this._rate = config.getValue('turbo.red'); if (this._enabled && config.getValue('turbo.enabled')) { window.ZenzaWatch.config.setValue('playbackRate', this._rate); } this._enabled = false; this._timer = null; } _onZenzaOpen() { if (this._dialog || !window.ZenzaWatch.debug.dialog) { return; } this._dialog = window.ZenzaWatch.debug.dialog; this._dialog.on('loadVideoInfo', this._onVideoInfoLoad.bind(this)); } _onZenzaClose() { this.disable(); } _onVideoInfoLoad(videoInfo) { const tags = (videoInfo.tagList || []) .map(t => { return t.tag.toUpperCase(); }); this._tags = tags; } _onHeatMapUpdate({map, duration}) { this._map = map; this._duration = duration; this._rate = config.getValue('turbo.red'); if (duration < config.getValue('turbo.minDuration')) { window.console.log('disable HeatSync by duration', duration); return this.disable(); } //if (this._videoElement && this._videoElement.playbackRate < this._rate) { // window.console.log('disable HeatSync by playbackRate', // this._videoElement.playbackRate); // return this.disable(); //} const currentTags = this._tags || []; const ignoreTags = config.getValue('turbo.ignoreTags').split(/[ ]/); if (currentTags.some(t => { return ignoreTags.includes(t.toUpperCase()); })) { window.console.log('disable HeatSync by tag'); //, currentTags, ignoreTags); return this.disable(); } this.enable(); } _onTimer() { //if (!this._videoElement) { this._videoElement = window.ZenzaWatch.external.getVideoElement(); if (!this._videoElement) { return; } //} const video = this._videoElement; const isEconomy = /smile\?m=[\d\.]+low$/.test(video.src); this._lastEnabled = config.getValue('turbo.enabled'); if (video.paused || !this._lastEnabled || isEconomy) { return; } const duration = video.duration; const current = video.currentTime; const per = current / duration; const perNear = Math.min(duration, current + 3) / duration; const isDmc = /dmc\.nico/.test(video.src); const map = this._map; const pos = Math.floor(map.length * per); const posNear = Math.floor(map.length * perNear); const blue = parseFloat(isDmc ? config.getValue('turbo.dmc-blue') : config.getValue('turbo.smile-blue')); const red = parseFloat(config.getValue('turbo.red')); const pt = Math.max(map[pos], map[posNear]); 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 (isNaN(rate)) { return; } if (Math.abs(rate - this._rate) < 0.05) { return; } // ユーザーが自分でスロー再生してるっぽい時は何もしない if (video.playbackRate < red) { return; } // スローは即時、加速はちょっと遅く反映 this._rate = rate > this._rate ? (rate * 2 + this._rate) / 3 : rate; window.ZenzaWatch.config.setValue('playbackRate', Math.floor(this._rate * 100) / 100); } _onBroadcast() { const lastEnabled = this._lastEnabled; window.setTimeout(() => { const currentEnabled = config.getValue('turbo.enabled'); if (lastEnabled && !currentEnabled) { this.disable(); } }, 1000); } } 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.append(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)); } get view() { return this._view; } _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"]'); this._elm.ignores = v.querySelector('*[data-config-name="turbo.ignoreTags"]'); const onChange = (e) => { const target = e.target, name = target.getAttribute('data-config-name'); switch (target.tagName) { case 'INPUT': case 'SELECT': if (target.type === 'checkbox') { config.setValue(name, target.checked); } else { 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); this._elm.ignores.addEventListener('change', onChange); v.querySelector('.closeButton') .addEventListener('click', this.hide.bind(this)); } _onClick(e) { super._onClick(e); e.stopPropagation(); } _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.ignores.value = '' + config.getValue('turbo.ignoreTags'); 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; pointer-events: auto !important; } .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; } .ignoreTags { margin: 8px; } .ignoreTags input { margin: auto; width: 100%; font-size: 110%; } .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="ignoreTags"> <label> このタグが含まれる動画では無効(スペース区切) <input type="text" data-config-name="turbo.ignoreTags"> </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(); class ToggleButton extends BaseViewComponent { constructor({parentNode}) { super({ parentNode, name: 'HeatSyncToggleButton', shadow: ToggleButton.__shadow__, template: '<div class="HeatSyncToggleButtonContainer"></div>', css: '' }); this._state = { isEnabled: undefined }; config.on('turbo.enabled', () => { this.refresh(); }); } refresh() { this.setState({isEnabled: config.getValue('turbo.enabled')}); } } ToggleButton.__shadow__ = ` <style> .controlButton { position: relative; display: inline-block; transition: opacity 0.4s ease, margin-left 0.2s ease, margin-top 0.2s ease; box-sizing: border-box; text-align: center; cursor: pointer; color: #fff; opacity: 0.8; vertical-align: middle; } .controlButton:hover { text-shadow: 0 0 8px #ff9; cursor: pointer; opacity: 1; } .heatSyncSwitch { font-size: 16px; width: 32px; height: 32px; line-height: 30px; cursor: pointer; } .is-Enabled .controlButtonInner { color: #aef; text-shadow: 0 0 4px #fea, 0 0 8px orange; } .controlButton .tooltip { display: none; pointer-events: none; position: absolute; left: 16px; top: -30px; transform: translate(-50%, 0); font-size: 12px; line-height: 16px; padding: 2px 4px; border: 1px solid !000; background: #ffc; color: #000; text-shadow: none; white-space: nowrap; z-index: 100; opacity: 0.8; } .controlButton:hover { background: #222; } .controlButton:hover .tooltip { display: block; opacity: 1; } </style> <div class="heatSyncSwitch controlButton root command" data-command="toggleHeatSyncDialog"> <div class="controlButtonInner" title="HeatSync">HS</div> <div class="tooltip">HeatSync</div> </div> `.trim(); const initExternal = (syncer) => { product.external = { syncer }; product.isReady = true; const ev = new CustomEvent(`${PRODUCT}Initialized`, { detail: { product } }); document.body.dispatchEvent(ev); }; let configPanel; const initDom = async (ZenzaWatch) => { 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(); }); const header = document.querySelector('#siteHeaderRightMenuContainer'); header && header.appendChild(li); const initButton = (container, handler) => { const toggleButton = new ToggleButton({parentNode: container}); product.toggleButton = toggleButton; toggleButton.on('command', handler); if (!configPanel) { configPanel = new ConfigPanel({parentNode: document.querySelector('.zenzaPlayerContainer')}); } toggleButton.refresh(); }; if (ZenzaWatch.emitter.promise) { const {container, handler} = await ZenzaWatch.emitter.promise('videoControBar.addonMenuReady'); initButton(container, handler); } else { ZenzaWatch.emitter.on('videoControBar.addonMenuReady', initButton); } }; const init = () => { let syncer; console.log('init HeatSync...'); ZenzaDetector.detect().then(() => { const ZenzaWatch = window.ZenzaWatch; ZenzaWatch.emitter.on('DialogPlayerOpen', () => { util.emitter.emit('zenzaOpen'); if (configPanel) { document.querySelector('.zenzaPlayerContainer').append(configPanel.view); } }); ZenzaWatch.emitter.on('DialogPlayerClose', () => { util.emitter.emit('zenzaClose'); if (configPanel) { document.body.append(configPanel.view); } }); ZenzaWatch.emitter.on('heatMapUpdate', (p) => { util.emitter.emit('heatMapUpdate', p); }); ZenzaWatch.emitter.on('command-toggleHeatSyncDialog', () => { if (!configPanel) { configPanel = new ConfigPanel({parentNode: document.querySelector('.zenzaPlayerContainer')}); } configPanel.toggle(); }); initDom(ZenzaWatch); //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); })(); })();