HeatSync

コメントの少ないところだけ自動で早送りする、忙しい人のためのZenzaWatch拡張

目前為 2018-05-17 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name HeatSync
  3. // @namespace https://github.com/segabito/
  4. // @description コメントの少ないところだけ自動で早送りする、忙しい人のためのZenzaWatch拡張
  5. // @match http://www.nicovideo.jp/*
  6. // @match http://ext.nicovideo.jp/
  7. // @match http://ext.nicovideo.jp/#*
  8. // @match http://ch.nicovideo.jp/*
  9. // @match http://com.nicovideo.jp/*
  10. // @match http://commons.nicovideo.jp/*
  11. // @match http://dic.nicovideo.jp/*
  12. // @exclude http://ads*.nicovideo.jp/*
  13. // @exclude http://www.upload.nicovideo.jp/*
  14. // @exclude http://www.nicovideo.jp/watch/*?edit=*
  15. // @exclude http://ch.nicovideo.jp/tool/*
  16. // @exclude http://flapi.nicovideo.jp/*
  17. // @exclude http://dic.nicovideo.jp/p/*
  18. // @version 0.0.10
  19. // @grant none
  20. // @author segabito macmoto
  21. // @license public domain
  22. // @noframes
  23. // ==/UserScript==
  24.  
  25.  
  26. (function() {
  27. const PRODUCT = 'HeatSync';
  28. const monkey = function(PRODUCT) {
  29. const console = window.console;
  30. let ZenzaWatch = null;
  31. //const $ = window.jQuery;
  32. console.log(`exec ${PRODUCT}..`);
  33.  
  34. const CONSTANT = {
  35. BASE_Z_INDEX: 150000
  36. };
  37. const product = {debug: {_const: CONSTANT}};
  38. window[PRODUCT] = product;
  39.  
  40.  
  41. const {util, Emitter} = (function() {
  42. const util = {};
  43. class Emitter {
  44. constructor() {
  45. }
  46.  
  47. on(name, callback) {
  48. if (!this._events) { this._events = {}; }
  49. name = name.toLowerCase();
  50. if (!this._events[name]) {
  51. this._events[name] = [];
  52. }
  53. this._events[name].push(callback);
  54. }
  55.  
  56. clear(name) {
  57. if (!this._events) { this._events = {}; }
  58. if (name) {
  59. this._events[name] = [];
  60. } else {
  61. this._events = {};
  62. }
  63. }
  64.  
  65. emit(name) {
  66. if (!this._events) { this._events = {}; }
  67. name = name.toLowerCase();
  68. if (!this._events.hasOwnProperty(name)) { return; }
  69. const e = this._events[name];
  70. const arg = Array.prototype.slice.call(arguments, 1);
  71. for (let i =0, len = e.length; i < len; i++) {
  72. e[i].apply(null, arg);
  73. }
  74. }
  75.  
  76. emitAsync(...args) {
  77. window.setTimeout(() => {
  78. this.emit(...args);
  79. }, 0);
  80. }
  81. }
  82.  
  83. util.emitter = new Emitter();
  84.  
  85. util.addStyle = function(styles, id) {
  86. var elm = document.createElement('style');
  87. elm.type = 'text/css';
  88. if (id) { elm.id = id; }
  89.  
  90. var text = styles.toString();
  91. text = document.createTextNode(text);
  92. elm.appendChild(text);
  93. var head = document.getElementsByTagName('head');
  94. head = head[0];
  95. head.appendChild(elm);
  96. return elm;
  97. };
  98.  
  99. util.mixin = function(self, o) {
  100. _.each(Object.keys(o), f => {
  101. if (!_.isFunction(o[f])) { return; }
  102. if (_.isFunction(self[f])) { return; }
  103. self[f] = o[f].bind(o);
  104. });
  105. };
  106.  
  107. util.attachShadowDom = function({host, tpl, mode = 'open'}) {
  108. const root = host.attachShadow ?
  109. host.attachShadow({mode}) : host.createShadowRoot();
  110. const node = document.importNode(tpl.content, true);
  111. root.appendChild(node);
  112. return root;
  113. };
  114.  
  115. util.getWatchId = function(url) {
  116. /\/?watch\/([a-z0-9]+)/.test(url || location.pathname);
  117. return RegExp.$1;
  118. };
  119.  
  120. util.isLogin = function() {
  121. return document.getElementsByClassName('siteHeaderLogin').length < 1;
  122. };
  123.  
  124. util.escapeHtml = function(text) {
  125. var map = {
  126. '&': '&amp;',
  127. '\x27': '&#39;',
  128. '"': '&quot;',
  129. '<': '&lt;',
  130. '>': '&gt;'
  131. };
  132. return text.replace(/[&"'<>]/g, char => {
  133. return map[char];
  134. });
  135. };
  136.  
  137. util.unescapeHtml = function(text) {
  138. var map = {
  139. '&amp;' : '&' ,
  140. '&#39;' : '\x27',
  141. '&quot;' : '"',
  142. '&lt;' : '<',
  143. '&gt;' : '>'
  144. };
  145. return text.replace(/(&amp;|&#39;|&quot;|&lt;|&gt;)/g, char => {
  146. return map[char];
  147. });
  148. };
  149.  
  150. util.escapeRegs = function(text) {
  151. const map = {
  152. '\\': '\\\\',
  153. '*': '\\*',
  154. '+': '\\+',
  155. '.': '\\.',
  156. '?': '\\?',
  157. '{': '\\{',
  158. '}': '\\}',
  159. '(': '\\(',
  160. ')': '\\)',
  161. '[': '\\[',
  162. ']': '\\]',
  163. '^': '\\^',
  164. '$': '\\$',
  165. '-': '\\-',
  166. '|': '\\|',
  167. '/': '\\/',
  168. };
  169. return text.replace(/[\\\*\+\.\?\{\}\(\)\[\]\^\$\-\|\/]/g, char => {
  170. return map[char];
  171. });
  172. };
  173.  
  174. util.hasLargeThumbnail = function(videoId) { // return true;
  175. // 大サムネが存在する最初の動画ID。 ソースはちゆ12歳
  176. // ※この数字以降でもごく稀に例外はある。
  177. var threthold = 16371888;
  178. var cid = videoId.substr(0, 2);
  179. if (cid !== 'sm') { return false; }
  180.  
  181. var fid = videoId.substr(2) * 1;
  182. if (fid < threthold) { return false; }
  183.  
  184. return true;
  185. };
  186.  
  187. const videoIdReg = /^[a-z]{2}\d+$/;
  188. util.getThumbnailUrlByVideoId = function(videoId) {
  189. if (!videoIdReg.test(videoId)) {
  190. return null;
  191. }
  192. const fileId = parseInt(videoId.substr(2), 10);
  193. const num = (fileId % 4) + 1;
  194. const large = util.hasLargeThumbnail(videoId) ? '.L' : '';
  195. return '//tn-skr' + num + '.smilevideo.jp/smile?i=' + fileId + large;
  196. };
  197.  
  198. util.isFirefox = function() {
  199. return navigator.userAgent.toLowerCase().indexOf('firefox') >= 0;
  200. };
  201.  
  202. return {util, Emitter};
  203. })(PRODUCT);
  204. product.util = util;
  205.  
  206.  
  207. const ZenzaDetector = (function() {
  208. let isReady = false;
  209. const emitter = new Emitter();
  210.  
  211. const onZenzaReady = () => {
  212. isReady = true;
  213. ZenzaWatch = window.ZenzaWatch;
  214.  
  215. emitter.emit('ready', window.ZenzaWatch);
  216. };
  217.  
  218. if (window.ZenzaWatch && window.ZenzaWatch.ready) {
  219. window.console.log('ZenzaWatch is Ready');
  220. isReady = true;
  221. } else {
  222. document.body.addEventListener('ZenzaWatchInitialize', () => {
  223. window.console.log('ZenzaWatchInitialize');
  224. onZenzaReady();
  225. });
  226. }
  227.  
  228. const detect = function() {
  229. return new Promise(res => {
  230. if (isReady) {
  231. return res(window.ZenzaWatch);
  232. }
  233. //window.setTimeout(() => {
  234. // window.console.log('ZenzaWatch is Ready?', window.ZenzaWatch);
  235. // if (window.ZenzaWatch && window.ZenzaWatch.ready && !isReady) {
  236. // window.console.log('ZenzaWatch is Ready!');
  237. // onZenzaReady();
  238. // }
  239. //}, 3000);
  240. emitter.on('ready', () => {
  241. res(window.ZenzaWatch);
  242. });
  243. });
  244. };
  245.  
  246. return {detect};
  247. })();
  248.  
  249. const broadcast = (() => {
  250. if (!window.BroadcastChannel) { return {send: () => {}}; }
  251. const bc = new window.BroadcastChannel(PRODUCT);
  252.  
  253. const onMessage = (e) => {
  254. const packet = e.data;
  255. //console.log('%creceive message', 'background: cyan;', packet);
  256. util.emitter.emit('broadcast', packet);
  257. };
  258.  
  259. const send = (packet) => {
  260. //console.log('%csend message', 'background: cyan;', packet);
  261. bc.postMessage(packet);
  262. };
  263.  
  264. bc.addEventListener('message', onMessage);
  265.  
  266. return {
  267. send
  268. };
  269. })();
  270.  
  271.  
  272.  
  273. const config = (function() {
  274. const prefix = PRODUCT + '_config_';
  275. const emitter = new Emitter();
  276.  
  277. const defaultConfig = {
  278. debug: false,
  279.  
  280. 'turbo.enabled': true,
  281. 'turbo.red': 1,
  282. 'turbo.smile-blue': 1.7,
  283. 'turbo.dmc-blue': 1.7,
  284. 'turbo.minDuration': 30,
  285.  
  286. 'turbo.ignoreTags': 'VOCALOID 音楽 作業用BGM 演奏してみた 歌ってみた'
  287.  
  288. };
  289.  
  290. const config = {};
  291. let noEmit = false;
  292.  
  293.  
  294. emitter.refresh = (emitChange = false) => {
  295. Object.keys(defaultConfig).forEach(key => {
  296. var storageKey = prefix + key;
  297. if (localStorage.hasOwnProperty(storageKey)) {
  298. try {
  299. let lastValue = config[key];
  300. let newValue = JSON.parse(localStorage.getItem(storageKey));
  301. if (lastValue !== newValue) {
  302. config[key] = newValue;
  303. if (emitChange) {
  304. emitter.emit('key', newValue);
  305. emitter.emit('@update', {key, value: newValue});
  306. }
  307. }
  308. } catch (e) {
  309. window.console.error('config parse error key:"%s" value:"%s" ', key, localStorage.getItem(storageKey), e);
  310. config[key] = defaultConfig[key];
  311. }
  312. } else {
  313. config[key] = defaultConfig[key];
  314. }
  315. });
  316. };
  317. emitter.refresh();
  318.  
  319. emitter.getValue = function(key, refresh) {
  320. if (refresh) {
  321. emitter.refreshValue(key);
  322. }
  323. return config[key];
  324. };
  325.  
  326. emitter.setValue = function(key, value) {
  327. if (config[key] !== value && arguments.length >= 2) {
  328. var storageKey = prefix + key;
  329. localStorage.setItem(storageKey, JSON.stringify(value));
  330. config[key] = value;
  331. emitter.emit(key, value);
  332. emitter.emit('@update', {key, value});
  333. broadcast.send('configUpdate');
  334. //console.log('%cconfig update "%s" = "%s"', 'background: cyan', key, value);
  335. }
  336. };
  337.  
  338. emitter.clearConfig = function() {
  339. noEmit = true;
  340. Object.keys(defaultConfig).forEach(key => {
  341. if (_.contains(['message', 'debug'], key)) { return; }
  342. var storageKey = prefix + key;
  343. try {
  344. if (localStorage.hasOwnProperty(storageKey)) {
  345. localStorage.removeItem(storageKey);
  346. }
  347. config[key] = defaultConfig[key];
  348. } catch (e) {}
  349. });
  350. noEmit = false;
  351. };
  352.  
  353. emitter.getKeys = function() {
  354. return Object.keys(defaultConfig);
  355. };
  356.  
  357. emitter.namespace = function(name) {
  358. return {
  359. getValue: (key) => { return emitter.getValue(name + '.' + key); },
  360. setValue: (key, value) => { emitter.setValue(name + '.' + key, value); },
  361. refresh: () => { emitter.refresh(); },
  362. on: (key, func) => {
  363. if (key === '@update') {
  364. emitter.on('@update', ({key, value}) => {
  365. const pre = name + '.';
  366. //console.log('@update', key, value, pre);
  367. if (key.startsWith(pre)) {
  368. func({key: key.replace(pre, ''), value});
  369. }
  370. });
  371. } else {
  372. emitter.on(name + '.' + key, func);
  373. }
  374. }
  375. };
  376. };
  377.  
  378. util.emitter.on('broadcast', (type) => {
  379. //if (type !== 'configUpdate') { return; }
  380. emitter.refresh(false);
  381. emitter.emit('refresh');
  382. });
  383.  
  384. return emitter;
  385. })();
  386. product.config = config;
  387.  
  388. class Syncer extends Emitter {
  389. constructor() {
  390. super();
  391. this._timer = null;
  392. this._videoElement = null;
  393. this._rate = 1.0;
  394.  
  395. this._config = config.namespace('turbo');
  396.  
  397. util.emitter.on('heatMapUpdate', this._onHeatMapUpdate.bind(this));
  398. util.emitter.on('zenzaClose', this._onZenzaClose.bind(this));
  399. util.emitter.on('zenzaOpen', this._onZenzaOpen.bind(this));
  400. util.emitter.on('broadcast', this._onBroadcast.bind(this));
  401. }
  402.  
  403. enable() {
  404. if (this._timer) { return; }
  405. console.info('start timer', this._timer, this._rate); //, this._map);
  406. this._timer = setInterval(this._onTimer.bind(this), 500);
  407. }
  408.  
  409. disable() {
  410. clearInterval(this._timer);
  411. this._rate = config.getValue('turbo.red');
  412. if (config.getValue('turbo.enabled')) {
  413. window.ZenzaWatch.config.setValue('playbackRate', this._rate);
  414. }
  415. this._timer = null;
  416. }
  417.  
  418. _onZenzaOpen() {
  419. if (this._dialog || !window.ZenzaWatch.debug.dialog) { return; }
  420. this._dialog = window.ZenzaWatch.debug.dialog;
  421. this._dialog.on('loadVideoInfo', this._onVideoInfoLoad.bind(this));
  422. }
  423.  
  424. _onZenzaClose() {
  425. this.disable();
  426. }
  427.  
  428. _onVideoInfoLoad(videoInfo) {
  429. const tags = (videoInfo.tagList || [])
  430. .map(t => { return t.tag.toUpperCase(); });
  431. this._tags = tags;
  432. }
  433.  
  434. _onHeatMapUpdate({map, duration}) {
  435. this._map = map;
  436. this._duration = duration;
  437. this._rate = config.getValue('turbo.red');
  438. if (duration < config.getValue('turbo.minDuration')) {
  439. window.console.log('disable HeatSync by duration', duration);
  440. return this.disable();
  441. }
  442. const currentTags = this._tags || [];
  443. const ignoreTags = config.getValue('turbo.ignoreTags').split(/[  ]/);
  444. if (currentTags.some(t => { return ignoreTags.includes(t.toUpperCase()); })) {
  445. window.console.log('disable HeatSync by tag'); //, currentTags, ignoreTags);
  446. return this.disable();
  447. }
  448. this.enable();
  449. }
  450.  
  451. _onTimer() {
  452. //if (!this._videoElement) {
  453. this._videoElement = window.ZenzaWatch.external.getVideoElement();
  454. if (!this._videoElement) { return; }
  455. //}
  456. const video = this._videoElement;
  457. const isEconomy = /smile\?m=[\d\.]+low$/.test(video.src);
  458. this._lastEnabled = config.getValue('turbo.enabled');
  459. if (video.paused || !this._lastEnabled || isEconomy) { return; }
  460. const duration = video.duration;
  461. const current = video.currentTime;
  462. const per = current / duration;
  463. const isDmc = /dmc\.nico/.test(video.src);
  464. const map = this._map;
  465. const pos = Math.floor(map.length * per);
  466.  
  467. const blue = parseFloat(isDmc ?
  468. config.getValue('turbo.dmc-blue') : config.getValue('turbo.smile-blue'));
  469. const red = parseFloat(config.getValue('turbo.red'));
  470.  
  471. const pt = map[pos];
  472.  
  473. let ratePer = (256 - pt) / 256;
  474. if (ratePer > 0.95) { ratePer = 1; }
  475. if (ratePer < 0.4) { ratePer = 0; }
  476. let rate = red + (blue - red) * ratePer;
  477.  
  478. rate = Math.round(rate * 100) / 100;
  479. //console.info('onTimer', pt, pt / 255, Math.round(ratePer * 100) / 100, rate);
  480. if (isNaN(rate)) { return; }
  481. if (Math.abs(rate - this._rate) < 0.05) { return; }
  482. this._rate = rate;
  483. // ユーザーが自分でスロー再生してるっぽい時は何もしない
  484. if (video.playbackRate < red) {
  485. return;
  486. }
  487. window.ZenzaWatch.config.setValue('playbackRate', this._rate);
  488. }
  489.  
  490. _onBroadcast() {
  491. const lastEnabled = this._lastEnabled;
  492. window.setTimeout(() => {
  493. const currentEnabled = config.getValue('turbo.enabled');
  494. if (lastEnabled && !currentEnabled) {
  495. this.disable();
  496. }
  497. }, 1000);
  498. }
  499. }
  500.  
  501. class BaseViewComponent extends Emitter {
  502. constructor({parentNode = null, name = '', template = '', shadow = '', css = ''}) {
  503. super();
  504.  
  505. this._params = {parentNode, name, template, shadow, css};
  506. this._bound = {};
  507. this._state = {};
  508. this._props = {};
  509. this._elm = {};
  510.  
  511. this._initDom({
  512. parentNode,
  513. name,
  514. template,
  515. shadow,
  516. css
  517. });
  518. }
  519.  
  520. _initDom({parentNode, name, template, css = '', shadow = ''}) {
  521. let tplId = `${PRODUCT}${name}Template`;
  522. let tpl = document.getElementById(tplId);
  523. if (!tpl) {
  524. if (css) { util.addStyle(css, `${name}Style`); }
  525. tpl = document.createElement('template');
  526. tpl.innerHTML = template;
  527. tpl.id = tplId;
  528. document.body.appendChild(tpl);
  529. }
  530. const onClick = this._bound.onClick = this._onClick.bind(this);
  531.  
  532. const view = document.importNode(tpl.content, true);
  533. this._view = view.querySelector('*') || document.createDocumentFragment();
  534. if (this._view) {
  535. this._view.addEventListener('click', onClick);
  536. }
  537. this.appendTo(parentNode);
  538.  
  539. if (shadow) {
  540. this._attachShadow({host: this._view, name, shadow});
  541. if (!this._isDummyShadow) {
  542. this._shadow.addEventListener('click', onClick);
  543. }
  544. }
  545. }
  546.  
  547. _attachShadow ({host, shadow, name, mode = 'open'}) {
  548. let tplId = `${PRODUCT}${name}Shadow`;
  549. let tpl = document.getElementById(tplId);
  550. if (!tpl) {
  551. tpl = document.createElement('template');
  552. tpl.innerHTML = shadow;
  553. tpl.id = tplId;
  554. document.body.appendChild(tpl);
  555. }
  556.  
  557. if (!host.attachShadow && !host.createShadowRoot) {
  558. return this._fallbackNoneShadowDom({host, tpl, name});
  559. }
  560.  
  561. const root = host.attachShadow ?
  562. host.attachShadow({mode}) : host.createShadowRoot();
  563. const node = document.importNode(tpl.content, true);
  564. root.appendChild(node);
  565. this._shadowRoot = root;
  566. this._shadow = root.querySelector('.root');
  567. this._isDummyShadow = false;
  568. }
  569.  
  570. _fallbackNoneShadowDom({host, tpl, name}) {
  571. const node = document.importNode(tpl.content, true);
  572. const style = node.querySelector('style');
  573. style.remove();
  574. util.addStyle(style.innerHTML, `${name}Shadow`);
  575. host.appendChild(node);
  576. this._shadow = this._shadowRoot = host.querySelector('.root');
  577. this._isDummyShadow = true;
  578. }
  579.  
  580. setState(key, val) {
  581. if (typeof key === 'string') {
  582. this._setState(key, val);
  583. }
  584. Object.keys(key).forEach(k => {
  585. this._setState(k, key[k]);
  586. });
  587. }
  588.  
  589. _setState(key, val) {
  590. if (this._state[key] !== val) {
  591. this._state[key] = val;
  592. if (/^is(.*)$/.test(key)) {
  593. this.toggleClass(`is-${RegExp.$1}`, !!val);
  594. }
  595. this.emit('update', {key, val});
  596. }
  597. }
  598.  
  599. _onClick(e) {
  600. const target = e.target.classList.contains('command') ?
  601. e.target : e.target.closest('.command');
  602.  
  603. if (!target) { return; }
  604.  
  605. const command = target.getAttribute('data-command');
  606. if (!command) { return; }
  607. const type = target.getAttribute('data-type') || 'string';
  608. let param = target.getAttribute('data-param');
  609. e.stopPropagation();
  610. e.preventDefault();
  611. param = this._parseParam(param, type);
  612. this._onCommand(command, param);
  613. }
  614.  
  615. _parseParam(param, type) {
  616. switch (type) {
  617. case 'json':
  618. case 'bool':
  619. case 'number':
  620. param = JSON.parse(param);
  621. break;
  622. }
  623. return param;
  624. }
  625.  
  626. appendTo(parentNode) {
  627. if (!parentNode) { return; }
  628. this._parentNode = parentNode;
  629. parentNode.appendChild(this._view);
  630. }
  631.  
  632. _onCommand(command, param) {
  633. this.emit('command', command, param);
  634. }
  635.  
  636. toggleClass(className, v) {
  637. (className || '').split(/ +/).forEach((c) => {
  638. if (this._view && this._view.classList) {
  639. this._view.classList.toggle(c, v);
  640. }
  641. if (this._shadow && this._shadow.classList) {
  642. this._shadow.classList.toggle(c, this._view.classList.contains(c));
  643. }
  644. });
  645. }
  646.  
  647. addClass(name) { this.toggleClass(name, true); }
  648. removeClass(name) { this.toggleClass(name, false); }
  649. }
  650.  
  651. class ConfigPanel extends BaseViewComponent {
  652. constructor({parentNode}) {
  653. super({
  654. parentNode,
  655. name: 'HeatSyncConfigPanel',
  656. shadow: ConfigPanel.__shadow__,
  657. template: '<div class="HeatSyncConfigPanelContainer"></div>',
  658. css: ''
  659. });
  660. this._state = {
  661. isOpen: false,
  662. isVisible: false
  663. };
  664. config.on('refresh', this._onBeforeShow.bind(this));
  665. }
  666.  
  667. _initDom(...args) {
  668. super._initDom(...args);
  669. const v = this._shadow;
  670.  
  671. this._elm.red = v.querySelector('*[data-config-name="turbo.red"]');
  672. this._elm.dmc = v.querySelector('*[data-config-name="turbo.dmc-blue"]');
  673. this._elm.smile = v.querySelector('*[data-config-name="turbo.smile-blue"]');
  674. this._elm.minDur = v.querySelector('*[data-config-name="turbo.minDuration"]');
  675. this._elm.enabled = v.querySelector('*[data-config-name="turbo.enabled"]');
  676. this._elm.ignores = v.querySelector('*[data-config-name="turbo.ignoreTags"]');
  677.  
  678. const onChange = (e) => {
  679. const target = e.target, name = target.getAttribute('data-config-name');
  680. switch (target.tagName) {
  681. case 'INPUT':
  682. case 'SELECT':
  683. if (target.type === 'checkbox') {
  684. config.setValue(name, target.checked);
  685. } else {
  686. const type = target.getAttribute('data-type');
  687. const value = this._parseParam(target.value, type);
  688. config.setValue(name, value);
  689. }
  690. break;
  691. default:
  692. //console.info('target', e, target, name, target.checked);
  693. config.setValue(name, !!target.checked);
  694. break;
  695. }
  696. };
  697. this._elm.red .addEventListener('change', onChange);
  698. this._elm.dmc .addEventListener('change', onChange);
  699. this._elm.smile .addEventListener('change', onChange);
  700. this._elm.minDur .addEventListener('change', onChange);
  701. this._elm.enabled.addEventListener('change', onChange);
  702. this._elm.ignores.addEventListener('change', onChange);
  703.  
  704. v.querySelector('.closeButton')
  705. .addEventListener('click', this.hide.bind(this));
  706. }
  707.  
  708. _onClick(e) {
  709. super._onClick(e);
  710. }
  711.  
  712. _onMouseDown(e) {
  713. this.hide();
  714. this._onClick(e);
  715. }
  716.  
  717. show() {
  718. document.body.addEventListener('click', this._bound.onBodyClick);
  719. this._onBeforeShow();
  720.  
  721. this.setState({isOpen: true});
  722. window.setTimeout(() => {
  723. this.setState({isVisible: true});
  724. }, 100);
  725. }
  726.  
  727. hide() {
  728. document.body.removeEventListener('click', this._bound.onBodyClick);
  729. this.setState({isVisible: false});
  730. window.setTimeout(() => {
  731. this.setState({isOpen: false});
  732. }, 2100);
  733. }
  734.  
  735. toggle() {
  736. if (this._state.isOpen) {
  737. this.hide();
  738. } else {
  739. this.show();
  740. }
  741. }
  742.  
  743. _onBeforeShow() {
  744. this._elm.red.value = '' + config.getValue('turbo.red');
  745. this._elm.dmc.value = '' + config.getValue('turbo.dmc-blue');
  746. this._elm.smile.value = '' + config.getValue('turbo.smile-blue');
  747. this._elm.minDur.value = '' + config.getValue('turbo.minDuration');
  748. this._elm.ignores.value = '' + config.getValue('turbo.ignoreTags');
  749.  
  750. this._elm.enabled.checked = !!config.getValue('turbo.enabled');
  751. }
  752. }
  753.  
  754. ConfigPanel.__shadow__ = (`
  755. <style>
  756. .HeatSyncConfigPanel {
  757. display: none;
  758. position: fixed;
  759. z-index: ${CONSTANT.BASE_Z_INDEX};
  760. top: 50vh;
  761. left: 50vw;
  762. padding: 8px;
  763. border: 2px outset;
  764. box-shadow: 0 0 8px #000;
  765. background: #ccc;
  766. transform: translate(-50%, -50%);
  767. /*transform: translate(-50%, -50%) perspective(200px) rotateX(90deg);*/
  768. transition: opacity 0.5s;
  769. transform-origin: center bottom;
  770. animation-timing-function: steps(10);
  771. perspective-origin: center bottom;
  772. user-select: none;
  773. -webkit-user-select: none;
  774. -moz-user-select: none;
  775. }
  776.  
  777. .HeatSyncConfigPanel.is-Open {
  778. display: block;
  779. opacity: 0;
  780. /*animation-name: dokahide;*/
  781. }
  782. .HeatSyncConfigPanel.is-Open.is-Visible {
  783. opacity: 1;
  784. /*animation-name: dokashow;*/
  785. /*transform: translate(-50%, -50%) perspective(200px) rotateX(0deg);*/
  786. }
  787.  
  788. @keyframes dokashow {
  789. 0% {
  790. opacity: 1;
  791. transform: translate(-50%, -50%) perspective(200px) rotateX(90deg);
  792. }
  793. 100% {
  794. opacity: 1;
  795. transform: translate(-50%, -50%) perspective(200px) rotateX(0deg);
  796. }
  797. }
  798.  
  799. @keyframes dokahide {
  800. 0% {
  801. opacity: 1;
  802. transform: translate(-50%, -50%) perspective(200px) rotateX(0deg);
  803. }
  804. 99% {
  805. opacity: 1;
  806. transform: translate(-50%, -50%) perspective(200px) rotateX(90deg);
  807. }
  808. 100% {
  809. opacity: 0;
  810. }
  811. }
  812.  
  813. .title {
  814. margin: 0;
  815. font-weight: bolder;
  816. font-size: 120%;
  817. }
  818.  
  819. .speedSelect {
  820. margin: 8px;
  821. }
  822.  
  823. .minDuration {
  824. margin: 8px;
  825. }
  826.  
  827. .ignoreTags {
  828. margin: 8px;
  829. }
  830. .ignoreTags input {
  831. margin: auto;
  832. width: 100%;
  833. font-size: 110%;
  834. }
  835.  
  836.  
  837. .enableSelect {
  838. margin: 8px;
  839. }
  840.  
  841. .closeButton {
  842. display: block;
  843. text-align: center;
  844. }
  845.  
  846. .closeButton {
  847. display: block;
  848. pading: 8px;
  849. cursor: pointer;
  850. margin: auto;
  851. }
  852.  
  853. label {
  854. cursor: pointer;
  855. }
  856.  
  857. input[type="number"] {
  858. width: 50px;
  859. }
  860. </style>
  861. <div class="root HeatSyncConfigPanel">
  862. <p class="title">†HeatSync†</p>
  863.  
  864. <div class="speedSelect dmc">
  865. <span>最高倍率(新仕様サーバー)</span>
  866. <select data-config-name="turbo.dmc-blue" data-type="number">
  867. <option value="3">3.0</option>
  868. <option>2.9</option>
  869. <option>2.8</option>
  870. <option>2.7</option>
  871. <option>2.6</option>
  872. <option>2.5</option>
  873. <option>2.4</option>
  874. <option>2.3</option>
  875. <option>2.2</option>
  876. <option>2.1</option>
  877. <option value="2">2.0</option>
  878. <option>1.9</option>
  879. <option>1.8</option>
  880. <option>1.7</option>
  881. <option>1.6</option>
  882. <option>1.5</option>
  883. <option>1.4</option>
  884. <option>1.3</option>
  885. <option>1.2</option>
  886. <option>1.1</option>
  887. <option value="1">1</option>
  888. </select>
  889. </div>
  890.  
  891. <div class="speedSelect smile">
  892. <span>最高倍率(旧仕様サーバー)</span>
  893. <select data-config-name="turbo.smile-blue" data-type="number">
  894. <option value="3">3.0</option>
  895. <option>2.9</option>
  896. <option>2.8</option>
  897. <option>2.7</option>
  898. <option>2.6</option>
  899. <option>2.5</option>
  900. <option>2.4</option>
  901. <option>2.3</option>
  902. <option>2.2</option>
  903. <option>2.1</option>
  904. <option value="2">2.0</option>
  905. <option>1.9</option>
  906. <option>1.8</option>
  907. <option>1.7</option>
  908. <option>1.6</option>
  909. <option>1.5</option>
  910. <option>1.4</option>
  911. <option>1.3</option>
  912. <option>1.2</option>
  913. <option>1.1</option>
  914. <option value="1">1</option>
  915. </select>
  916. </div>
  917.  
  918. <div class="speedSelect minimum">
  919. <span>最低倍率</span>
  920. <select data-config-name="turbo.red" data-type="number">
  921. <option value="3">3.0</option>
  922. <option>2.9</option>
  923. <option>2.8</option>
  924. <option>2.7</option>
  925. <option>2.6</option>
  926. <option>2.5</option>
  927. <option>2.4</option>
  928. <option>2.3</option>
  929. <option>2.2</option>
  930. <option>2.1</option>
  931. <option value="2">2.0</option>
  932. <option>1.9</option>
  933. <option>1.8</option>
  934. <option>1.7</option>
  935. <option>1.6</option>
  936. <option>1.5</option>
  937. <option>1.4</option>
  938. <option>1.3</option>
  939. <option>1.2</option>
  940. <option>1.1</option>
  941. <option value="1">1.0</option>
  942. </select>
  943. </div>
  944.  
  945. <div class="minDuration">
  946. <label>
  947. <input type="number" data-config-name="turbo.minDuration" data-type="number">
  948. 秒未満の動画には適用しない
  949. </label>
  950. </div>
  951.  
  952. <div class="ignoreTags">
  953. <label>
  954. このタグが含まれる動画では無効(スペース区切)
  955. <input type="text" data-config-name="turbo.ignoreTags">
  956. </label>
  957. </div>
  958.  
  959. <div class="enableSelect">
  960. <label>
  961. <input type="checkbox" data-config-name="turbo.enabled" data-type="bool">
  962. HeatSyncを有効にする
  963. </label>
  964. </div>
  965.  
  966. <div class="closeButtonContainer">
  967. <button class="closeButton" type="button">
  968. 閉じる
  969. </button>
  970. </div>
  971.  
  972. </div>
  973. `).trim();
  974.  
  975.  
  976. class ToggleButton extends BaseViewComponent {
  977. constructor({parentNode}) {
  978. super({
  979. parentNode,
  980. name: 'HeatSyncToggleButton',
  981. shadow: ToggleButton.__shadow__,
  982. template: '<div class="HeatSyncToggleButtonContainer"></div>',
  983. css: ''
  984. });
  985.  
  986. this._state = {
  987. isEnabled: undefined
  988. };
  989.  
  990. config.on('turbo.enabled', () => {
  991. this.refresh();
  992. });
  993. }
  994.  
  995. refresh() {
  996. this.setState({isEnabled: config.getValue('turbo.enabled')});
  997. }
  998. }
  999.  
  1000.  
  1001. ToggleButton.__shadow__ = `
  1002. <style>
  1003. .controlButton {
  1004. position: relative;
  1005. display: inline-block;
  1006. transition: opacity 0.4s ease, margin-left 0.2s ease, margin-top 0.2s ease;
  1007. box-sizing: border-box;
  1008. text-align: center;
  1009. cursor: pointer;
  1010. color: #fff;
  1011. opacity: 0.8;
  1012. vertical-align: middle;
  1013. }
  1014. .controlButton:hover {
  1015. text-shadow: 0 0 8px #ff9;
  1016. cursor: pointer;
  1017. opacity: 1;
  1018. }
  1019. .heatSyncSwitch {
  1020. font-size: 16px;
  1021. width: 32px;
  1022. height: 32px;
  1023. line-height: 30px;
  1024. cursor: pointer;
  1025. }
  1026. .is-Enabled .controlButtonInner {
  1027. color: #aef;
  1028. text-shadow: 0 0 4px #fea, 0 0 8px orange;
  1029. }
  1030.  
  1031. .controlButton .tooltip {
  1032. display: none;
  1033. pointer-events: none;
  1034. position: absolute;
  1035. left: 16px;
  1036. top: -30px;
  1037. transform: translate(-50%, 0);
  1038. font-size: 12px;
  1039. line-height: 16px;
  1040. padding: 2px 4px;
  1041. border: 1px solid !000;
  1042. background: #ffc;
  1043. color: #000;
  1044. text-shadow: none;
  1045. white-space: nowrap;
  1046. z-index: 100;
  1047. opacity: 0.8;
  1048. }
  1049.  
  1050. .controlButton:hover {
  1051. background: #222;
  1052. }
  1053.  
  1054. .controlButton:hover .tooltip {
  1055. display: block;
  1056. opacity: 1;
  1057. }
  1058.  
  1059. </style>
  1060. <div class="heatSyncSwitch controlButton root command" data-command="toggleHeatSync">
  1061. <div class="controlButtonInner" title="HeatSync">HS</div>
  1062. <div class="tooltip">HeatSync</div>
  1063. </div>
  1064. `.trim();
  1065.  
  1066.  
  1067.  
  1068. const initExternal = (syncer) => {
  1069. product.external = {
  1070. syncer
  1071. };
  1072.  
  1073. product.isReady = true;
  1074. const ev = new CustomEvent(`${PRODUCT}Initialized`, { detail: { product } });
  1075. document.body.dispatchEvent(ev);
  1076. };
  1077.  
  1078. const initDom = (ZenzaWatch) => {
  1079. let configPanel;
  1080. const li = document.createElement('li');
  1081. li.innerHTML = `<a href="javascript:;">†HeatSync†設定</a>`;
  1082. li.addEventListener('click', () => {
  1083. if (!configPanel) {
  1084. configPanel = new ConfigPanel({parentNode: document.body});
  1085. }
  1086. configPanel.toggle();
  1087. });
  1088. document.querySelector('#siteHeaderRightMenuContainer, body').appendChild(li);
  1089.  
  1090. ZenzaWatch.emitter.on('videoControBar.addonMenuReady', (container, handler) => {
  1091. let toggleButton = new ToggleButton({parentNode: container});
  1092. product.toggleButton = toggleButton;
  1093. toggleButton.on('command', handler);
  1094. toggleButton.refresh();
  1095. });
  1096. };
  1097.  
  1098. const init = () => {
  1099. let syncer;
  1100. console.log('init HeatSync...');
  1101. ZenzaDetector.detect().then(() => {
  1102. const ZenzaWatch = window.ZenzaWatch;
  1103. ZenzaWatch.emitter.on('DialogPlayerOpen', () => {
  1104. util.emitter.emit('zenzaOpen');
  1105. });
  1106.  
  1107. ZenzaWatch.emitter.on('DialogPlayerClose', () => {
  1108. util.emitter.emit('zenzaClose');
  1109. });
  1110.  
  1111. ZenzaWatch.emitter.on('heatMapUpdate', (p) => {
  1112. util.emitter.emit('heatMapUpdate', p);
  1113. });
  1114.  
  1115. ZenzaWatch.emitter.on('command-toggleHeatSync', () => {
  1116. const isEnable = !config.getValue('turbo.enabled');
  1117. ZenzaWatch.external.execCommand('notify', `HeatSync: ${isEnable ? 'ON' : 'OFF'}`);
  1118. if (!isEnable) {
  1119. syncer.disable();
  1120. }
  1121. config.setValue('turbo.enabled', isEnable);
  1122. });
  1123.  
  1124. initDom(ZenzaWatch);
  1125.  
  1126.  
  1127. //console.info('detect zenzawatch...');
  1128.  
  1129. syncer = new Syncer();
  1130.  
  1131. initExternal(syncer);
  1132. });
  1133. };
  1134.  
  1135. init();
  1136. };
  1137.  
  1138. (() => {
  1139. const script = document.createElement('script');
  1140. script.id = `${PRODUCT}Loader`;
  1141. script.setAttribute('type', 'text/javascript');
  1142. script.setAttribute('charset', 'UTF-8');
  1143. script.appendChild(document.createTextNode( '(' + monkey + ')("' + PRODUCT + '");' ));
  1144. document.body.appendChild(script);
  1145. })();
  1146. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址