HeatSync

コメント密度の薄いところを早送りする、忙しい人のためのZenzaWatch拡張

目前為 2017-04-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.3
  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. const {util, Emitter} = (function() {
  41. const util = {};
  42. class Emitter {
  43. constructor() {
  44. }
  45.  
  46. on(name, callback) {
  47. if (!this._events) { this._events = {}; }
  48. name = name.toLowerCase();
  49. if (!this._events[name]) {
  50. this._events[name] = [];
  51. }
  52. this._events[name].push(callback);
  53. }
  54.  
  55. clear(name) {
  56. if (!this._events) { this._events = {}; }
  57. if (name) {
  58. this._events[name] = [];
  59. } else {
  60. this._events = {};
  61. }
  62. }
  63.  
  64. emit(name) {
  65. if (!this._events) { this._events = {}; }
  66. name = name.toLowerCase();
  67. if (!this._events.hasOwnProperty(name)) { return; }
  68. const e = this._events[name];
  69. const arg = Array.prototype.slice.call(arguments, 1);
  70. for (let i =0, len = e.length; i < len; i++) {
  71. e[i].apply(null, arg);
  72. }
  73. }
  74.  
  75. emitAsync(...args) {
  76. window.setTimeout(() => {
  77. this.emit(...args);
  78. }, 0);
  79. }
  80. }
  81.  
  82. util.emitter = new Emitter();
  83.  
  84. util.addStyle = function(styles, id) {
  85. var elm = document.createElement('style');
  86. elm.type = 'text/css';
  87. if (id) { elm.id = id; }
  88.  
  89. var text = styles.toString();
  90. text = document.createTextNode(text);
  91. elm.appendChild(text);
  92. var head = document.getElementsByTagName('head');
  93. head = head[0];
  94. head.appendChild(elm);
  95. return elm;
  96. };
  97.  
  98. util.mixin = function(self, o) {
  99. _.each(Object.keys(o), f => {
  100. if (!_.isFunction(o[f])) { return; }
  101. if (_.isFunction(self[f])) { return; }
  102. self[f] = o[f].bind(o);
  103. });
  104. };
  105.  
  106. util.attachShadowDom = function({host, tpl, mode = 'open'}) {
  107. const root = host.attachShadow ?
  108. host.attachShadow({mode}) : host.createShadowRoot();
  109. const node = document.importNode(tpl.content, true);
  110. root.appendChild(node);
  111. return root;
  112. };
  113.  
  114. util.getWatchId = function(url) {
  115. /\/?watch\/([a-z0-9]+)/.test(url || location.pathname);
  116. return RegExp.$1;
  117. };
  118.  
  119. util.isLogin = function() {
  120. return document.getElementsByClassName('siteHeaderLogin').length < 1;
  121. };
  122.  
  123. util.escapeHtml = function(text) {
  124. var map = {
  125. '&': '&amp;',
  126. '\x27': '&#39;',
  127. '"': '&quot;',
  128. '<': '&lt;',
  129. '>': '&gt;'
  130. };
  131. return text.replace(/[&"'<>]/g, char => {
  132. return map[char];
  133. });
  134. };
  135.  
  136. util.unescapeHtml = function(text) {
  137. var map = {
  138. '&amp;' : '&' ,
  139. '&#39;' : '\x27',
  140. '&quot;' : '"',
  141. '&lt;' : '<',
  142. '&gt;' : '>'
  143. };
  144. return text.replace(/(&amp;|&#39;|&quot;|&lt;|&gt;)/g, char => {
  145. return map[char];
  146. });
  147. };
  148.  
  149. util.escapeRegs = function(text) {
  150. const map = {
  151. '\\': '\\\\',
  152. '*': '\\*',
  153. '+': '\\+',
  154. '.': '\\.',
  155. '?': '\\?',
  156. '{': '\\{',
  157. '}': '\\}',
  158. '(': '\\(',
  159. ')': '\\)',
  160. '[': '\\[',
  161. ']': '\\]',
  162. '^': '\\^',
  163. '$': '\\$',
  164. '-': '\\-',
  165. '|': '\\|',
  166. '/': '\\/',
  167. };
  168. return text.replace(/[\\\*\+\.\?\{\}\(\)\[\]\^\$\-\|\/]/g, char => {
  169. return map[char];
  170. });
  171. };
  172.  
  173. util.hasLargeThumbnail = function(videoId) { // return true;
  174. // 大サムネが存在する最初の動画ID。 ソースはちゆ12歳
  175. // ※この数字以降でもごく稀に例外はある。
  176. var threthold = 16371888;
  177. var cid = videoId.substr(0, 2);
  178. if (cid !== 'sm') { return false; }
  179.  
  180. var fid = videoId.substr(2) * 1;
  181. if (fid < threthold) { return false; }
  182.  
  183. return true;
  184. };
  185.  
  186. const videoIdReg = /^[a-z]{2}\d+$/;
  187. util.getThumbnailUrlByVideoId = function(videoId) {
  188. if (!videoIdReg.test(videoId)) {
  189. return null;
  190. }
  191. const fileId = parseInt(videoId.substr(2), 10);
  192. const num = (fileId % 4) + 1;
  193. const large = util.hasLargeThumbnail(videoId) ? '.L' : '';
  194. return '//tn-skr' + num + '.smilevideo.jp/smile?i=' + fileId + large;
  195. };
  196.  
  197. util.isFirefox = function() {
  198. return navigator.userAgent.toLowerCase().indexOf('firefox') >= 0;
  199. };
  200.  
  201. return {util, Emitter};
  202. })(PRODUCT);
  203. product.util = util;
  204.  
  205.  
  206. const ZenzaDetector = (function() {
  207. let isReady = false;
  208. const emitter = new Emitter();
  209.  
  210. const onZenzaReady = () => {
  211. isReady = true;
  212. ZenzaWatch = window.ZenzaWatch;
  213.  
  214. emitter.emit('ready', window.ZenzaWatch);
  215. };
  216.  
  217. if (window.ZenzaWatch && window.ZenzaWatch.ready) {
  218. window.console.log('ZenzaWatch is Ready');
  219. isReady = true;
  220. } else {
  221. document.body.addEventListener('ZenzaWatchInitialize', () => {
  222. window.console.log('ZenzaWatchInitialize');
  223. onZenzaReady();
  224. });
  225. }
  226.  
  227. const detect = function() {
  228. return new Promise(res => {
  229. if (isReady) {
  230. return res(window.ZenzaWatch);
  231. }
  232. emitter.on('ready', () => {
  233. res(window.ZenzaWatch);
  234. });
  235. });
  236. };
  237.  
  238. return {detect};
  239. })();
  240.  
  241. const broadcast = (() => {
  242. const bc = new window.BroadcastChannel(PRODUCT);
  243.  
  244. const onMessage = (e) => {
  245. const packet = e.data;
  246. //console.log('%creceive message', 'background: cyan;', packet);
  247. util.emitter.emit('broadcast', packet);
  248. };
  249.  
  250. const send = (packet) => {
  251. //console.log('%csend message', 'background: cyan;', packet);
  252. bc.postMessage(packet);
  253. };
  254.  
  255. bc.addEventListener('message', onMessage);
  256.  
  257. return {
  258. send
  259. };
  260. })();
  261.  
  262.  
  263.  
  264. const config = (function() {
  265. const prefix = PRODUCT + '_config_';
  266. const emitter = new Emitter();
  267.  
  268. const defaultConfig = {
  269. debug: false,
  270.  
  271. 'turbo.enabled': true,
  272. 'turbo.red': 1,
  273. 'turbo.smile-blue': 2,
  274. 'turbo.dmc-blue': 1.7,
  275.  
  276. };
  277.  
  278. const config = {};
  279. let noEmit = false;
  280.  
  281.  
  282. emitter.refresh = (emitChange = false) => {
  283. Object.keys(defaultConfig).forEach(key => {
  284. var storageKey = prefix + key;
  285. if (localStorage.hasOwnProperty(storageKey)) {
  286. try {
  287. let lastValue = config[key];
  288. let newValue = JSON.parse(localStorage.getItem(storageKey));
  289. if (lastValue !== newValue) {
  290. config[key] = newValue;
  291. if (emitChange) {
  292. emitter.emit('key', newValue);
  293. emitter.emit('@update', {key, value: newValue});
  294. }
  295. }
  296. } catch (e) {
  297. window.console.error('config parse error key:"%s" value:"%s" ', key, localStorage.getItem(storageKey), e);
  298. config[key] = defaultConfig[key];
  299. }
  300. } else {
  301. config[key] = defaultConfig[key];
  302. }
  303. });
  304. };
  305. emitter.refresh();
  306.  
  307. emitter.getValue = function(key, refresh) {
  308. if (refresh) {
  309. emitter.refreshValue(key);
  310. }
  311. return config[key];
  312. };
  313.  
  314. emitter.setValue = function(key, value) {
  315. if (config[key] !== value && arguments.length >= 2) {
  316. var storageKey = prefix + key;
  317. localStorage.setItem(storageKey, value);
  318. config[key] = value;
  319. emitter.emit(key, value);
  320. emitter.emit('@update', {key, value});
  321. broadcast.send('configUpdate');
  322. //console.log('%cconfig update "%s" = "%s"', 'background: cyan', key, value);
  323. }
  324. };
  325.  
  326. emitter.clearConfig = function() {
  327. noEmit = true;
  328. Object.keys(defaultConfig).forEach(key => {
  329. if (_.contains(['message', 'debug'], key)) { return; }
  330. var storageKey = prefix + key;
  331. try {
  332. if (localStorage.hasOwnProperty(storageKey)) {
  333. localStorage.removeItem(storageKey);
  334. }
  335. config[key] = defaultConfig[key];
  336. } catch (e) {}
  337. });
  338. noEmit = false;
  339. };
  340.  
  341. emitter.getKeys = function() {
  342. return Object.keys(defaultConfig);
  343. };
  344.  
  345. emitter.namespace = function(name) {
  346. return {
  347. getValue: (key) => { return emitter.getValue(name + '.' + key); },
  348. setValue: (key, value) => { emitter.setValue(name + '.' + key, value); },
  349. refresh: () => { emitter.refresh(); },
  350. on: (key, func) => {
  351. if (key === '@update') {
  352. emitter.on('@update', ({key, value}) => {
  353. const pre = name + '.';
  354. //console.log('@update', key, value, pre);
  355. if (key.startsWith(pre)) {
  356. func({key: key.replace(pre, ''), value});
  357. }
  358. });
  359. } else {
  360. emitter.on(name + '.' + key, func);
  361. }
  362. }
  363. };
  364. };
  365.  
  366. util.emitter.on('broadcast', (type) => {
  367. console.log('%cbroadcast "%s"', 'background: cyan', type);
  368. //if (type !== 'configUpdate') { return; }
  369. emitter.refresh(false);
  370. emitter.emit('refresh');
  371. });
  372.  
  373. return emitter;
  374. })();
  375. product.config = config;
  376.  
  377. class Syncer extends Emitter {
  378. constructor() {
  379. super();
  380. this._timer = null;
  381. this._videoElement = null;
  382. this._rate = 1.0;
  383.  
  384. this._config = config.namespace('turbo');
  385.  
  386. util.emitter.on('heatMapUpdate', this._onHeatMapUpdate.bind(this));
  387. util.emitter.on('zenzaClose', this._onZenzaClose.bind(this));
  388. }
  389.  
  390. enable() {
  391. if (this._timer) { return; }
  392. console.info('start timer', this._timer, this._rate, this._map);
  393. this._timer = setInterval(this._onTimer.bind(this), 500);
  394. }
  395.  
  396. disable() {
  397. clearInterval(this._timer);
  398. this._rate = 1.0;
  399. this._timer = null;
  400. }
  401.  
  402. _onZenzaClose() {
  403. this.disable();
  404. }
  405.  
  406. _onHeatMapUpdate({map, duration}) {
  407. this._map = map;
  408. this._duration = duration;
  409. this._rate = 1.0;
  410. //console.info('_onHeatMapUpdate', map, duration);
  411. this.enable();
  412. }
  413.  
  414. _onTimer() {
  415. if (!this._videoElement) {
  416. this._videoElement = window.ZenzaWatch.external.getVideoElement();
  417. if (!this._videoElement) { return; }
  418. }
  419. const video = this._videoElement;
  420. if (video.paused || !config.getValue('turbo.enabled')) { return; }
  421. const duration = video.duration;
  422. const current = video.currentTime;
  423. const per = current / duration;
  424. const isDmc = /dmc\.nico/.test(video.src);
  425. const map = this._map;
  426. const pos = Math.floor(map.length * per);
  427.  
  428. const blue = parseFloat(isDmc ?
  429. config.getValue('turbo.dmc-blue') : config.getValue('turbo.smile-blue'));
  430. const red = parseFloat(config.getValue('turbo.red'));
  431.  
  432. const pt = map[pos];
  433.  
  434. let ratePer = (256 - pt) / 256;
  435. if (ratePer > 0.95) { ratePer = 1; }
  436. if (ratePer < 0.4) { ratePer = 0; }
  437. let rate = red + (blue - red) * ratePer;
  438.  
  439. rate = Math.round(rate * 100) / 100;
  440. //console.info('onTimer', pt, pt / 255, Math.round(ratePer * 100) / 100, rate);
  441.  
  442. if (Math.abs(rate - this._rate) < 0.05) { return; }
  443. this._rate = rate;
  444. // ユーザーが自分でスロー再生してるっぽい時は何もしない
  445. if (video.playbackRate < red) {
  446. return;
  447. }
  448. window.ZenzaWatch.config.setValue('playbackRate', this._rate);
  449. }
  450. }
  451.  
  452. class BaseViewComponent extends Emitter {
  453. constructor({parentNode = null, name = '', template = '', shadow = '', css = ''}) {
  454. super();
  455.  
  456. this._params = {parentNode, name, template, shadow, css};
  457. this._bound = {};
  458. this._state = {};
  459. this._props = {};
  460. this._elm = {};
  461.  
  462. this._initDom({
  463. parentNode,
  464. name,
  465. template,
  466. shadow,
  467. css
  468. });
  469. }
  470.  
  471. _initDom({parentNode, name, template, css = '', shadow = ''}) {
  472. let tplId = `${PRODUCT}${name}Template`;
  473. let tpl = document.getElementById(tplId);
  474. if (!tpl) {
  475. if (css) { util.addStyle(css, `${name}Style`); }
  476. tpl = document.createElement('template');
  477. tpl.innerHTML = template;
  478. tpl.id = tplId;
  479. document.body.appendChild(tpl);
  480. }
  481. const onClick = this._bound.onClick = this._onClick.bind(this);
  482.  
  483. const view = document.importNode(tpl.content, true);
  484. this._view = view.querySelector('*') || document.createDocumentFragment();
  485. if (this._view) {
  486. this._view.addEventListener('click', onClick);
  487. }
  488. this.appendTo(parentNode);
  489.  
  490. if (shadow) {
  491. this._attachShadow({host: this._view, name, shadow});
  492. if (!this._isDummyShadow) {
  493. this._shadow.addEventListener('click', onClick);
  494. }
  495. }
  496. }
  497.  
  498. _attachShadow ({host, shadow, name, mode = 'open'}) {
  499. let tplId = `${PRODUCT}${name}Shadow`;
  500. let tpl = document.getElementById(tplId);
  501. if (!tpl) {
  502. tpl = document.createElement('template');
  503. tpl.innerHTML = shadow;
  504. tpl.id = tplId;
  505. document.body.appendChild(tpl);
  506. }
  507.  
  508. if (!host.attachShadow && !host.createShadowRoot) {
  509. return this._fallbackNoneShadowDom({host, tpl, name});
  510. }
  511.  
  512. const root = host.attachShadow ?
  513. host.attachShadow({mode}) : host.createShadowRoot();
  514. const node = document.importNode(tpl.content, true);
  515. root.appendChild(node);
  516. this._shadowRoot = root;
  517. this._shadow = root.querySelector('.root');
  518. this._isDummyShadow = false;
  519. }
  520.  
  521. _fallbackNoneShadowDom({host, tpl, name}) {
  522. const node = document.importNode(tpl.content, true);
  523. const style = node.querySelector('style');
  524. style.remove();
  525. util.addStyle(style.innerHTML, `${name}Shadow`);
  526. host.appendChild(node);
  527. this._shadow = this._shadowRoot = host.querySelector('.root');
  528. this._isDummyShadow = true;
  529. }
  530.  
  531. setState(key, val) {
  532. if (typeof key === 'string') {
  533. this._setState(key, val);
  534. }
  535. Object.keys(key).forEach(k => {
  536. this._setState(k, key[k]);
  537. });
  538. }
  539.  
  540. _setState(key, val) {
  541. if (this._state[key] !== val) {
  542. this._state[key] = val;
  543. if (/^is(.*)$/.test(key)) {
  544. this.toggleClass(`is-${RegExp.$1}`, !!val);
  545. }
  546. this.emit('update', {key, val});
  547. }
  548. }
  549.  
  550. _onClick(e) {
  551. const target = e.target.classList.contains('command') ?
  552. e.target : e.target.closest('.command');
  553.  
  554. if (!target) { return; }
  555.  
  556. const command = target.getAttribute('data-command');
  557. if (!command) { return; }
  558. const type = target.getAttribute('data-type') || 'string';
  559. let param = target.getAttribute('data-param');
  560. e.stopPropagation();
  561. e.preventDefault();
  562. switch (type) {
  563. case 'json':
  564. case 'bool':
  565. case 'number':
  566. param = JSON.parse(param);
  567. break;
  568. }
  569.  
  570. this._onCommand(command, param);
  571. }
  572.  
  573. appendTo(parentNode) {
  574. if (!parentNode) { return; }
  575. this._parentNode = parentNode;
  576. parentNode.appendChild(this._view);
  577. }
  578.  
  579. _onCommand(command, param) {
  580. this.emit('command', command, param);
  581. }
  582.  
  583. toggleClass(className, v) {
  584. (className || '').split(/ +/).forEach((c) => {
  585. if (this._view && this._view.classList) {
  586. this._view.classList.toggle(c, v);
  587. }
  588. if (this._shadow && this._shadow.classList) {
  589. this._shadow.classList.toggle(c, this._view.classList.contains(c));
  590. }
  591. });
  592. }
  593.  
  594. addClass(name) { this.toggleClass(name, true); }
  595. removeClass(name) { this.toggleClass(name, false); }
  596. }
  597.  
  598. class ConfigPanel extends BaseViewComponent {
  599. constructor({parentNode}) {
  600. super({
  601. parentNode,
  602. name: 'HeatSyncConfigPanel',
  603. shadow: ConfigPanel.__shadow__,
  604. template: '<div class="HeatSyncConfigPanelContainer"></div>',
  605. css: ''
  606. });
  607. this._state = {
  608. isOpen: false,
  609. isVisible: false
  610. };
  611. config.on('refresh', this._onBeforeShow.bind(this));
  612. }
  613.  
  614. _initDom(...args) {
  615. super._initDom(...args);
  616. const v = this._shadow;
  617.  
  618. this._elm.red = v.querySelector('*[data-config-name="turbo.red"]');
  619. this._elm.dmc = v.querySelector('*[data-config-name="turbo.dmc-blue"]');
  620. this._elm.smile = v.querySelector('*[data-config-name="turbo.smile-blue"]');
  621. this._elm.enabled = v.querySelector('*[data-config-name="turbo.enabled"]');
  622.  
  623. const onChange = (e) => {
  624. const target = e.target, name = target.getAttribute('data-config-name');
  625. switch (target.tagName) {
  626. case 'SELECT':
  627. //console.info('target', e, target, name, target.value);
  628. config.setValue(name, target.value);
  629. break;
  630. default:
  631. //console.info('target', e, target, name, target.checked);
  632. config.setValue(name, !!target.checked);
  633. break;
  634. }
  635. };
  636. this._elm.red .addEventListener('change', onChange);
  637. this._elm.dmc .addEventListener('change', onChange);
  638. this._elm.smile .addEventListener('change', onChange);
  639. this._elm.enabled.addEventListener('change', onChange);
  640.  
  641. v.querySelector('.closeButton')
  642. .addEventListener('click', this.hide.bind(this));
  643. }
  644.  
  645. _onClick(e) {
  646. super._onClick(e);
  647. }
  648.  
  649. _onMouseDown(e) {
  650. this.hide();
  651. this._onClick(e);
  652. }
  653.  
  654. show() {
  655. document.body.addEventListener('click', this._bound.onBodyClick);
  656. this._onBeforeShow();
  657.  
  658. this.setState({isOpen: true});
  659. window.setTimeout(() => {
  660. this.setState({isVisible: true});
  661. }, 100);
  662. }
  663.  
  664. hide() {
  665. document.body.removeEventListener('click', this._bound.onBodyClick);
  666. this.setState({isVisible: false});
  667. window.setTimeout(() => {
  668. this.setState({isOpen: false});
  669. }, 2100);
  670. }
  671.  
  672. toggle() {
  673. if (this._state.isOpen) {
  674. this.hide();
  675. } else {
  676. this.show();
  677. }
  678. }
  679.  
  680. _onBeforeShow() {
  681. this._elm.red.value = '' + config.getValue('turbo.red');
  682. this._elm.dmc.value = '' + config.getValue('turbo.dmc-blue');
  683. this._elm.smile.value = '' + config.getValue('turbo.smile-blue');
  684.  
  685. this._elm.enabled.checked = !!config.getValue('turbo.enabled');
  686. }
  687. }
  688.  
  689. ConfigPanel.__shadow__ = (`
  690. <style>
  691. .HeatSyncConfigPanel {
  692. display: none;
  693. position: fixed;
  694. z-index: ${CONSTANT.BASE_Z_INDEX};
  695. top: 50vh;
  696. left: 50vw;
  697. padding: 8px;
  698. border: 2px outset;
  699. box-shadow: 0 0 8px #000;
  700. background: #ccc;
  701. transform: translate(-50%, -50%);
  702. /*transform: translate(-50%, -50%) perspective(200px) rotateX(90deg);*/
  703. transition: opacity 0.5s;
  704. transform-origin: center bottom;
  705. animation-timing-function: steps(10);
  706. perspective-origin: center bottom;
  707. user-select: none;
  708. -webkit-user-select: none;
  709. -moz-user-select: none;
  710. }
  711.  
  712. .HeatSyncConfigPanel.is-Open {
  713. display: block;
  714. opacity: 0;
  715. /*animation-name: dokahide;*/
  716. }
  717. .HeatSyncConfigPanel.is-Open.is-Visible {
  718. opacity: 1;
  719. /*animation-name: dokashow;*/
  720. /*transform: translate(-50%, -50%) perspective(200px) rotateX(0deg);*/
  721. }
  722.  
  723. @keyframes dokashow {
  724. 0% {
  725. opacity: 1;
  726. transform: translate(-50%, -50%) perspective(200px) rotateX(90deg);
  727. }
  728. 100% {
  729. opacity: 1;
  730. transform: translate(-50%, -50%) perspective(200px) rotateX(0deg);
  731. }
  732. }
  733.  
  734. @keyframes dokahide {
  735. 0% {
  736. opacity: 1;
  737. transform: translate(-50%, -50%) perspective(200px) rotateX(0deg);
  738. }
  739. 99% {
  740. opacity: 1;
  741. transform: translate(-50%, -50%) perspective(200px) rotateX(90deg);
  742. }
  743. 100% {
  744. opacity: 0;
  745. }
  746. }
  747.  
  748. .title {
  749. margin: 0;
  750. font-weight: bolder;
  751. font-size: 120%;
  752. }
  753.  
  754. .speedSelect, .enableSelect {
  755. margin: 8px;
  756. }
  757.  
  758. .closeButton {
  759. display: block;
  760. text-align: center;
  761. }
  762.  
  763. .closeButton {
  764. display: block;
  765. pading: 8px;
  766. cursor: pointer;
  767. }
  768. </style>
  769. <div class="root HeatSyncConfigPanel">
  770. <p class="title">†HeatSync†</p>
  771.  
  772. <div class="speedSelect dmc">
  773. <span>最大倍率(新仕様サーバー)</span>
  774. <select data-config-name="turbo.dmc-blue">
  775. <option value="3">3.0</option>
  776. <option>2.9</option>
  777. <option>2.8</option>
  778. <option>2.7</option>
  779. <option>2.6</option>
  780. <option>2.5</option>
  781. <option>2.4</option>
  782. <option>2.3</option>
  783. <option>2.2</option>
  784. <option>2.1</option>
  785. <option value="2">2.0</option>
  786. <option>1.9</option>
  787. <option>1.8</option>
  788. <option>1.7</option>
  789. <option>1.6</option>
  790. <option>1.5</option>
  791. <option>1.4</option>
  792. <option>1.3</option>
  793. <option>1.2</option>
  794. <option>1.1</option>
  795. <option value="1">1</option>
  796. </select>
  797. </div>
  798.  
  799. <div class="speedSelect smile">
  800. <span>最大倍率(旧仕様サーバー)</span>
  801. <select data-config-name="turbo.smile-blue">
  802. <option value="3">3.0</option>
  803. <option>2.9</option>
  804. <option>2.8</option>
  805. <option>2.7</option>
  806. <option>2.6</option>
  807. <option>2.5</option>
  808. <option>2.4</option>
  809. <option>2.3</option>
  810. <option>2.2</option>
  811. <option>2.1</option>
  812. <option value="2">2.0</option>
  813. <option>1.9</option>
  814. <option>1.8</option>
  815. <option>1.7</option>
  816. <option>1.6</option>
  817. <option>1.5</option>
  818. <option>1.4</option>
  819. <option>1.3</option>
  820. <option>1.2</option>
  821. <option>1.1</option>
  822. <option value="1">1</option>
  823. </select>
  824. </div>
  825.  
  826. <div class="speedSelect minimum">
  827. <span>最低倍率</span>
  828. <select data-config-name="turbo.red">
  829. <option value="3">3.0</option>
  830. <option>2.9</option>
  831. <option>2.8</option>
  832. <option>2.7</option>
  833. <option>2.6</option>
  834. <option>2.5</option>
  835. <option>2.4</option>
  836. <option>2.3</option>
  837. <option>2.2</option>
  838. <option>2.1</option>
  839. <option value="2">2.0</option>
  840. <option>1.9</option>
  841. <option>1.8</option>
  842. <option>1.7</option>
  843. <option>1.6</option>
  844. <option>1.5</option>
  845. <option>1.4</option>
  846. <option>1.3</option>
  847. <option>1.2</option>
  848. <option>1.1</option>
  849. <option value="1">1.0</option>
  850. </select>
  851. </div>
  852.  
  853. <div class="enableSelect">
  854. <label>
  855. <input type="checkbox" data-config-name="turbo.enabled" data-type="bool" />
  856. HeatSyncを有効にする
  857. </label>
  858. </div>
  859.  
  860. <div class="closeButtonContainer">
  861. <button class="closeButton" type="button">
  862. 閉じる
  863. </button>
  864. </div>
  865.  
  866. </div>
  867. `).trim();
  868.  
  869.  
  870.  
  871.  
  872. const initExternal = () => {
  873. product.external = {
  874. };
  875.  
  876. product.isReady = true;
  877. const ev = new CustomEvent(`${PRODUCT}Initialized`, { detail: { product } });
  878. document.body.dispatchEvent(ev);
  879. };
  880.  
  881. const initDom = () => {
  882. let configPanel;
  883. const li = document.createElement('li');
  884. li.innerHTML = `<a href="javascript:;">†HeatSync†設定</a>`;
  885. li.addEventListener('click', () => {
  886. if (!configPanel) {
  887. configPanel = new ConfigPanel(document.body);
  888. }
  889. configPanel.toggle();
  890. });
  891. document.querySelector('#siteHeaderRightMenuContainer').appendChild(li);
  892. };
  893.  
  894. const init = () => {
  895. let syncer;
  896. ZenzaDetector.detect().then(() => {
  897. window.ZenzaWatch.emitter.on('DialogPlayerClose', () => {
  898. util.emitter.emit('zenzaClose');
  899. });
  900.  
  901. window.ZenzaWatch.emitter.on('heatMapUpdate', (p) => {
  902. util.emitter.emit('heatMapUpdate', p);
  903. });
  904.  
  905. initDom();
  906.  
  907. //console.info('detect zenzawatch...');
  908.  
  909. syncer = new Syncer();
  910.  
  911. initExternal(syncer);
  912. });
  913. };
  914.  
  915. init();
  916. };
  917.  
  918. (() => {
  919. const script = document.createElement('script');
  920. script.id = `${PRODUCT}Loader`;
  921. script.setAttribute('type', 'text/javascript');
  922. script.setAttribute('charset', 'UTF-8');
  923. script.appendChild(document.createTextNode( '(' + monkey + ')("' + PRODUCT + '");' ));
  924. document.body.appendChild(script);
  925. })();
  926. })();

QingJ © 2025

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