HeatSync

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

目前为 2017-04-19 提交的版本。查看 最新版本

  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.5
  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. 'turbo.minDuration': 30,
  276.  
  277. };
  278.  
  279. const config = {};
  280. let noEmit = false;
  281.  
  282.  
  283. emitter.refresh = (emitChange = false) => {
  284. Object.keys(defaultConfig).forEach(key => {
  285. var storageKey = prefix + key;
  286. if (localStorage.hasOwnProperty(storageKey)) {
  287. try {
  288. let lastValue = config[key];
  289. let newValue = JSON.parse(localStorage.getItem(storageKey));
  290. if (lastValue !== newValue) {
  291. config[key] = newValue;
  292. if (emitChange) {
  293. emitter.emit('key', newValue);
  294. emitter.emit('@update', {key, value: newValue});
  295. }
  296. }
  297. } catch (e) {
  298. window.console.error('config parse error key:"%s" value:"%s" ', key, localStorage.getItem(storageKey), e);
  299. config[key] = defaultConfig[key];
  300. }
  301. } else {
  302. config[key] = defaultConfig[key];
  303. }
  304. });
  305. };
  306. emitter.refresh();
  307.  
  308. emitter.getValue = function(key, refresh) {
  309. if (refresh) {
  310. emitter.refreshValue(key);
  311. }
  312. return config[key];
  313. };
  314.  
  315. emitter.setValue = function(key, value) {
  316. if (config[key] !== value && arguments.length >= 2) {
  317. var storageKey = prefix + key;
  318. localStorage.setItem(storageKey, value);
  319. config[key] = value;
  320. emitter.emit(key, value);
  321. emitter.emit('@update', {key, value});
  322. broadcast.send('configUpdate');
  323. //console.log('%cconfig update "%s" = "%s"', 'background: cyan', key, value);
  324. }
  325. };
  326.  
  327. emitter.clearConfig = function() {
  328. noEmit = true;
  329. Object.keys(defaultConfig).forEach(key => {
  330. if (_.contains(['message', 'debug'], key)) { return; }
  331. var storageKey = prefix + key;
  332. try {
  333. if (localStorage.hasOwnProperty(storageKey)) {
  334. localStorage.removeItem(storageKey);
  335. }
  336. config[key] = defaultConfig[key];
  337. } catch (e) {}
  338. });
  339. noEmit = false;
  340. };
  341.  
  342. emitter.getKeys = function() {
  343. return Object.keys(defaultConfig);
  344. };
  345.  
  346. emitter.namespace = function(name) {
  347. return {
  348. getValue: (key) => { return emitter.getValue(name + '.' + key); },
  349. setValue: (key, value) => { emitter.setValue(name + '.' + key, value); },
  350. refresh: () => { emitter.refresh(); },
  351. on: (key, func) => {
  352. if (key === '@update') {
  353. emitter.on('@update', ({key, value}) => {
  354. const pre = name + '.';
  355. //console.log('@update', key, value, pre);
  356. if (key.startsWith(pre)) {
  357. func({key: key.replace(pre, ''), value});
  358. }
  359. });
  360. } else {
  361. emitter.on(name + '.' + key, func);
  362. }
  363. }
  364. };
  365. };
  366.  
  367. util.emitter.on('broadcast', (type) => {
  368. console.log('%cbroadcast "%s"', 'background: cyan', type);
  369. //if (type !== 'configUpdate') { return; }
  370. emitter.refresh(false);
  371. emitter.emit('refresh');
  372. });
  373.  
  374. return emitter;
  375. })();
  376. product.config = config;
  377.  
  378. class Syncer extends Emitter {
  379. constructor() {
  380. super();
  381. this._timer = null;
  382. this._videoElement = null;
  383. this._rate = 1.0;
  384.  
  385. this._config = config.namespace('turbo');
  386.  
  387. util.emitter.on('heatMapUpdate', this._onHeatMapUpdate.bind(this));
  388. util.emitter.on('zenzaClose', this._onZenzaClose.bind(this));
  389. }
  390.  
  391. enable() {
  392. if (this._timer) { return; }
  393. console.info('start timer', this._timer, this._rate, this._map);
  394. this._timer = setInterval(this._onTimer.bind(this), 500);
  395. }
  396.  
  397. disable() {
  398. clearInterval(this._timer);
  399. this._rate = config.getValue('turbo.red');
  400. window.ZenzaWatch.config.setValue('playbackRate', this._rate);
  401. this._timer = null;
  402. }
  403.  
  404. _onZenzaClose() {
  405. this.disable();
  406. }
  407.  
  408. _onHeatMapUpdate({map, duration}) {
  409. this._map = map;
  410. this._duration = duration;
  411. this._rate = config.getValue('turbo.red');
  412. if (duration < config.getValue('turbo.minDuration')) {
  413. return this.disable();
  414. }
  415. this.enable();
  416. }
  417.  
  418. _onTimer() {
  419. if (!this._videoElement) {
  420. this._videoElement = window.ZenzaWatch.external.getVideoElement();
  421. if (!this._videoElement) { return; }
  422. }
  423. const video = this._videoElement;
  424. if (video.paused || !config.getValue('turbo.enabled')) { return; }
  425. const duration = video.duration;
  426. const current = video.currentTime;
  427. const per = current / duration;
  428. const isDmc = /dmc\.nico/.test(video.src);
  429. const map = this._map;
  430. const pos = Math.floor(map.length * per);
  431.  
  432. const blue = parseFloat(isDmc ?
  433. config.getValue('turbo.dmc-blue') : config.getValue('turbo.smile-blue'));
  434. const red = parseFloat(config.getValue('turbo.red'));
  435.  
  436. const pt = map[pos];
  437.  
  438. let ratePer = (256 - pt) / 256;
  439. if (ratePer > 0.95) { ratePer = 1; }
  440. if (ratePer < 0.4) { ratePer = 0; }
  441. let rate = red + (blue - red) * ratePer;
  442.  
  443. rate = Math.round(rate * 100) / 100;
  444. //console.info('onTimer', pt, pt / 255, Math.round(ratePer * 100) / 100, rate);
  445.  
  446. if (Math.abs(rate - this._rate) < 0.05) { return; }
  447. this._rate = rate;
  448. // ユーザーが自分でスロー再生してるっぽい時は何もしない
  449. if (video.playbackRate < red) {
  450. return;
  451. }
  452. window.ZenzaWatch.config.setValue('playbackRate', this._rate);
  453. }
  454. }
  455.  
  456. class BaseViewComponent extends Emitter {
  457. constructor({parentNode = null, name = '', template = '', shadow = '', css = ''}) {
  458. super();
  459.  
  460. this._params = {parentNode, name, template, shadow, css};
  461. this._bound = {};
  462. this._state = {};
  463. this._props = {};
  464. this._elm = {};
  465.  
  466. this._initDom({
  467. parentNode,
  468. name,
  469. template,
  470. shadow,
  471. css
  472. });
  473. }
  474.  
  475. _initDom({parentNode, name, template, css = '', shadow = ''}) {
  476. let tplId = `${PRODUCT}${name}Template`;
  477. let tpl = document.getElementById(tplId);
  478. if (!tpl) {
  479. if (css) { util.addStyle(css, `${name}Style`); }
  480. tpl = document.createElement('template');
  481. tpl.innerHTML = template;
  482. tpl.id = tplId;
  483. document.body.appendChild(tpl);
  484. }
  485. const onClick = this._bound.onClick = this._onClick.bind(this);
  486.  
  487. const view = document.importNode(tpl.content, true);
  488. this._view = view.querySelector('*') || document.createDocumentFragment();
  489. if (this._view) {
  490. this._view.addEventListener('click', onClick);
  491. }
  492. this.appendTo(parentNode);
  493.  
  494. if (shadow) {
  495. this._attachShadow({host: this._view, name, shadow});
  496. if (!this._isDummyShadow) {
  497. this._shadow.addEventListener('click', onClick);
  498. }
  499. }
  500. }
  501.  
  502. _attachShadow ({host, shadow, name, mode = 'open'}) {
  503. let tplId = `${PRODUCT}${name}Shadow`;
  504. let tpl = document.getElementById(tplId);
  505. if (!tpl) {
  506. tpl = document.createElement('template');
  507. tpl.innerHTML = shadow;
  508. tpl.id = tplId;
  509. document.body.appendChild(tpl);
  510. }
  511.  
  512. if (!host.attachShadow && !host.createShadowRoot) {
  513. return this._fallbackNoneShadowDom({host, tpl, name});
  514. }
  515.  
  516. const root = host.attachShadow ?
  517. host.attachShadow({mode}) : host.createShadowRoot();
  518. const node = document.importNode(tpl.content, true);
  519. root.appendChild(node);
  520. this._shadowRoot = root;
  521. this._shadow = root.querySelector('.root');
  522. this._isDummyShadow = false;
  523. }
  524.  
  525. _fallbackNoneShadowDom({host, tpl, name}) {
  526. const node = document.importNode(tpl.content, true);
  527. const style = node.querySelector('style');
  528. style.remove();
  529. util.addStyle(style.innerHTML, `${name}Shadow`);
  530. host.appendChild(node);
  531. this._shadow = this._shadowRoot = host.querySelector('.root');
  532. this._isDummyShadow = true;
  533. }
  534.  
  535. setState(key, val) {
  536. if (typeof key === 'string') {
  537. this._setState(key, val);
  538. }
  539. Object.keys(key).forEach(k => {
  540. this._setState(k, key[k]);
  541. });
  542. }
  543.  
  544. _setState(key, val) {
  545. if (this._state[key] !== val) {
  546. this._state[key] = val;
  547. if (/^is(.*)$/.test(key)) {
  548. this.toggleClass(`is-${RegExp.$1}`, !!val);
  549. }
  550. this.emit('update', {key, val});
  551. }
  552. }
  553.  
  554. _onClick(e) {
  555. const target = e.target.classList.contains('command') ?
  556. e.target : e.target.closest('.command');
  557.  
  558. if (!target) { return; }
  559.  
  560. const command = target.getAttribute('data-command');
  561. if (!command) { return; }
  562. const type = target.getAttribute('data-type') || 'string';
  563. let param = target.getAttribute('data-param');
  564. e.stopPropagation();
  565. e.preventDefault();
  566. param = this._parseParam(param, type);
  567. this._onCommand(command, param);
  568. }
  569.  
  570. _parseParam(param, type) {
  571. switch (type) {
  572. case 'json':
  573. case 'bool':
  574. case 'number':
  575. param = JSON.parse(param);
  576. break;
  577. }
  578. return param;
  579. }
  580.  
  581. appendTo(parentNode) {
  582. if (!parentNode) { return; }
  583. this._parentNode = parentNode;
  584. parentNode.appendChild(this._view);
  585. }
  586.  
  587. _onCommand(command, param) {
  588. this.emit('command', command, param);
  589. }
  590.  
  591. toggleClass(className, v) {
  592. (className || '').split(/ +/).forEach((c) => {
  593. if (this._view && this._view.classList) {
  594. this._view.classList.toggle(c, v);
  595. }
  596. if (this._shadow && this._shadow.classList) {
  597. this._shadow.classList.toggle(c, this._view.classList.contains(c));
  598. }
  599. });
  600. }
  601.  
  602. addClass(name) { this.toggleClass(name, true); }
  603. removeClass(name) { this.toggleClass(name, false); }
  604. }
  605.  
  606. class ConfigPanel extends BaseViewComponent {
  607. constructor({parentNode}) {
  608. super({
  609. parentNode,
  610. name: 'HeatSyncConfigPanel',
  611. shadow: ConfigPanel.__shadow__,
  612. template: '<div class="HeatSyncConfigPanelContainer"></div>',
  613. css: ''
  614. });
  615. this._state = {
  616. isOpen: false,
  617. isVisible: false
  618. };
  619. config.on('refresh', this._onBeforeShow.bind(this));
  620. }
  621.  
  622. _initDom(...args) {
  623. super._initDom(...args);
  624. const v = this._shadow;
  625.  
  626. this._elm.red = v.querySelector('*[data-config-name="turbo.red"]');
  627. this._elm.dmc = v.querySelector('*[data-config-name="turbo.dmc-blue"]');
  628. this._elm.smile = v.querySelector('*[data-config-name="turbo.smile-blue"]');
  629. this._elm.minDur = v.querySelector('*[data-config-name="turbo.minDuration"]');
  630. this._elm.enabled = v.querySelector('*[data-config-name="turbo.enabled"]');
  631.  
  632. const onChange = (e) => {
  633. const target = e.target, name = target.getAttribute('data-config-name');
  634. switch (target.tagName) {
  635. case 'INPUT':
  636. case 'SELECT':
  637. const type = target.getAttribute('data-type');
  638. const value = this._parseParam(target.value, type);
  639. config.setValue(name, value);
  640. break;
  641. default:
  642. //console.info('target', e, target, name, target.checked);
  643. config.setValue(name, !!target.checked);
  644. break;
  645. }
  646. };
  647. this._elm.red .addEventListener('change', onChange);
  648. this._elm.dmc .addEventListener('change', onChange);
  649. this._elm.smile .addEventListener('change', onChange);
  650. this._elm.minDur .addEventListener('change', onChange);
  651. this._elm.enabled.addEventListener('change', onChange);
  652.  
  653. v.querySelector('.closeButton')
  654. .addEventListener('click', this.hide.bind(this));
  655. }
  656.  
  657. _onClick(e) {
  658. super._onClick(e);
  659. }
  660.  
  661. _onMouseDown(e) {
  662. this.hide();
  663. this._onClick(e);
  664. }
  665.  
  666. show() {
  667. document.body.addEventListener('click', this._bound.onBodyClick);
  668. this._onBeforeShow();
  669.  
  670. this.setState({isOpen: true});
  671. window.setTimeout(() => {
  672. this.setState({isVisible: true});
  673. }, 100);
  674. }
  675.  
  676. hide() {
  677. document.body.removeEventListener('click', this._bound.onBodyClick);
  678. this.setState({isVisible: false});
  679. window.setTimeout(() => {
  680. this.setState({isOpen: false});
  681. }, 2100);
  682. }
  683.  
  684. toggle() {
  685. if (this._state.isOpen) {
  686. this.hide();
  687. } else {
  688. this.show();
  689. }
  690. }
  691.  
  692. _onBeforeShow() {
  693. this._elm.red.value = '' + config.getValue('turbo.red');
  694. this._elm.dmc.value = '' + config.getValue('turbo.dmc-blue');
  695. this._elm.smile.value = '' + config.getValue('turbo.smile-blue');
  696. this._elm.minDur.value = '' + config.getValue('turbo.minDuration');
  697.  
  698. this._elm.enabled.checked = !!config.getValue('turbo.enabled');
  699. }
  700. }
  701.  
  702. ConfigPanel.__shadow__ = (`
  703. <style>
  704. .HeatSyncConfigPanel {
  705. display: none;
  706. position: fixed;
  707. z-index: ${CONSTANT.BASE_Z_INDEX};
  708. top: 50vh;
  709. left: 50vw;
  710. padding: 8px;
  711. border: 2px outset;
  712. box-shadow: 0 0 8px #000;
  713. background: #ccc;
  714. transform: translate(-50%, -50%);
  715. /*transform: translate(-50%, -50%) perspective(200px) rotateX(90deg);*/
  716. transition: opacity 0.5s;
  717. transform-origin: center bottom;
  718. animation-timing-function: steps(10);
  719. perspective-origin: center bottom;
  720. user-select: none;
  721. -webkit-user-select: none;
  722. -moz-user-select: none;
  723. }
  724.  
  725. .HeatSyncConfigPanel.is-Open {
  726. display: block;
  727. opacity: 0;
  728. /*animation-name: dokahide;*/
  729. }
  730. .HeatSyncConfigPanel.is-Open.is-Visible {
  731. opacity: 1;
  732. /*animation-name: dokashow;*/
  733. /*transform: translate(-50%, -50%) perspective(200px) rotateX(0deg);*/
  734. }
  735.  
  736. @keyframes dokashow {
  737. 0% {
  738. opacity: 1;
  739. transform: translate(-50%, -50%) perspective(200px) rotateX(90deg);
  740. }
  741. 100% {
  742. opacity: 1;
  743. transform: translate(-50%, -50%) perspective(200px) rotateX(0deg);
  744. }
  745. }
  746.  
  747. @keyframes dokahide {
  748. 0% {
  749. opacity: 1;
  750. transform: translate(-50%, -50%) perspective(200px) rotateX(0deg);
  751. }
  752. 99% {
  753. opacity: 1;
  754. transform: translate(-50%, -50%) perspective(200px) rotateX(90deg);
  755. }
  756. 100% {
  757. opacity: 0;
  758. }
  759. }
  760.  
  761. .title {
  762. margin: 0;
  763. font-weight: bolder;
  764. font-size: 120%;
  765. }
  766.  
  767. .speedSelect {
  768. margin: 8px;
  769. }
  770.  
  771. .minDuration {
  772. margin: 8px;
  773. }
  774.  
  775. .enableSelect {
  776. margin: 8px;
  777. }
  778.  
  779. .closeButton {
  780. display: block;
  781. text-align: center;
  782. }
  783.  
  784. .closeButton {
  785. display: block;
  786. pading: 8px;
  787. cursor: pointer;
  788. margin: auto;
  789. }
  790.  
  791. label {
  792. cursor: pointer;
  793. }
  794.  
  795. input[type="number"] {
  796. width: 50px;
  797. }
  798. </style>
  799. <div class="root HeatSyncConfigPanel">
  800. <p class="title">†HeatSync†</p>
  801.  
  802. <div class="speedSelect dmc">
  803. <span>最高倍率(新仕様サーバー)</span>
  804. <select data-config-name="turbo.dmc-blue" data-type="number">
  805. <option value="3">3.0</option>
  806. <option>2.9</option>
  807. <option>2.8</option>
  808. <option>2.7</option>
  809. <option>2.6</option>
  810. <option>2.5</option>
  811. <option>2.4</option>
  812. <option>2.3</option>
  813. <option>2.2</option>
  814. <option>2.1</option>
  815. <option value="2">2.0</option>
  816. <option>1.9</option>
  817. <option>1.8</option>
  818. <option>1.7</option>
  819. <option>1.6</option>
  820. <option>1.5</option>
  821. <option>1.4</option>
  822. <option>1.3</option>
  823. <option>1.2</option>
  824. <option>1.1</option>
  825. <option value="1">1</option>
  826. </select>
  827. </div>
  828.  
  829. <div class="speedSelect smile">
  830. <span>最高倍率(旧仕様サーバー)</span>
  831. <select data-config-name="turbo.smile-blue" data-type="number">
  832. <option value="3">3.0</option>
  833. <option>2.9</option>
  834. <option>2.8</option>
  835. <option>2.7</option>
  836. <option>2.6</option>
  837. <option>2.5</option>
  838. <option>2.4</option>
  839. <option>2.3</option>
  840. <option>2.2</option>
  841. <option>2.1</option>
  842. <option value="2">2.0</option>
  843. <option>1.9</option>
  844. <option>1.8</option>
  845. <option>1.7</option>
  846. <option>1.6</option>
  847. <option>1.5</option>
  848. <option>1.4</option>
  849. <option>1.3</option>
  850. <option>1.2</option>
  851. <option>1.1</option>
  852. <option value="1">1</option>
  853. </select>
  854. </div>
  855.  
  856. <div class="speedSelect minimum">
  857. <span>最低倍率</span>
  858. <select data-config-name="turbo.red" data-type="number">
  859. <option value="3">3.0</option>
  860. <option>2.9</option>
  861. <option>2.8</option>
  862. <option>2.7</option>
  863. <option>2.6</option>
  864. <option>2.5</option>
  865. <option>2.4</option>
  866. <option>2.3</option>
  867. <option>2.2</option>
  868. <option>2.1</option>
  869. <option value="2">2.0</option>
  870. <option>1.9</option>
  871. <option>1.8</option>
  872. <option>1.7</option>
  873. <option>1.6</option>
  874. <option>1.5</option>
  875. <option>1.4</option>
  876. <option>1.3</option>
  877. <option>1.2</option>
  878. <option>1.1</option>
  879. <option value="1">1.0</option>
  880. </select>
  881. </div>
  882.  
  883. <div class="minDuration">
  884. <label>
  885. <input type="number" data-config-name="turbo.minDuration" data-type="number">
  886. 秒未満の動画には適用しない
  887. </label>
  888. </div>
  889.  
  890. <div class="enableSelect">
  891. <label>
  892. <input type="checkbox" data-config-name="turbo.enabled" data-type="bool">
  893. HeatSyncを有効にする
  894. </label>
  895. </div>
  896.  
  897. <div class="closeButtonContainer">
  898. <button class="closeButton" type="button">
  899. 閉じる
  900. </button>
  901. </div>
  902.  
  903. </div>
  904. `).trim();
  905.  
  906.  
  907.  
  908.  
  909. const initExternal = (syncer) => {
  910. product.external = {
  911. syncer
  912. };
  913.  
  914. product.isReady = true;
  915. const ev = new CustomEvent(`${PRODUCT}Initialized`, { detail: { product } });
  916. document.body.dispatchEvent(ev);
  917. };
  918.  
  919. const initDom = () => {
  920. let configPanel;
  921. const li = document.createElement('li');
  922. li.innerHTML = `<a href="javascript:;">†HeatSync†設定</a>`;
  923. li.addEventListener('click', () => {
  924. if (!configPanel) {
  925. configPanel = new ConfigPanel({parentNode: document.body});
  926. }
  927. configPanel.toggle();
  928. });
  929. document.querySelector('#siteHeaderRightMenuContainer').appendChild(li);
  930. };
  931.  
  932. const init = () => {
  933. let syncer;
  934. ZenzaDetector.detect().then(() => {
  935. window.ZenzaWatch.emitter.on('DialogPlayerClose', () => {
  936. util.emitter.emit('zenzaClose');
  937. });
  938.  
  939. window.ZenzaWatch.emitter.on('heatMapUpdate', (p) => {
  940. util.emitter.emit('heatMapUpdate', p);
  941. });
  942.  
  943. initDom();
  944.  
  945. //console.info('detect zenzawatch...');
  946.  
  947. syncer = new Syncer();
  948.  
  949. initExternal(syncer);
  950. });
  951. };
  952.  
  953. init();
  954. };
  955.  
  956. (() => {
  957. const script = document.createElement('script');
  958. script.id = `${PRODUCT}Loader`;
  959. script.setAttribute('type', 'text/javascript');
  960. script.setAttribute('charset', 'UTF-8');
  961. script.appendChild(document.createTextNode( '(' + monkey + ')("' + PRODUCT + '");' ));
  962. document.body.appendChild(script);
  963. })();
  964. })();

QingJ © 2025

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