// ==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.result) {
return this.result.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.result = Promise.resolve(...args);
}
return this;
}
reject(...args) {
if (this.key.reject) {
this.key.reject(...args);
} else {
this.key.result = 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);
})();
})();