ZenzaGamePad

ZenzaWatchをゲームパッドで操作

  1. // ==UserScript==
  2. // @name ZenzaGamePad
  3. // @namespace https://github.com/segabito/
  4. // @description ZenzaWatchをゲームパッドで操作
  5. // @include *://*.nicovideo.jp/*
  6. // @version 1.5.3
  7. // @author segabito macmoto
  8. // @license public domain
  9. // @grant none
  10. // @noframes
  11. // ==/UserScript==
  12. /* eslint-disable */
  13.  
  14.  
  15. // 推奨
  16. //
  17. // XInput系 (XboxOne, Xbox360コントローラ等)
  18. // DualShock4
  19. // USBサターンパッド
  20. // 8bitdo FC30系
  21. // Joy-Con L R
  22.  
  23.  
  24. (async (window) => {
  25.  
  26. const monkey = (ZenzaWatch) => {
  27. if (!window.navigator.getGamepads) {
  28. window.console.log('%cGamepad APIがサポートされていません', 'background: red; color: yellow;');
  29. return;
  30. }
  31.  
  32. const PRODUCT = 'ZenzaGamePad';
  33. const CONSTANT = {
  34. BASE_Z_INDEX: 150000
  35. };
  36.  
  37. const ButtonMapJoyConL = {
  38. Y: 0,
  39. B: 1,
  40. X: 2,
  41. A: 3,
  42. SUP: 4,
  43. SDN: 5,
  44. SEL: 8,
  45. CAP: 13,
  46. LR: 14,
  47. META: 15,
  48. PUSH: 10
  49. };
  50. const ButtonMapJoyConR = {
  51. Y: 3,
  52. B: 2,
  53. X: 1,
  54. A: 0,
  55. SUP: 5,
  56. SDN: 4,
  57. SEL: 9,
  58. CAP: 12,
  59. LR: 14,
  60. META: 15,
  61. PUSH: 11
  62. };
  63.  
  64. const JoyConAxisCenter = +1.28571;
  65.  
  66. const AxisMapJoyConL = {
  67. CENTER: JoyConAxisCenter,
  68. UP: +0.71429,
  69. U_R: +1.00000,
  70. RIGHT: -1.00000,
  71. D_R: -0.71429,
  72. DOWN: -0.42857,
  73. D_L: -0.14286,
  74. LEFT: +0.14286,
  75. U_L: +0.42857,
  76. };
  77.  
  78. const AxisMapJoyConR = {
  79. CENTER: JoyConAxisCenter,
  80. UP: -0.42857,
  81. U_R: -0.14286,
  82. RIGHT: +0.14286,
  83. D_R: +0.42857,
  84. DOWN: +0.71429,
  85. D_L: +1.00000,
  86. LEFT: -1.00000,
  87. U_L: -0.71429,
  88. };
  89.  
  90. let _ = window._ || ZenzaWatch.lib._;
  91. let $ = window.jQuery || ZenzaWatch.lib.$;
  92. let util = ZenzaWatch.util;
  93. let Emitter = ZenzaWatch.modules ? ZenzaWatch.modules.Emitter : ZenzaWatch.lib.AsyncEmitter;
  94.  
  95. let isZenzaWatchOpen = false;
  96.  
  97. let console;
  98. let debugMode = !true;
  99.  
  100. let dummyConsole = {
  101. log: _.noop, error: _.noop, time: _.noop, timeEnd: _.noop, trace: _.noop
  102. };
  103. console = debugMode ? window.console : dummyConsole;
  104.  
  105. let isPauseButtonDown = false;
  106. let isRate1ButtonDown = false;
  107. let isMetaButtonDown = false;
  108.  
  109. const getVideo = () => {
  110. return document.querySelector('.zenzaWatchVideoElement');
  111. };
  112.  
  113. const video = {
  114. get duration() {
  115. return getVideo().duration;
  116. }
  117. };
  118.  
  119. const Config = (() => {
  120. const prefix = PRODUCT + '_config_';
  121. const emitter = new Emitter();
  122.  
  123. const defaultConfig = {
  124. debug: false,
  125. enabled: true,
  126. needFocus: false,
  127. deviceIndex: 0
  128. };
  129.  
  130. const config = {};
  131.  
  132. emitter.refresh = (emitChange = false) => {
  133. Object.keys(defaultConfig).forEach(key => {
  134. const storageKey = prefix + key;
  135. if (localStorage.hasOwnProperty(storageKey)) {
  136. try {
  137. let lastValue = config[key];
  138. let newValue = JSON.parse(localStorage.getItem(storageKey));
  139. if (lastValue !== newValue) {
  140. config[key] = newValue;
  141. if (emitChange) {
  142. emitter.emit('key', newValue);
  143. emitter.emit('@update', {key, value: newValue});
  144. }
  145. }
  146. } catch (e) {
  147. window.console.error('config parse error key:"%s" value:"%s" ', key, localStorage.getItem(storageKey), e);
  148. config[key] = defaultConfig[key];
  149. }
  150. } else {
  151. config[key] = defaultConfig[key];
  152. }
  153. });
  154. };
  155. emitter.refresh();
  156.  
  157. emitter.get = (key, refresh) => {
  158. if (refresh) {
  159. emitter.refreshValue(key);
  160. }
  161. return config[key];
  162. };
  163.  
  164. emitter.set = (key, value = undefined) => {
  165. if (config[key] !== value && value !== undefined) {
  166. const storageKey = prefix + key;
  167. localStorage.setItem(storageKey, JSON.stringify(value));
  168. config[key] = value;
  169. emitter.emit(key, value);
  170. emitter.emit('@update', {key, value});
  171. }
  172. };
  173.  
  174. return emitter;
  175. })();
  176.  
  177. class BaseViewComponent extends Emitter {
  178. constructor({parentNode = null, name = '', template = '', shadow = '', css = ''}) {
  179. super();
  180.  
  181. this._params = {parentNode, name, template, shadow, css};
  182. this._bound = {};
  183. this._state = {};
  184. this._props = {};
  185. this._elm = {};
  186.  
  187. this._initDom({
  188. parentNode,
  189. name,
  190. template,
  191. shadow,
  192. css
  193. });
  194. }
  195.  
  196. _initDom({parentNode, name, template, css = '', shadow = ''}) {
  197. let tplId = `${PRODUCT}${name}Template`;
  198. let tpl = document.getElementById(tplId);
  199. if (!tpl) {
  200. if (css) { util.addStyle(css, `${name}Style`); }
  201. tpl = document.createElement('template');
  202. tpl.innerHTML = template;
  203. tpl.id = tplId;
  204. document.body.appendChild(tpl);
  205. }
  206. const onClick = this._bound.onClick = this._onClick.bind(this);
  207.  
  208. const view = document.importNode(tpl.content, true);
  209. this._view = view.querySelector('*') || document.createDocumentFragment();
  210. if (this._view) {
  211. this._view.addEventListener('click', onClick);
  212. }
  213. this.appendTo(parentNode);
  214.  
  215. if (shadow) {
  216. this._attachShadow({host: this._view, name, shadow});
  217. if (!this._isDummyShadow) {
  218. this._shadow.addEventListener('click', onClick);
  219. }
  220. }
  221. }
  222.  
  223. _attachShadow ({host, shadow, name, mode = 'open'}) {
  224. let tplId = `${PRODUCT}${name}Shadow`;
  225. let tpl = document.getElementById(tplId);
  226. if (!tpl) {
  227. tpl = document.createElement('template');
  228. tpl.innerHTML = shadow;
  229. tpl.id = tplId;
  230. document.body.appendChild(tpl);
  231. }
  232.  
  233. if (!host.attachShadow && !host.createShadowRoot) {
  234. return this._fallbackNoneShadowDom({host, tpl, name});
  235. }
  236.  
  237. const root = host.attachShadow ?
  238. host.attachShadow({mode}) : host.createShadowRoot();
  239. const node = document.importNode(tpl.content, true);
  240. root.appendChild(node);
  241. this._shadowRoot = root;
  242. this._shadow = root.querySelector('.root');
  243. this._isDummyShadow = false;
  244. }
  245.  
  246. _fallbackNoneShadowDom({host, tpl, name}) {
  247. const node = document.importNode(tpl.content, true);
  248. const style = node.querySelector('style');
  249. style.remove();
  250. util.addStyle(style.innerHTML, `${name}Shadow`);
  251. host.appendChild(node);
  252. this._shadow = this._shadowRoot = host.querySelector('.root');
  253. this._isDummyShadow = true;
  254. }
  255.  
  256. setState(key, val) {
  257. if (typeof key === 'string') {
  258. this._setState(key, val);
  259. }
  260. Object.keys(key).forEach(k => {
  261. this._setState(k, key[k]);
  262. });
  263. }
  264.  
  265. _setState(key, val) {
  266. if (this._state[key] !== val) {
  267. this._state[key] = val;
  268. if (/^is(.*)$/.test(key)) {
  269. this.toggleClass(`is-${RegExp.$1}`, !!val);
  270. }
  271. this.emit('update', {key, val});
  272. }
  273. }
  274.  
  275. _onClick(e) {
  276. const target = e.target.classList.contains('command') ?
  277. e.target : e.target.closest('.command');
  278.  
  279. if (!target) { return; }
  280.  
  281. const command = target.getAttribute('data-command');
  282. if (!command) { return; }
  283. const type = target.getAttribute('data-type') || 'string';
  284. let param = target.getAttribute('data-param');
  285. e.stopPropagation();
  286. e.preventDefault();
  287. param = this._parseParam(param, type);
  288. this._onCommand(command, param);
  289. }
  290.  
  291. _parseParam(param, type) {
  292. switch (type) {
  293. case 'json':
  294. case 'bool':
  295. case 'number':
  296. param = JSON.parse(param);
  297. break;
  298. }
  299. return param;
  300. }
  301.  
  302. appendTo(parentNode) {
  303. if (!parentNode) { return; }
  304. this._parentNode = parentNode;
  305. parentNode.appendChild(this._view);
  306. }
  307.  
  308. _onCommand(command, param) {
  309. this.emit('command', command, param);
  310. }
  311.  
  312. toggleClass(className, v) {
  313. (className || '').split(/ +/).forEach((c) => {
  314. if (this._view && this._view.classList) {
  315. this._view.classList.toggle(c, v);
  316. }
  317. if (this._shadow && this._shadow.classList) {
  318. this._shadow.classList.toggle(c, this._view.classList.contains(c));
  319. }
  320. });
  321. }
  322.  
  323. addClass(name) { this.toggleClass(name, true); }
  324. removeClass(name) { this.toggleClass(name, false); }
  325. }
  326.  
  327. class ConfigPanel extends BaseViewComponent {
  328. constructor({parentNode}) {
  329. super({
  330. parentNode,
  331. name: 'ZenzaGamePadConfigPanel',
  332. shadow: ConfigPanel.__shadow__,
  333. template: '<div class="ZenzaGamePadConfigPanelContainer zen-family"></div>',
  334. css: ''
  335. });
  336. this._state = {
  337. isOpen: false,
  338. isVisible: false
  339. };
  340. Config.on('refresh', this._onBeforeShow.bind(this));
  341. }
  342.  
  343. _initDom(...args) {
  344. super._initDom(...args);
  345. const v = this._shadow;
  346.  
  347. this._elm.enabled = v.querySelector('[data-config-name="enabled"]');
  348. this._elm.needFocus = v.querySelector('[data-config-name="needFocus"]');
  349. this._elm.deviceIndex = v.querySelector('[data-config-name="deviceIndex"]');
  350.  
  351. const onChange = e => {
  352. const target = e.target, name = target.getAttribute('data-config-name');
  353. switch (target.tagName) {
  354. case 'SELECT':
  355. case 'INPUT':
  356. if (target.type === 'checkbox') {
  357. Config.set(name, target.checked);
  358. } else {
  359. const type = target.getAttribute('data-type');
  360. const value = this._parseParam(target.value, type);
  361. Config.set(name, value);
  362. }
  363. break;
  364. default:
  365. //console.info('target', e, target, name, target.checked);
  366. Config.set(name, !!target.checked);
  367. break;
  368. }
  369. };
  370.  
  371. this._elm.enabled.addEventListener('change', onChange);
  372. this._elm.needFocus.addEventListener('change', onChange);
  373. this._elm.deviceIndex.addEventListener('change', onChange);
  374.  
  375. v.querySelector('.closeButton')
  376. .addEventListener('click', this.hide.bind(this));
  377. }
  378.  
  379. _onClick(e) {
  380. super._onClick(e);
  381. }
  382.  
  383. _onMouseDown(e) {
  384. this.hide();
  385. this._onClick(e);
  386. }
  387.  
  388. show() {
  389. document.body.addEventListener('click', this._bound.onBodyClick);
  390. this._onBeforeShow();
  391.  
  392. this.setState({isOpen: true});
  393. if (this._shadow.showModal) {
  394. this._shadow.showModal();
  395. }
  396. window.setTimeout(() => {
  397. this.setState({isVisible: true});
  398. }, 100);
  399. }
  400.  
  401. hide() {
  402. document.body.removeEventListener('click', this._bound.onBodyClick);
  403. if (this._shadow.close) {
  404. this._shadow.close();
  405. }
  406. this.setState({isVisible: false});
  407. window.setTimeout(() => {
  408. this.setState({isOpen: false});
  409. }, 2100);
  410. }
  411.  
  412. toggle() {
  413. if (this._state.isOpen) {
  414. this.hide();
  415. } else {
  416. this.show();
  417. }
  418. }
  419.  
  420. _onBeforeShow() {
  421. this._elm.enabled.checked = !!Config.get('enabled');
  422. this._elm.needFocus.checked = !!Config.get('needFocus');
  423. this._elm.deviceIndex.value = Config.get('deviceIndex');
  424. }
  425. }
  426.  
  427. ConfigPanel.__shadow__ = (`
  428. <style>
  429. .ZenzaGamePadConfigPanel {
  430. display: none;
  431. position: fixed;
  432. z-index: ${CONSTANT.BASE_Z_INDEX};
  433. top: 50vh;
  434. left: 50vw;
  435. padding: 8px;
  436. border: 2px outset;
  437. box-shadow: 0 0 8px #000;
  438. background: #ccc;
  439. transform: translate(-50%, -50%);
  440. transition: opacity 0.5s;
  441. transform-origin: center bottom;
  442. animation-timing-function: steps(10);
  443. perspective-origin: center bottom;
  444. user-select: none;
  445. margin: 0;
  446. pointer-events: auto !important;
  447. }
  448. .ZenzaGamePadConfigPanel[open] {
  449. display: block;
  450. opacity: 1;
  451. }
  452.  
  453. .ZenzaGamePadConfigPanel.is-Open {
  454. display: block;
  455. opacity: 0;
  456. }
  457.  
  458. .ZenzaGamePadConfigPanel.is-Open.is-Visible {
  459. opacity: 1;
  460. }
  461.  
  462. .title {
  463. font-weight: bolder;
  464. font-size: 120%;
  465. font-family: 'arial black';
  466. margin: 0 0 8px;
  467. text-align: center;
  468. }
  469.  
  470. .closeButton {
  471. display: block;
  472. text-align: center;
  473. }
  474.  
  475. .closeButton {
  476. display: block;
  477. padding: 8px 16px;
  478. cursor: pointer;
  479. margin: auto;
  480. }
  481.  
  482. label {
  483. cursor: pointer;
  484. }
  485.  
  486. input[type="number"] {
  487. width: 50px;
  488. }
  489.  
  490. input[type="checkbox"] {
  491. transform: scale(2);
  492. margin-right: 16px;
  493. }
  494.  
  495. .ZenzaGamePadConfigPanel>div {
  496. padding: 8px;
  497. }
  498. </style>
  499. <dialog class="root ZenzaGamePadConfigPanel zen-family">
  500. <p class="title">†ZenzaGamePad†</p>
  501.  
  502. <div class="enableSelect">
  503. <label>
  504. <input type="checkbox" data-config-name="enabled" data-type="bool">
  505. ZenzaGamePadを有効にする
  506. </label>
  507. </div>
  508.  
  509. <div class="needFocusSelect">
  510. <label>
  511. <input type="checkbox" data-config-name="needFocus" data-type="bool">
  512. ウィンドウフォーカスのあるときのみ有効
  513. </label>
  514. </div>
  515.  
  516. <div class="deviceIndex">
  517. <label>
  518. デバイス番号
  519. <select class="deviceIndexSelector"
  520. data-config-name="deviceIndex" data-type="number">
  521. <option value="0">0</option>
  522. <option value="1">1</option>
  523. <option value="2">2</option>
  524. <option value="3">3</option>
  525. </select>
  526. <small>(※リロードが必要)</small>
  527. </label>
  528. </div>
  529.  
  530. <div class="closeButtonContainer">
  531. <button class="closeButton" type="button">
  532. 閉じる
  533. </button>
  534. </div>
  535.  
  536. </dialog>
  537. `).trim();
  538.  
  539.  
  540. class ToggleButton extends BaseViewComponent {
  541. constructor({parentNode}) {
  542. super({
  543. parentNode,
  544. name: 'ZenzaGamePadToggleButton',
  545. shadow: ToggleButton.__shadow__,
  546. template: '<div class="ZenzaGamePadToggleButtonContainer"></div>',
  547. css: ''
  548. });
  549.  
  550. this._state = {
  551. isEnabled: undefined
  552. };
  553.  
  554. Config.on('enabled', () => {
  555. this.refresh();
  556. });
  557. }
  558.  
  559. refresh() {
  560. this.setState({isEnabled: Config.get('enabled')});
  561. }
  562. }
  563.  
  564.  
  565.  
  566. ToggleButton.__shadow__ = `
  567. <style>
  568. .controlButton {
  569. position: relative;
  570. display: inline-block;
  571. transition: opacity 0.4s ease, margin-left 0.2s ease, margin-top 0.2s ease;
  572. box-sizing: border-box;
  573. text-align: center;
  574. cursor: pointer;
  575. color: #fff;
  576. opacity: 0.8;
  577. vertical-align: middle;
  578. }
  579. .controlButton:hover {
  580. cursor: pointer;
  581. opacity: 1;
  582. }
  583. .controlButton .controlButtonInner {
  584. filter: grayscale(100%);
  585. }
  586. .heatSyncSwitch {
  587. font-size: 16px;
  588. width: 32px;
  589. height: 32px;
  590. line-height: 30px;
  591. cursor: pointer;
  592. }
  593. .is-Enabled .controlButtonInner {
  594. color: #aef;
  595. filter: none;
  596. }
  597.  
  598. .controlButton .tooltip {
  599. display: none;
  600. pointer-events: none;
  601. position: absolute;
  602. left: 16px;
  603. top: -30px;
  604. transform: translate(-50%, 0);
  605. font-size: 12px;
  606. line-height: 16px;
  607. padding: 2px 4px;
  608. border: 1px solid !000;
  609. background: #ffc;
  610. color: #000;
  611. text-shadow: none;
  612. white-space: nowrap;
  613. z-index: 100;
  614. opacity: 0.8;
  615. }
  616.  
  617. .controlButton:hover {
  618. background: #222;
  619. }
  620.  
  621. .controlButton:hover .tooltip {
  622. display: block;
  623. opacity: 1;
  624. }
  625.  
  626. </style>
  627. <div class="heatSyncSwitch controlButton root command" data-command="toggleZenzaGamePadConfig">
  628. <div class="controlButtonInner" title="ZenzaGamePad">&#x1F3AE;</div>
  629. <div class="tooltip">ZenzaGamePad</div>
  630. </div>
  631. `.trim();
  632.  
  633.  
  634.  
  635.  
  636. const execCommand = (command, param) =>
  637. ZenzaWatch.external.execCommand(command, param);
  638.  
  639. const speedUp = () => {
  640. // TODO:
  641. // configを直接参照するのはお行儀が悪いのでexternalのインターフェースをつける
  642. const current = parseFloat(ZenzaWatch.config.getValue('playbackRate'), 10);
  643. window.console.log('speedUp', current);
  644. execCommand('playbackRate', Math.floor(Math.min(current + 0.1, 3) * 10) / 10);
  645. };
  646.  
  647. const speedDown = () => {
  648. // TODO:
  649. // configを直接参照するのはお行儀が悪いのでexternalのインターフェースをつける
  650. const current = parseFloat(ZenzaWatch.config.getValue('playbackRate'), 10);
  651. window.console.log('speedDown', current);
  652. execCommand('playbackRate', Math.floor(Math.max(current - 0.1, 0.1) * 10) / 10);
  653. };
  654.  
  655. const scrollUp = () => {
  656. document.documentElement.scrollTop =
  657. Math.max(0, document.documentElement.scrollTop - window.innerHeight / 5);
  658. };
  659.  
  660. const scrollDown = () => {
  661. document.documentElement.scrollTop =
  662. document.documentElement.scrollTop + window.innerHeight / 5;
  663. };
  664.  
  665. const scrollToVideo = () => {
  666. getVideo().scrollIntoView({behavior: 'smooth', block: 'center'});
  667. };
  668.  
  669. const swapABXY_FC30 = btn => {
  670. switch (btn) {
  671. case 0: return 1;
  672. case 1: return 0;
  673. case 3: return 4;
  674. case 4: return 3;
  675. }
  676. return btn;
  677. };
  678.  
  679.  
  680. const onButtonDown = (button, deviceId) => {
  681. if (!isZenzaWatchOpen) { return; }
  682. if (deviceId.match(/Vendor: 04b4 Product: 010a/i)) {
  683. //USB Gamepad (Vendor: 04b4 Product: 010a)"
  684. return onButtonDownSaturn(button, deviceId);
  685. } else
  686. if (deviceId.match(/Vendor: (3810|05a0|1235|1002)/i)) {
  687. // FC30なのにみんなVendor違うってどういうことだよ
  688. // 8Bitdo FC30 Pro (Vendor: 1002 Product: 9000)
  689. return onButtonDownFC30(button, deviceId);
  690. } else
  691. if (deviceId.match(/Vendor: 057e Product: 200[67]/i)) {
  692. return onButtonDownJoyCon(button, deviceId);
  693. }
  694.  
  695. switch (button) {
  696. case 0: // A
  697. isPauseButtonDown = true;
  698. execCommand('togglePlay');
  699. break;
  700. case 1: // B
  701. execCommand('toggle-mute');
  702. break;
  703. case 2: // X
  704. execCommand('toggle-showComment');
  705. break;
  706. case 3: // Y
  707. isRate1ButtonDown = true;
  708. execCommand('playbackRate', 0.1);
  709. break;
  710. case 4: // LB
  711. execCommand('playPreviousVideo');
  712. break;
  713. case 5: // RB
  714. execCommand('playNextVideo');
  715. break;
  716. case 6: // LT
  717. execCommand('playbackRate', 0.5);
  718. break;
  719. case 7: // RT
  720. execCommand('playbackRate', 3);
  721. break;
  722. case 8: // しいたけの左 ビューボタン (Back)
  723. execCommand('screenShot');
  724. break;
  725. case 9: // しいたけの右 メニューボタン (Start)
  726. execCommand('deflistAdd');
  727. break;
  728. case 10: // Lスティック
  729. execCommand('seek', 0);
  730. break;
  731. case 11: // Rスティック
  732. execCommand('toggle-fullscreen');
  733. break;
  734. case 12: // up
  735. if (isPauseButtonDown) {
  736. speedUp();
  737. } else {
  738. execCommand('volumeUp');
  739. }
  740. break;
  741. case 13: // down
  742. if (isPauseButtonDown) {
  743. speedDown();
  744. } else {
  745. execCommand('volumeDown');
  746. }
  747. break;
  748. case 14: // left
  749. if (isPauseButtonDown) {
  750. execCommand('seekPrevFrame');
  751. } else {
  752. execCommand('seekBy', isRate1ButtonDown ? -1 : -5);
  753. }
  754. break;
  755. case 15: // right
  756. if (isPauseButtonDown) {
  757. execCommand('seekNextFrame');
  758. } else {
  759. execCommand('seekBy', isRate1ButtonDown ? +1 : +5);
  760. }
  761. break;
  762. }
  763. };
  764.  
  765. const onButtonDownSaturn = (button, deviceId) => {
  766. switch (button) {
  767. case 0: // A
  768. isPauseButtonDown = true;
  769. execCommand('togglePlay');
  770. break;
  771. case 1: // B
  772. execCommand('toggle-mute');
  773. break;
  774. case 2: // C
  775. execCommand('toggle-showComment');
  776. break;
  777. case 3: // X
  778. execCommand('playbackRate', 0.5);
  779. break;
  780. case 4: // Y
  781. isRate1ButtonDown = true;
  782. execCommand('playbackRate', 0.1);
  783. break;
  784. case 5: // Z
  785. execCommand('playbackRate', 3);
  786. break;
  787. case 6: // L
  788. execCommand('playPreviousVideo');
  789. break;
  790. case 7: // R
  791. execCommand('playNextVideo');
  792. break;
  793. case 8: // START
  794. execCommand('deflistAdd');
  795. break;
  796. }
  797. };
  798.  
  799. const onButtonDownJoyCon = (button, deviceId) => {
  800. const ButtonMap = deviceId.match(/Vendor: 057e Product: 2006/i) ?
  801. ButtonMapJoyConL : ButtonMapJoyConR;
  802. switch (button) {
  803. case ButtonMap.Y:
  804. if (isPauseButtonDown) {
  805. execCommand('seekPrevFrame');
  806. } else {
  807. execCommand('toggle-showComment');
  808. }
  809. break;
  810. case ButtonMap.B:
  811. isPauseButtonDown = true;
  812. execCommand('togglePlay');
  813. break;
  814. case ButtonMap.X:
  815. if (isMetaButtonDown) {
  816. execCommand('playbackRate', 2);
  817. } else {
  818. isRate1ButtonDown = true;
  819. execCommand('playbackRate', 0.1);
  820. }
  821. break;
  822. case ButtonMap.A:
  823. if (isPauseButtonDown) {
  824. execCommand('seekNextFrame');
  825. } else {
  826. execCommand('toggle-mute');
  827. }
  828. break;
  829. case ButtonMap.SUP:
  830. if (isMetaButtonDown) {
  831. scrollUp();
  832. } else {
  833. execCommand('playPreviousVideo');
  834. }
  835. break;
  836. case ButtonMap.SDN:
  837. if (isMetaButtonDown) {
  838. scrollDown();
  839. } else {
  840. execCommand('playNextVideo');
  841. }
  842. break;
  843. case ButtonMap.SEL:
  844. if (isMetaButtonDown) {
  845. execCommand('toggle-loop');
  846. } else {
  847. execCommand('deflistAdd');
  848. }
  849. break;
  850. case ButtonMap.CAP:
  851. execCommand('screenShot');
  852. break;
  853. case ButtonMap.PUSH:
  854. if (isMetaButtonDown) {
  855. scrollToVideo();
  856. } else {
  857. execCommand('seek', 0);
  858. }
  859. break;
  860. case ButtonMap.LR:
  861. execCommand('toggle-fullscreen');
  862. break;
  863. case ButtonMap.META:
  864. isMetaButtonDown = true;
  865. break;
  866. }
  867. };
  868.  
  869.  
  870. const onButtonDownFC30 = (button, deviceId) => {
  871. if (deviceId.match(/Product: (3232)/i)) { // FC30 Zero / FC30
  872. button = swapABXY_FC30(button);
  873. }
  874.  
  875. switch (button) {
  876. case 0: // B
  877. execCommand('toggle-mute');
  878. break;
  879. case 1: // A
  880. isPauseButtonDown = true;
  881. execCommand('togglePlay');
  882. break;
  883. case 2: // ???
  884. //execCommand('toggle-showComment');
  885. break;
  886. case 3: // X
  887. isRate1ButtonDown = true;
  888. execCommand('playbackRate', 0.1);
  889. break;
  890. case 4: // Y
  891. execCommand('toggle-showComment');
  892. break;
  893. case 5: //
  894. break;
  895. case 6: // L1
  896. if (isPauseButtonDown) {
  897. execCommand('playPreviousVideo');
  898. } else {
  899. execCommand('playbackRate', 0.5);
  900. }
  901. break;
  902. case 7: // R1
  903. if (isPauseButtonDown) {
  904. execCommand('playNextVideo');
  905. } else {
  906. execCommand('playbackRate', 3);
  907. }
  908. break;
  909. case 8: // L2
  910. execCommand('playPreviousVideo');
  911. break;
  912. case 9: // R2
  913. execCommand('playNextVideo');
  914. break;
  915. case 10: // SELECT
  916. execCommand('screenShot');
  917. break;
  918. case 11: // START
  919. execCommand('deflistAdd');
  920. break;
  921. case 13: // Lスティック
  922. execCommand('seek', 0);
  923. break;
  924. case 14: // Rスティック
  925. break;
  926.  
  927. }
  928. };
  929.  
  930.  
  931. const onButtonUp = (button, deviceId) => {
  932. if (!isZenzaWatchOpen) { return; }
  933. if (deviceId.match(/Vendor: 04b4 Product: 010a/i)) {
  934. //USB Gamepad (Vendor: 04b4 Product: 010a)"
  935. return onButtonUpSaturn(button, deviceId);
  936. } else
  937. if (deviceId.match(/Vendor: (3810|05a0|1235|1002)/i)) {
  938. // 8Bitdo FC30 Pro (Vendor: 1002 Product: 9000)
  939. return onButtonUpFC30(button, deviceId);
  940. } else
  941. if (deviceId.match(/Vendor: 057e Product: 200[67]/i)) {
  942. return onButtonUpJoyCon(button, deviceId);
  943. }
  944. switch (button) {
  945. case 0: // A
  946. isPauseButtonDown = false;
  947. break;
  948. case 3: // Y
  949. isRate1ButtonDown = false;
  950. execCommand('playbackRate', 1.0);
  951. break;
  952. case 7: // RT
  953. execCommand('playbackRate', 1.5);
  954. break;
  955. }
  956. };
  957.  
  958. const onButtonUpSaturn = (button, deviceId) => {
  959. switch (button) {
  960. case 0: // A
  961. isPauseButtonDown = false;
  962. break;
  963. case 1: // B
  964. break;
  965. case 2: // C
  966. break;
  967. case 3: // X
  968. break;
  969. case 4: // Y
  970. isRate1ButtonDown = false;
  971. execCommand('playbackRate', 1.0);
  972. break;
  973. case 5: // Z
  974. execCommand('playbackRate', 1.5);
  975. break;
  976. case 6: // L
  977. break;
  978. case 7: // R
  979. break;
  980. case 8: // START
  981. break;
  982. }
  983. };
  984.  
  985. const onButtonUpFC30 = (button, deviceId) => {
  986. if (deviceId.match(/Product: (3232)/i)) { // FC30Zero / FC30
  987. button = swapABXY_FC30(button);
  988. }
  989.  
  990. switch (button) {
  991. case 0: // B
  992. break;
  993. case 1: // A
  994. isPauseButtonDown = false;
  995. break;
  996. case 2: // ???
  997. break;
  998. case 3: // X
  999. isRate1ButtonDown = false;
  1000. execCommand('playbackRate', 1.0);
  1001. break;
  1002. case 4: // Y
  1003. break;
  1004. case 5: //
  1005. break;
  1006. case 6: // L1
  1007. break;
  1008. case 7: // R1
  1009. if (isPauseButtonDown) { return; }
  1010. execCommand('playbackRate', 1.5);
  1011. break;
  1012. case 8: // L2
  1013. break;
  1014. case 9: // R2
  1015. break;
  1016. case 10: // SELECT
  1017. break;
  1018. case 11: // START
  1019. break;
  1020. case 13: // Lスティック
  1021. break;
  1022. case 14: // Rスティック
  1023. break;
  1024. }
  1025. };
  1026.  
  1027. const onButtonUpJoyCon = (button, deviceId) => {
  1028. const ButtonMap = deviceId.match(/Vendor: 057e Product: 2006/i) ?
  1029. ButtonMapJoyConL : ButtonMapJoyConR;
  1030. switch (button) {
  1031. case ButtonMap.Y:
  1032. break;
  1033. case ButtonMap.B:
  1034. isPauseButtonDown = false;
  1035. break;
  1036. case ButtonMap.X:
  1037. isRate1ButtonDown = false;
  1038. execCommand('playbackRate', 1);
  1039. break;
  1040. case ButtonMap.META:
  1041. isMetaButtonDown = false;
  1042. break;
  1043. }
  1044. };
  1045.  
  1046. const onButtonRepeat = (button, deviceId) => {
  1047. if (!isZenzaWatchOpen) { return; }
  1048.  
  1049. if (deviceId.match(/Vendor: 057e Product: 200[67]/i)) { // Joy-Con
  1050. return onButtonRepeatJoyCon(button, deviceId);
  1051. }
  1052.  
  1053. switch (button) {
  1054. case 12: // up
  1055. if (isPauseButtonDown) {
  1056. speedUp();
  1057. } else {
  1058. execCommand('volumeUp');
  1059. }
  1060. break;
  1061. case 13: // down
  1062. if (isPauseButtonDown) {
  1063. speedUp();
  1064. } else {
  1065. execCommand('volumeDown');
  1066. }
  1067. break;
  1068. case 14: // left
  1069. if (isPauseButtonDown) {
  1070. execCommand('seekPrevFrame');
  1071. } else {
  1072. execCommand('seekBy', isRate1ButtonDown ? -1 : -5);
  1073. }
  1074. break;
  1075. case 15: // right
  1076. if (isPauseButtonDown) {
  1077. execCommand('seekNextFrame');
  1078. } else {
  1079. execCommand('seekBy', isRate1ButtonDown ? +1 : +5);
  1080. }
  1081. break;
  1082. }
  1083. };
  1084.  
  1085. const onButtonRepeatJoyCon = (button, deviceId) => {
  1086. const ButtonMap = deviceId.match(/Vendor: 057e Product: 2006/i) ?
  1087. ButtonMapJoyConL : ButtonMapJoyConR;
  1088. switch (button) {
  1089. case ButtonMap.Y:
  1090. if (isMetaButtonDown) {
  1091. execCommand('seekBy', -15);
  1092. } else if (isPauseButtonDown) {
  1093. execCommand('seekPrevFrame');
  1094. }
  1095. break;
  1096.  
  1097. case ButtonMap.A:
  1098. if (isMetaButtonDown) {
  1099. execCommand('seekBy', 15);
  1100. } else if (isPauseButtonDown) {
  1101. execCommand('seekNextFrame');
  1102. }
  1103. break;
  1104. case ButtonMap.SUP:
  1105. if (isMetaButtonDown) {
  1106. scrollUp();
  1107. } else {
  1108. execCommand('playPreviousVideo');
  1109. }
  1110. break;
  1111. case ButtonMap.SDN:
  1112. if (isMetaButtonDown) {
  1113. scrollDown();
  1114. } else {
  1115. execCommand('playNextVideo');
  1116. }
  1117. break;
  1118. }
  1119. };
  1120.  
  1121. const onAxisChange = (axis, value, deviceId) => {
  1122. if (!isZenzaWatchOpen) { return; }
  1123. if (Math.abs(value) < 0.1) { return; }
  1124.  
  1125. const isFC30 = deviceId.match(/Vendor: 3810/i) ? true : false;
  1126. if (isFC30) {
  1127. switch (axis) {
  1128. case 3: // L2なぜか反応する
  1129. case 4: // R2なぜか反応する
  1130. return;
  1131. case 5: // FC30のRスティック上下?
  1132. axis = 3;
  1133. break;
  1134. }
  1135. }
  1136.  
  1137. if (deviceId.match(/Vendor: 057e Product: 200[67]/i)) { // Joy-Con
  1138. return;
  1139. }
  1140.  
  1141. switch (axis) {
  1142. case 0: {// Lスティック X
  1143. const step = isRate1ButtonDown ? 1 : 5;
  1144. execCommand('seekBy', (value < 0 ? -1 : 1) * step);
  1145. }
  1146. break;
  1147. case 1: // Lスティック Y
  1148. if (isPauseButtonDown) {
  1149. if (value < 0) { speedUp(); }
  1150. else { speedDown(); }
  1151. } else {
  1152. execCommand(value < 0 ? 'volumeUp' : 'volumeDown');
  1153. }
  1154. break;
  1155. case 2: // Rスティック X
  1156. break;
  1157. case 3: // Rスティック Y
  1158. if (value < 0) { speedUp(); }
  1159. else { speedDown(); }
  1160. break;
  1161. }
  1162. };
  1163.  
  1164. const onAxisRepeat = (axis, value, deviceId) => {
  1165. if (!isZenzaWatchOpen) { return; }
  1166. if (Math.abs(value) < 0.1) { return; }
  1167.  
  1168. if (deviceId.match(/Vendor: 057e Product: 2006/i)) { // Joy-Con (L)
  1169. return;
  1170. } else
  1171. if (deviceId.match(/Vendor: 057e Product: 2007/i)) { // Joy-Con (R)
  1172. return;
  1173. }
  1174.  
  1175. switch (axis) {
  1176. case 0: {// Lスティック X
  1177. const step = isRate1ButtonDown ? 1 : +5;
  1178. execCommand('seekBy', (value < 0 ? -1 : 1) * step);
  1179. }
  1180. break;
  1181. case 1: // Lスティック Y
  1182. if (isPauseButtonDown) {
  1183. if (value < 0) { speedUp(); }
  1184. else { speedDown(); }
  1185. } else {
  1186. execCommand(value < 0 ? 'volumeUp' : 'volumeDown');
  1187. }
  1188. break;
  1189. case 2: // Rスティック X
  1190. break;
  1191. case 3: // Rスティック Y
  1192. if (value < 0) { speedUp(); }
  1193. else { speedDown(); }
  1194. break;
  1195. }
  1196. };
  1197.  
  1198. const onPovChange = (pov, deviceId) => {
  1199. switch(pov) {
  1200. case 'UP':
  1201. if (isMetaButtonDown) {
  1202. speedUp();
  1203. } else {
  1204. execCommand('volumeUp');
  1205. }
  1206. break;
  1207. case 'DOWN':
  1208. if (isMetaButtonDown) {
  1209. speedDown();
  1210. } else {
  1211. execCommand('volumeDown');
  1212. }
  1213. break;
  1214. case 'LEFT':
  1215. execCommand('seekBy', isRate1ButtonDown || isMetaButtonDown ? -1 : -5);
  1216. break;
  1217. case 'RIGHT':
  1218. execCommand('seekBy', isRate1ButtonDown || isMetaButtonDown ? +1 : +5);
  1219. break;
  1220. }
  1221. };
  1222.  
  1223. const onPovRepeat = onPovChange;
  1224.  
  1225. class PollingTimer {
  1226. constructor(callback, interval) {
  1227. this._timer = null;
  1228. this._callback = callback;
  1229. if (typeof interval === 'number') {
  1230. this.changeInterval(interval);
  1231. }
  1232. }
  1233. changeInterval(interval) {
  1234. if (this._timer) {
  1235. if (this._currentInterval === interval) {
  1236. return;
  1237. }
  1238. window.clearInterval(this._timer);
  1239. }
  1240. console.log('%cupdate Interval:%s', 'background: lightblue;', interval);
  1241. this._currentInterval = interval;
  1242. this._timer = window.setInterval(this._callback, interval);
  1243. }
  1244. pause() {
  1245. window.clearInterval(this._timer);
  1246. this._timer = null;
  1247. }
  1248. start() {
  1249. if (typeof this._currentInterval !== 'number') {
  1250. return;
  1251. }
  1252. this.changeInterval(this._currentInterval);
  1253. }
  1254. }
  1255.  
  1256. const GamePadModel = ((Emitter) => {
  1257. class GamePad extends Emitter {
  1258. constructor(gamepadStatus) {
  1259. super();
  1260. this._gamepadStatus = gamepadStatus;
  1261. this._buttons = [];
  1262. this._axes = [];
  1263. this._pov = '';
  1264. this._lastTimestamp = 0;
  1265. this._povRepeat = 0;
  1266. this.initialize(gamepadStatus);
  1267. }
  1268.  
  1269. get isConnected() {
  1270. return this._gamepadStatus.connected ? true : false;
  1271. }
  1272.  
  1273. get deviceId() {
  1274. return this._id;
  1275. }
  1276.  
  1277. get deviceIndex() {
  1278. return this._index;
  1279. }
  1280.  
  1281. get buttonCount() {
  1282. return this._buttons ? this._buttons.length : 0;
  1283. }
  1284.  
  1285. get axisCount() {
  1286. return this._axes ? this._axes.length : 0;
  1287. }
  1288.  
  1289. get pov() {
  1290. return this._pov;
  1291. }
  1292.  
  1293. get x() {
  1294. return this._axes.length > 0 ? this._axes[0] : 0;
  1295. }
  1296.  
  1297. get y() {
  1298. return this._axes.length > 1 ? this._axes[1] : 0;
  1299. }
  1300.  
  1301. get z() {
  1302. return this._axes.length > 2 ? this._axes[2] : 0;
  1303. }
  1304.  
  1305. initialize(gamepadStatus) {
  1306. this._buttons.length = gamepadStatus.buttons.length;
  1307. this._axes.length = gamepadStatus.axes.length;
  1308. this._id = gamepadStatus.id;
  1309. this._index = gamepadStatus.index;
  1310. this._isRepeating = false;
  1311. this.reset();
  1312. }
  1313. reset() {
  1314. this._pov = '';
  1315. this._povRepeat = 0;
  1316.  
  1317. for (let i = 0, len = this._gamepadStatus.buttons.length + 16; i < len; i++) {
  1318. this._buttons[i] = {pressed: false, repeat: 0};
  1319. }
  1320. for (let i = 0, len = this._gamepadStatus.axes.length + 16; i < len; i++) {
  1321. this._axes[i] = {value: null, repeat: 0};
  1322. }
  1323. }
  1324. update() {
  1325. const gamepadStatus = (navigator.getGamepads())[this._index];
  1326. // gp || this._gamepadStatus;
  1327. if (!gamepadStatus) { console.log('no status'); return; }
  1328. if (!this._isRepeating && this._lastTimestamp === gamepadStatus.timestamp) {
  1329. return;
  1330. }
  1331. this._gamepadStatus = gamepadStatus;
  1332. this._lastTimestamp = gamepadStatus.timestamp;
  1333.  
  1334. const buttons = gamepadStatus.buttons, axes = gamepadStatus.axes;
  1335. let isRepeating = false;
  1336.  
  1337. for (let i = 0, len = Math.min(this._buttons.length, buttons.length); i < len; i++) {
  1338. const buttonStatus = buttons[i].pressed ? 1 : 0;
  1339.  
  1340. if (this._buttons[i].pressed !== buttonStatus) {
  1341. const eventName = (buttonStatus === 1) ? 'onButtonDown' : 'onButtonUp';
  1342. //console.log('%cbutton%s:%s', 'background: lightblue;', i, buttonStatus, 0);
  1343. this.emit(eventName, i, 0);
  1344. this.emit('onButtonStatusChange', i, buttonStatus);
  1345. }
  1346. this._buttons[i].pressed = buttonStatus;
  1347. if (buttonStatus) {
  1348. this._buttons[i].repeat++;
  1349. isRepeating = true;
  1350. if (this._buttons[i].repeat % 5 === 0) {
  1351. //console.log('%cbuttonRepeat%s', 'background: lightblue;', i);
  1352. this.emit('onButtonRepeat', i);
  1353. }
  1354. } else {
  1355. this._buttons[i].repeat = 0;
  1356. }
  1357. }
  1358. for (let i = 0, len = Math.min(8, this._axes.length); i < len; i++) {
  1359. const axis = Math.round(axes[i] * 1000) / 1000;
  1360.  
  1361. if (this._axes[i].value === null) {
  1362. this._axes[i].value = axis;
  1363. continue;
  1364. }
  1365.  
  1366. const diff = Math.round(Math.abs(axis - this._axes[i].value));
  1367. if (diff >= 1) {
  1368. this.emit('onAxisChange', i, axis);
  1369. }
  1370. if (Math.abs(axis) <= 0.1 && this._axes[i].repeat > 0) {
  1371. this._axes[i].repeat = 0;
  1372. } else if (Math.abs(axis) > 0.1) {
  1373. this._axes[i].repeat++;
  1374. isRepeating = true;
  1375. } else {
  1376. this._axes[i].repeat = 0;
  1377. }
  1378. this._axes[i].value = axis;
  1379.  
  1380. }
  1381.  
  1382. if (typeof axes[9] !== 'number') {
  1383. this._isRepeating = isRepeating;
  1384. return;
  1385. }
  1386. let pov = '';
  1387. if (this._id.match(/Vendor: 057e Product: 200[67]/i)) {
  1388. const b = 100000;
  1389. const axis = Math.trunc(axes[9] * b);
  1390. const margin = b / 10;
  1391. const AxisMap = this._id.match(/Vendor: 057e Product: 2006/i) ? AxisMapJoyConL : AxisMapJoyConR;
  1392. if (Math.abs(JoyConAxisCenter * b - axis) <= margin) {
  1393. pov = '';
  1394. } else {
  1395. Object.keys(AxisMap).forEach(key => {
  1396. if (Math.abs(AxisMap[key] * b - axis) <= margin) {
  1397. pov = key;
  1398. }
  1399. });
  1400. }
  1401.  
  1402. } else if (this._id.match(/Vendor: (3810|05a0|1235|1002)/i)) {
  1403. const p = Math.round(axes[9] * 1000);
  1404. if (p < -500) { pov = 'UP';
  1405. } else if (p < 0) { pov = 'RIGHT';
  1406. } else if (p > 3000) { pov = '';
  1407. } else if (p > 500){ pov = 'LEFT';
  1408. } else { pov = 'DOWN'; }
  1409. }
  1410. if (this._pov !== pov) {
  1411. this._pov = pov;
  1412. this._povRepeat = 0;
  1413. isRepeating = pov !== '';
  1414. this.emit('onPovChange', this._pov);
  1415. } else if (pov !== '') {
  1416. this._povRepeat++;
  1417. isRepeating = true;
  1418. if (this._povRepeat % 5 === 0) {
  1419. this.emit('onPovRepeat', this._pov);
  1420. }
  1421. }
  1422. this._isRepeating = isRepeating;
  1423. //console.log(JSON.stringify(this.dump()));
  1424. }
  1425. dump() {
  1426. const gamepadStatus = this._gamepadStatus, buttons = gamepadStatus.buttons, axes = gamepadStatus.axes;
  1427. const btmp = [], atmp = [];
  1428. for (let i = 0, len = axes.length; i < len; i++) {
  1429. atmp.push('ax' + i + ': ' + axes[i]);
  1430. }
  1431. for (let i = 0, len = buttons.length; i < len; i++) {
  1432. btmp.push('bt' + i + ': ' + (buttons[i].pressed ? 1 : 0));
  1433. }
  1434. return atmp.join('\n') + '\n' + btmp.join(', ');
  1435. }
  1436. getX() {
  1437. return this._axes.length > 0 ? this._axes[0] : 0;
  1438. }
  1439. getY() {
  1440. return this._axes.length > 1 ? this._axes[1] : 0;
  1441. }
  1442. getZ() {
  1443. return this._axes.length > 2 ? this._axes[2] : 0;
  1444. }
  1445. getButtonCount() {
  1446. return this._buttons ? this._buttons.length : 0;
  1447. }
  1448. getButtonStatus(index) {
  1449. return this._buttons[index] || 0;
  1450. }
  1451. getAxisCount() {
  1452. return this._axes ? this._axes.length : 0;
  1453. }
  1454. getAxisValue(index) {
  1455. return this._axes[index] || 0;
  1456. }
  1457. getDeviceIndex() {
  1458. return this._index;
  1459. }
  1460. getDeviceId() {
  1461. return this._id;
  1462. }
  1463. getPov() {
  1464. return this._pov;
  1465. }
  1466. release() {
  1467. // TODO: clear events
  1468. }
  1469. }
  1470.  
  1471. return GamePad;
  1472. })(Emitter);
  1473.  
  1474. const ZenzaGamePad = (($, PollingTimer, GamePadModel) => {
  1475. let activeGamepad = null;
  1476. let pollingTimer = null;
  1477. let ZenzaGamePad = ZenzaWatch.modules ?
  1478. new ZenzaWatch.modules.Emitter() : new Emitter();
  1479.  
  1480. const padIndex = Config.get('deviceIndex') * 1;
  1481.  
  1482. const detectGamepad = () => {
  1483. if (activeGamepad) {
  1484. return;
  1485. }
  1486. const gamepads = navigator.getGamepads();
  1487. if (gamepads.length < 1) {
  1488. return;
  1489. }
  1490. const pad = Array.from(gamepads).find(pad => {
  1491. return pad &&
  1492. pad.connected &&
  1493. pad.id &&
  1494. pad.index === padIndex &&
  1495. // windowsにDualShock4を繋ぐとあらわれる謎のデバイス
  1496. !pad.id.match(/Vendor: 00ff/i);
  1497. });
  1498. if (!pad) { return; }
  1499. window.console.log(
  1500. '%cdetect gamepad index: %s, id: "%s"',
  1501. 'background: lightgreen; font-weight: bolder;',
  1502. pad.index, pad.id
  1503. );
  1504. const gamepad = new GamePadModel(pad);
  1505. activeGamepad = gamepad;
  1506.  
  1507. const self = ZenzaGamePad;
  1508. const onButtonDown = number => {
  1509. self.emit('onButtonDown', number, gamepad.deviceIndex);
  1510. };
  1511. const onButtonRepeat = number => {
  1512. self.emit('onButtonRepeat', number, gamepad.deviceIndex);
  1513. };
  1514. const onButtonUp = number => {
  1515. self.emit('onButtonUp', number, gamepad.deviceIndex);
  1516. };
  1517. const onAxisChange = (number, value) => {
  1518. self.emit('onAxisChange', number, value, gamepad.deviceIndex);
  1519. };
  1520. const onAxisRepeat = (number, value) => {
  1521. self.emit('onAxisRepeat', number, value, gamepad.deviceIndex);
  1522. };
  1523. const onAxisRelease = number => {
  1524. self.emit('onAxisRelease', number, gamepad.deviceIndex);
  1525. };
  1526. const onPovChange = pov => {
  1527. self.emit('onPovChange', pov, gamepad.deviceIndex);
  1528. };
  1529. const onPovRepeat = pov => {
  1530. self.emit('onPovRepeat', pov, gamepad.deviceIndex);
  1531. };
  1532.  
  1533.  
  1534. gamepad.on('onButtonDown', onButtonDown);
  1535. gamepad.on('onButtonRepeat', onButtonRepeat);
  1536. gamepad.on('onButtonUp', onButtonUp);
  1537. gamepad.on('onAxisChange', onAxisChange);
  1538. gamepad.on('onAxisRepeat', onAxisRepeat);
  1539. gamepad.on('onAxisRelease', onAxisRelease);
  1540. gamepad.on('onPovChange', onPovChange);
  1541. gamepad.on('onPovRepeat', onPovRepeat);
  1542.  
  1543. self.emit('onDeviceConnect', gamepad.getDeviceIndex(), gamepad.getDeviceId());
  1544.  
  1545. pollingTimer.changeInterval(30);
  1546. };
  1547.  
  1548.  
  1549. const onGamepadConnectStatusChange = (e, isConnected) => {
  1550. const padIndex = Config.get('deviceIndex') * 1;
  1551. console.log('onGamepadConnetcStatusChange', e, e.gamepad.index, isConnected);
  1552. if (e.gamepad.index !== padIndex) {
  1553. return;
  1554. }
  1555.  
  1556. if (isConnected) {
  1557. console.log('%cgamepad connected id:"%s"', 'background: lightblue;', e.gamepad.id);
  1558. detectGamepad();
  1559. } else {
  1560. ZenzaGamePad.emit('onDeviceDisconnect', activeGamepad.getDeviceIndex());
  1561. if (activeGamepad) {
  1562. activeGamepad.release();
  1563. }
  1564. activeGamepad = null;
  1565. console.log('%cgamepad disconneced id:"%s"', 'background: lightblue;', e.gamepad.id);
  1566. }
  1567. };
  1568.  
  1569. const initializeTimer = () => {
  1570. console.log('%cinitializeGamepadTimer', 'background: lightgreen;');
  1571. const onTimerInterval = () => {
  1572. if (!Config.get('enabled')) {
  1573. return;
  1574. }
  1575. if (Config.get('needFocus') && !document.hasFocus()) {
  1576. return;
  1577. }
  1578. if (!activeGamepad) {
  1579. detectGamepad();
  1580. return;
  1581. }
  1582. if (!activeGamepad.isConnected) {
  1583. return;
  1584. }
  1585. activeGamepad.update();
  1586. };
  1587. pollingTimer = new PollingTimer(onTimerInterval, 1000);
  1588. };
  1589.  
  1590. const initializeGamepadConnectEvent = () => {
  1591. console.log('%cinitializeGamepadConnectEvent', 'background: lightgreen;');
  1592.  
  1593. window.addEventListener('gamepadconnected',
  1594. e => onGamepadConnectStatusChange(e, true));
  1595. window.addEventListener('gamepaddisconnected',
  1596. e => onGamepadConnectStatusChange(e, false));
  1597.  
  1598. if (activeGamepad) {
  1599. return;
  1600. }
  1601. window.setTimeout(detectGamepad, 1000);
  1602. };
  1603.  
  1604. ZenzaGamePad.startDetect = () => {
  1605. ZenzaGamePad.startDetect = _.noop;
  1606. initializeTimer();
  1607. initializeGamepadConnectEvent();
  1608. };
  1609.  
  1610. ZenzaGamePad.startPolling = () => {
  1611. if (pollingTimer) { pollingTimer.start(); }
  1612. };
  1613. ZenzaGamePad.stopPolling = () => {
  1614. if (pollingTimer) { pollingTimer.pause(); }
  1615. };
  1616.  
  1617. return ZenzaGamePad;
  1618. })($, PollingTimer, GamePadModel);
  1619.  
  1620. let hasInitGamePad = false;
  1621. const initGamePad = () => {
  1622. if (hasInitGamePad) { return; }
  1623. hasInitGamePad = true;
  1624.  
  1625. let isActivated = false;
  1626. let isDetected = false;
  1627. let deviceId, deviceIndex;
  1628. const notifyDetect = () => {
  1629. if (!document.hasFocus() || isDetected) { return; }
  1630. isActivated = true;
  1631. isDetected = true;
  1632.  
  1633. // 初めてボタンかキーが押されたタイミングで通知する
  1634. execCommand(
  1635. 'notify',
  1636. 'ゲームパッド "' + deviceId + '" が検出されました'
  1637. );
  1638. };
  1639.  
  1640. const _onButtonDown = number => {
  1641. notifyDetect();
  1642. if (!isActivated) { return; }
  1643. onButtonDown(number, deviceId);
  1644. };
  1645. const _onButtonRepeat = number => {
  1646. if (!isActivated) { return; }
  1647. onButtonRepeat(number, deviceId);
  1648. };
  1649. const _onButtonUp = number => {
  1650. if (!isActivated) { return; }
  1651. onButtonUp(number, deviceId);
  1652. };
  1653. const _onAxisChange = (number, value) => {
  1654. notifyDetect();
  1655. if (!isActivated) { return; }
  1656. onAxisChange(number, value, deviceId);
  1657. };
  1658. const _onAxisRepeat = (number, value) => {
  1659. if (!isActivated) { return; }
  1660. onAxisRepeat(number, value, deviceId);
  1661. };
  1662. const _onAxisRelease = () => {
  1663. if (!isActivated) { return; }
  1664. };
  1665.  
  1666. const _onPovChange = pov => {
  1667. notifyDetect();
  1668. if (!isActivated) { return; }
  1669. onPovChange(pov, deviceId);
  1670. };
  1671.  
  1672. const _onPovRepeat = pov => {
  1673. if (!isActivated) { return; }
  1674. onPovRepeat(pov, deviceId);
  1675. };
  1676.  
  1677. let hasBound = false;
  1678. const bindEvents = () => {
  1679. if (hasBound) { return; }
  1680. hasBound = true;
  1681.  
  1682. ZenzaGamePad.on('onButtonDown', _onButtonDown);
  1683. ZenzaGamePad.on('onButtonRepeat', _onButtonRepeat);
  1684. ZenzaGamePad.on('onButtonUp', _onButtonUp);
  1685. ZenzaGamePad.on('onAxisChange', _onAxisChange);
  1686. ZenzaGamePad.on('onAxisRepeat', _onAxisRepeat);
  1687. ZenzaGamePad.on('onAxisRelease', _onAxisRelease);
  1688. ZenzaGamePad.on('onPovChange', _onPovChange);
  1689. ZenzaGamePad.on('onPovRepeat', _onPovRepeat);
  1690. };
  1691.  
  1692. const onDeviceConnect = (index, id) => {
  1693. deviceIndex = index;
  1694. deviceId = id;
  1695.  
  1696. bindEvents();
  1697. };
  1698.  
  1699. ZenzaGamePad.on('onDeviceConnect', onDeviceConnect);
  1700. //ZenzaGamePad.on('onDeviceDisConnect', onDeviceDisConnect);
  1701. ZenzaGamePad.startDetect();
  1702. window.ZenzaWatch.ZenzaGamePad = ZenzaGamePad;
  1703. };
  1704.  
  1705. const onZenzaWatchOpen = () => {
  1706. isZenzaWatchOpen = true;
  1707. initGamePad();
  1708. ZenzaGamePad.startPolling();
  1709. };
  1710.  
  1711. const onZenzaWatchClose = () => {
  1712. isZenzaWatchOpen = false;
  1713. ZenzaGamePad.stopPolling();
  1714. };
  1715.  
  1716.  
  1717. const initialize = async () => {
  1718. ZenzaWatch.emitter.on('DialogPlayerOpen', onZenzaWatchOpen);
  1719. ZenzaWatch.emitter.on('DialogPlayerClose', onZenzaWatchClose);
  1720.  
  1721. const initButton = (container, handler) => {
  1722. ZenzaGamePad.configPanel = new ConfigPanel({parentNode: document.querySelector('#zenzaVideoPlayerDialog')});
  1723. const toggleButton = new ToggleButton({parentNode: container});
  1724. toggleButton.on('command', handler);
  1725. toggleButton.refresh();
  1726. };
  1727. if (ZenzaWatch.emitter.promise) {
  1728. const {container, handler} = await ZenzaWatch.emitter.promise('videoControBar.addonMenuReady');
  1729. initButton(container, handler);
  1730. } else {
  1731. ZenzaWatch.emitter.on('videoControBar.addonMenuReady', initButton);
  1732. }
  1733. ZenzaWatch.emitter.on('command-toggleZenzaGamePadConfig', () => {
  1734. ZenzaGamePad.configPanel.toggle();
  1735. });
  1736.  
  1737. };
  1738.  
  1739. initialize();
  1740. };
  1741.  
  1742. const loadMonkey = () => {
  1743. const script = document.createElement('script');
  1744. script.id = 'ZenzaGamePadLoader';
  1745. script.setAttribute('type', 'text/javascript');
  1746. script.setAttribute('charset', 'UTF-8');
  1747. script.append(`(${monkey})(window.ZenzaWatch);`);
  1748. document.head.append(script);
  1749. };
  1750.  
  1751. const ZenzaDetector = (() => {
  1752. const promise =
  1753. (window.ZenzaWatch && window.ZenzaWatch.ready) ?
  1754. Promise.resolve(window.ZenzaWatch) :
  1755. new Promise(resolve => {
  1756. [window, (document.body || document.documentElement)]
  1757. .forEach(e => e.addEventListener('ZenzaWatchInitialize', () => {
  1758. resolve(window.ZenzaWatch);
  1759. }));
  1760. });
  1761. return {detect: () => promise};
  1762. })();
  1763. await ZenzaDetector.detect();
  1764. loadMonkey();
  1765.  
  1766. })(globalThis ? globalThis.window : window);