// ==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.10
// @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);
}
//window.setTimeout(() => {
// window.console.log('ZenzaWatch is Ready?', window.ZenzaWatch);
// if (window.ZenzaWatch && window.ZenzaWatch.ready && !isReady) {
// window.console.log('ZenzaWatch is Ready!');
// onZenzaReady();
// }
//}, 3000);
emitter.on('ready', () => {
res(window.ZenzaWatch);
});
});
};
return {detect};
})();
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._timer = setInterval(this._onTimer.bind(this), 500);
}
disable() {
clearInterval(this._timer);
this._rate = config.getValue('turbo.red');
if (config.getValue('turbo.enabled')) {
window.ZenzaWatch.config.setValue('playbackRate', this._rate);
}
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();
}
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 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 (isNaN(rate)) { return; }
if (Math.abs(rate - this._rate) < 0.05) { return; }
this._rate = rate;
// ユーザーが自分でスロー再生してるっぽい時は何もしない
if (video.playbackRate < red) {
return;
}
window.ZenzaWatch.config.setValue('playbackRate', this._rate);
}
_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.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"]');
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);
}
_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;
}
.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="toggleHeatSync">
<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);
};
const initDom = (ZenzaWatch) => {
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, body').appendChild(li);
ZenzaWatch.emitter.on('videoControBar.addonMenuReady', (container, handler) => {
let toggleButton = new ToggleButton({parentNode: container});
product.toggleButton = toggleButton;
toggleButton.on('command', handler);
toggleButton.refresh();
});
};
const init = () => {
let syncer;
console.log('init HeatSync...');
ZenzaDetector.detect().then(() => {
const ZenzaWatch = window.ZenzaWatch;
ZenzaWatch.emitter.on('DialogPlayerOpen', () => {
util.emitter.emit('zenzaOpen');
});
ZenzaWatch.emitter.on('DialogPlayerClose', () => {
util.emitter.emit('zenzaClose');
});
ZenzaWatch.emitter.on('heatMapUpdate', (p) => {
util.emitter.emit('heatMapUpdate', p);
});
ZenzaWatch.emitter.on('command-toggleHeatSync', () => {
const isEnable = !config.getValue('turbo.enabled');
ZenzaWatch.external.execCommand('notify', `HeatSync: ${isEnable ? 'ON' : 'OFF'}`);
if (!isEnable) {
syncer.disable();
}
config.setValue('turbo.enabled', isEnable);
});
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);
})();
})();