您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ZenzaWatchをゲームパッドで操作
当前为
// ==UserScript== // @name ZenzaGamePad // @namespace https://github.com/segabito/ // @description ZenzaWatchをゲームパッドで操作 // @include *://*.nicovideo.jp/* // @version 1.5.0 // @author segabito macmoto // @license public domain // @grant none // @noframes // ==/UserScript== // 推奨 // // XInput系 (XboxOne, Xbox360コントローラ等) // DualShock4 // USBサターンパッド // 8bitdo FC30系 // Joy-Con L R (function() { var monkey = function(ZenzaWatch) { if (!window.navigator.getGamepads) { window.console.log('%cGamepad APIがサポートされていません', 'background: red; color: yellow;'); return; } const PRODUCT = 'ZenzaGamePad'; const CONSTANT = { BASE_Z_INDEX: 150000 }; let _ = window._ || ZenzaWatch.lib._; let $ = window.jQuery || ZenzaWatch.lib.$; let util = ZenzaWatch.util; let Emitter = ZenzaWatch.modules ? ZenzaWatch.modules.Emitter : ZenzaWatch.lib.AsyncEmitter; let isZenzaWatchOpen = false; let console; let debugMode = !true; let dummyConsole = { log: _.noop, error: _.noop, time: _.noop, timeEnd: _.noop, trace: _.noop }; console = debugMode ? window.console : dummyConsole; let isPauseButtonDown = false; let isRate1ButtonDown = false; let isMetaButtonDown = false; const Config = (() => { const prefix = PRODUCT + '_config_'; const emitter = new Emitter(); const defaultConfig = { debug: false, enabled: true, needFocus: false, deviceIndex: 0 }; const config = {}; emitter.refresh = (emitChange = false) => { Object.keys(defaultConfig).forEach(key => { var storageKey = prefix + key; if (localStorage.hasOwnProperty(storageKey)) { try { let lastValue = config[key]; let newValue = JSON.parse(localStorage.getItem(storageKey)); if (lastValue !== newValue) { config[key] = newValue; if (emitChange) { emitter.emit('key', newValue); emitter.emit('@update', {key, value: newValue}); } } } catch (e) { window.console.error('config parse error key:"%s" value:"%s" ', key, localStorage.getItem(storageKey), e); config[key] = defaultConfig[key]; } } else { config[key] = defaultConfig[key]; } }); }; emitter.refresh(); emitter.get = function(key, refresh) { if (refresh) { emitter.refreshValue(key); } return config[key]; }; emitter.set = function(key, value) { if (config[key] !== value && arguments.length >= 2) { let storageKey = prefix + key; localStorage.setItem(storageKey, JSON.stringify(value)); config[key] = value; emitter.emit(key, value); emitter.emit('@update', {key, value}); } }; return emitter; })(); class BaseViewComponent extends Emitter { constructor({parentNode = null, name = '', template = '', shadow = '', css = ''}) { super(); this._params = {parentNode, name, template, shadow, css}; this._bound = {}; this._state = {}; this._props = {}; this._elm = {}; this._initDom({ parentNode, name, template, shadow, css }); } _initDom({parentNode, name, template, css = '', shadow = ''}) { let tplId = `${PRODUCT}${name}Template`; let tpl = document.getElementById(tplId); if (!tpl) { if (css) { util.addStyle(css, `${name}Style`); } tpl = document.createElement('template'); tpl.innerHTML = template; tpl.id = tplId; document.body.appendChild(tpl); } const onClick = this._bound.onClick = this._onClick.bind(this); const view = document.importNode(tpl.content, true); this._view = view.querySelector('*') || document.createDocumentFragment(); if (this._view) { this._view.addEventListener('click', onClick); } this.appendTo(parentNode); if (shadow) { this._attachShadow({host: this._view, name, shadow}); if (!this._isDummyShadow) { this._shadow.addEventListener('click', onClick); } } } _attachShadow ({host, shadow, name, mode = 'open'}) { let tplId = `${PRODUCT}${name}Shadow`; let tpl = document.getElementById(tplId); if (!tpl) { tpl = document.createElement('template'); tpl.innerHTML = shadow; tpl.id = tplId; document.body.appendChild(tpl); } if (!host.attachShadow && !host.createShadowRoot) { return this._fallbackNoneShadowDom({host, tpl, name}); } const root = host.attachShadow ? host.attachShadow({mode}) : host.createShadowRoot(); const node = document.importNode(tpl.content, true); root.appendChild(node); this._shadowRoot = root; this._shadow = root.querySelector('.root'); this._isDummyShadow = false; } _fallbackNoneShadowDom({host, tpl, name}) { const node = document.importNode(tpl.content, true); const style = node.querySelector('style'); style.remove(); util.addStyle(style.innerHTML, `${name}Shadow`); host.appendChild(node); this._shadow = this._shadowRoot = host.querySelector('.root'); this._isDummyShadow = true; } setState(key, val) { if (typeof key === 'string') { this._setState(key, val); } Object.keys(key).forEach(k => { this._setState(k, key[k]); }); } _setState(key, val) { if (this._state[key] !== val) { this._state[key] = val; if (/^is(.*)$/.test(key)) { this.toggleClass(`is-${RegExp.$1}`, !!val); } this.emit('update', {key, val}); } } _onClick(e) { const target = e.target.classList.contains('command') ? e.target : e.target.closest('.command'); if (!target) { return; } const command = target.getAttribute('data-command'); if (!command) { return; } const type = target.getAttribute('data-type') || 'string'; let param = target.getAttribute('data-param'); e.stopPropagation(); e.preventDefault(); param = this._parseParam(param, type); this._onCommand(command, param); } _parseParam(param, type) { switch (type) { case 'json': case 'bool': case 'number': param = JSON.parse(param); break; } return param; } appendTo(parentNode) { if (!parentNode) { return; } this._parentNode = parentNode; parentNode.appendChild(this._view); } _onCommand(command, param) { this.emit('command', command, param); } toggleClass(className, v) { (className || '').split(/ +/).forEach((c) => { if (this._view && this._view.classList) { this._view.classList.toggle(c, v); } if (this._shadow && this._shadow.classList) { this._shadow.classList.toggle(c, this._view.classList.contains(c)); } }); } addClass(name) { this.toggleClass(name, true); } removeClass(name) { this.toggleClass(name, false); } } class ConfigPanel extends BaseViewComponent { constructor({parentNode}) { super({ parentNode, name: 'ZenzaGamePadConfigPanel', shadow: ConfigPanel.__shadow__, template: '<div class="ZenzaGamePadConfigPanelContainer zen-family"></div>', css: '' }); this._state = { isOpen: false, isVisible: false }; Config.on('refresh', this._onBeforeShow.bind(this)); } _initDom(...args) { super._initDom(...args); const v = this._shadow; this._elm.enabled = v.querySelector('[data-config-name="enabled"]'); this._elm.needFocus = v.querySelector('[data-config-name="needFocus"]'); this._elm.deviceIndex = v.querySelector('[data-config-name="deviceIndex"]'); const onChange = e => { const target = e.target, name = target.getAttribute('data-config-name'); switch (target.tagName) { case 'SELECT': case 'INPUT': if (target.type === 'checkbox') { Config.set(name, target.checked); } else { const type = target.getAttribute('data-type'); const value = this._parseParam(target.value, type); Config.set(name, value); } break; default: //console.info('target', e, target, name, target.checked); Config.set(name, !!target.checked); break; } }; this._elm.enabled.addEventListener('change', onChange); this._elm.needFocus.addEventListener('change', onChange); this._elm.deviceIndex.addEventListener('change', onChange); v.querySelector('.closeButton') .addEventListener('click', this.hide.bind(this)); } _onClick(e) { super._onClick(e); } _onMouseDown(e) { this.hide(); this._onClick(e); } show() { document.body.addEventListener('click', this._bound.onBodyClick); this._onBeforeShow(); this.setState({isOpen: true}); if (this._shadow.showModal) { this._shadow.showModal(); } window.setTimeout(() => { this.setState({isVisible: true}); }, 100); } hide() { document.body.removeEventListener('click', this._bound.onBodyClick); if (this._shadow.close) { this._shadow.close(); } this.setState({isVisible: false}); window.setTimeout(() => { this.setState({isOpen: false}); }, 2100); } toggle() { if (this._state.isOpen) { this.hide(); } else { this.show(); } } _onBeforeShow() { this._elm.enabled.checked = !!Config.get('enabled'); this._elm.needFocus.checked = !!Config.get('needFocus'); this._elm.deviceIndex.value = Config.get('deviceIndex'); } } ConfigPanel.__shadow__ = (` <style> .ZenzaGamePadConfigPanel { display: none; position: fixed; z-index: ${CONSTANT.BASE_Z_INDEX}; top: 50vh; left: 50vw; padding: 8px; border: 2px outset; box-shadow: 0 0 8px #000; background: #ccc; transform: translate(-50%, -50%); transition: opacity 0.5s; transform-origin: center bottom; animation-timing-function: steps(10); perspective-origin: center bottom; user-select: none; margin: 0; pointer-events: auto !important; } .ZenzaGamePadConfigPanel[open] { display: block; opacity: 1; } .ZenzaGamePadConfigPanel.is-Open { display: block; opacity: 0; } .ZenzaGamePadConfigPanel.is-Open.is-Visible { opacity: 1; } .title { font-weight: bolder; font-size: 120%; font-family: 'arial black'; margin: 0 0 8px; text-align: center; } .closeButton { display: block; text-align: center; } .closeButton { display: block; padding: 8px 16px; cursor: pointer; margin: auto; } label { cursor: pointer; } input[type="number"] { width: 50px; } input[type="checkbox"] { transform: scale(2); margin-right: 16px; } .ZenzaGamePadConfigPanel>div { padding: 8px; } </style> <dialog class="root ZenzaGamePadConfigPanel zen-family"> <p class="title">†ZenzaGamePad†</p> <div class="enableSelect"> <label> <input type="checkbox" data-config-name="enabled" data-type="bool"> ZenzaGamePadを有効にする </label> </div> <div class="needFocusSelect"> <label> <input type="checkbox" data-config-name="needFocus" data-type="bool"> ウィンドウフォーカスのあるときのみ有効 </label> </div> <div class="deviceIndex"> <label> デバイス番号 <select class="deviceIndexSelector" data-config-name="deviceIndex" data-type="number"> <option value="0">0</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <small>(※リロードが必要)</small> </label> </div> <div class="closeButtonContainer"> <button class="closeButton" type="button"> 閉じる </button> </div> </dialog> `).trim(); class ToggleButton extends BaseViewComponent { constructor({parentNode}) { super({ parentNode, name: 'ZenzaGamePadToggleButton', shadow: ToggleButton.__shadow__, template: '<div class="ZenzaGamePadToggleButtonContainer"></div>', css: '' }); this._state = { isEnabled: undefined }; Config.on('enabled', () => { this.refresh(); }); } refresh() { this.setState({isEnabled: Config.get('enabled')}); } } ToggleButton.__shadow__ = ` <style> .controlButton { position: relative; display: inline-block; transition: opacity 0.4s ease, margin-left 0.2s ease, margin-top 0.2s ease; box-sizing: border-box; text-align: center; cursor: pointer; color: #fff; opacity: 0.8; vertical-align: middle; } .controlButton:hover { cursor: pointer; opacity: 1; } .controlButton .controlButtonInner { filter: grayscale(100%); } .heatSyncSwitch { font-size: 16px; width: 32px; height: 32px; line-height: 30px; cursor: pointer; } .is-Enabled .controlButtonInner { color: #aef; filter: none; } .controlButton .tooltip { display: none; pointer-events: none; position: absolute; left: 16px; top: -30px; transform: translate(-50%, 0); font-size: 12px; line-height: 16px; padding: 2px 4px; border: 1px solid !000; background: #ffc; color: #000; text-shadow: none; white-space: nowrap; z-index: 100; opacity: 0.8; } .controlButton:hover { background: #222; } .controlButton:hover .tooltip { display: block; opacity: 1; } </style> <div class="heatSyncSwitch controlButton root command" data-command="toggleZenzaGamePadConfig"> <div class="controlButtonInner" title="ZenzaGamePad">🎮</div> <div class="tooltip">ZenzaGamePad</div> </div> `.trim(); var execCommand = function(command, param) { ZenzaWatch.external.execCommand(command, param); }; var speedUp = function() { // TODO: // configを直接参照するのはお行儀が悪いのでexternalのインターフェースをつける var current = parseFloat(ZenzaWatch.config.getValue('playbackRate'), 10); window.console.log('speedUp', current); execCommand('playbackRate', Math.floor(Math.min(current + 0.1, 3) * 10) / 10); }; var speedDown = function() { // TODO: // configを直接参照するのはお行儀が悪いのでexternalのインターフェースをつける var current = parseFloat(ZenzaWatch.config.getValue('playbackRate'), 10); window.console.log('speedDown', current); execCommand('playbackRate', Math.floor(Math.max(current - 0.1, 0.1) * 10) / 10); }; var swapABXY_FC30 = function(btn) { switch (btn) { case 0: return 1; case 1: return 0; case 3: return 4; case 4: return 3; } return btn; }; var onButtonDown = function(button, deviceId) { if (!isZenzaWatchOpen) { return; } if (deviceId.match(/Vendor: 04b4 Product: 010a/i)) { //USB Gamepad (Vendor: 04b4 Product: 010a)" return onButtonDownSaturn(button, deviceId); } else if (deviceId.match(/Vendor: (3810|05a0|1235|1002)/i)) { // FC30なのにみんなVendor違うってどういうことだよ // 8Bitdo FC30 Pro (Vendor: 1002 Product: 9000) return onButtonDownFC30(button, deviceId); } else if (deviceId.match(/Vendor: 057e Product: 2006/i)) { return onButtonDownJoyConL(button, deviceId); } else if (deviceId.match(/Vendor: 057e Product: 2007/i)) { return onButtonDownJoyConR(button, deviceId); } switch (button) { case 0: // A isPauseButtonDown = true; execCommand('togglePlay'); break; case 1: // B execCommand('toggle-mute'); break; case 2: // X execCommand('toggle-showComment'); break; case 3: // Y isRate1ButtonDown = true; execCommand('playbackRate', 0.1); break; case 4: // LB execCommand('playPreviousVideo'); break; case 5: // RB execCommand('playNextVideo'); break; case 6: // LT execCommand('playbackRate', 0.5); break; case 7: // RT execCommand('playbackRate', 3); break; case 8: // しいたけの左 ビューボタン (Back) execCommand('screenShot'); break; case 9: // しいたけの右 メニューボタン (Start) execCommand('deflistAdd'); break; case 10: // Lスティック execCommand('seek', 0); break; case 11: // Rスティック break; case 12: // up if (isPauseButtonDown) { speedUp(); } else { execCommand('volumeUp'); } break; case 13: // down if (isPauseButtonDown) { speedDown(); } else { execCommand('volumeDown'); } break; case 14: // left if (isPauseButtonDown) { execCommand('seekPrevFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? -1 : -5); } break; case 15: // right if (isPauseButtonDown) { execCommand('seekNextFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? +1 : +5); } break; } }; var onButtonDownSaturn = function(button, deviceId) { switch (button) { case 0: // A isPauseButtonDown = true; execCommand('togglePlay'); break; case 1: // B execCommand('toggle-mute'); break; case 2: // C execCommand('toggle-showComment'); break; case 3: // X execCommand('playbackRate', 0.5); break; case 4: // Y isRate1ButtonDown = true; execCommand('playbackRate', 0.1); break; case 5: // Z execCommand('playbackRate', 3); break; case 6: // L execCommand('playPreviousVideo'); break; case 7: // R execCommand('playNextVideo'); break; case 8: // START execCommand('deflistAdd'); break; } }; var onButtonDownFC30 = function(button, deviceId) { if (deviceId.match(/Product: (3232)/i)) { // FC30 Zero / FC30 button = swapABXY_FC30(button); } switch (button) { case 0: // B execCommand('toggle-mute'); break; case 1: // A isPauseButtonDown = true; execCommand('togglePlay'); break; case 2: // ??? //execCommand('toggle-showComment'); break; case 3: // X isRate1ButtonDown = true; execCommand('playbackRate', 0.1); break; case 4: // Y execCommand('toggle-showComment'); break; case 5: // break; case 6: // L1 if (isPauseButtonDown) { execCommand('playPreviousVideo'); } else { execCommand('playbackRate', 0.5); } break; case 7: // R1 if (isPauseButtonDown) { execCommand('playNextVideo'); } else { execCommand('playbackRate', 3); } break; case 8: // L2 execCommand('playPreviousVideo'); break; case 9: // R2 execCommand('playNextVideo'); break; case 10: // SELECT execCommand('screenShot'); break; case 11: // START execCommand('deflistAdd'); break; case 13: // Lスティック execCommand('seek', 0); break; case 14: // Rスティック break; } }; var onButtonDownJoyConL = function(button) { switch (button) { case 0: // Y ◀ if (isMetaButtonDown) { execCommand('toggle-showComment'); } else if (isPauseButtonDown) { execCommand('seekPrevFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? -1 : -5); } break; case 1: // B ▼ isPauseButtonDown = true; execCommand('togglePlay'); break; case 2: // X ▲ if (isMetaButtonDown) { execCommand('playbackRate', 2); } else { isRate1ButtonDown = true; execCommand('playbackRate', 0.1); } break; case 3: // A ▶ if (isMetaButtonDown) { execCommand('toggle-mute'); } else if (isPauseButtonDown) { execCommand('seekNextFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? +1 : +5); } break; case 4: // SL if (isMetaButtonDown) { speedUp(); } else { execCommand('volumeUp'); } break; case 5: // SR if (isMetaButtonDown) { speedDown(); } else { execCommand('volumeDown'); } break; case 8: // - if (isMetaButtonDown) { execCommand('playPreviousVideo'); } else { execCommand('playNextVideo'); } break; case 13: // SCREENSHOT execCommand('screenShot'); break; case 10: // スティック押し込み execCommand('seek', 0); break; case 14: // L if (isMetaButtonDown) { execCommand('toggle-showComment'); } else { execCommand('toggle-mute'); } break; case 15: // ZL isMetaButtonDown = true; break; } }; var onButtonDownJoyConR = function(button) { switch (button) { case 3: // Y if (isMetaButtonDown) { execCommand('toggle-showComment'); } else if (isPauseButtonDown) { execCommand('seekPrevFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? -1 : -5); } break; case 2: // B isPauseButtonDown = true; execCommand('togglePlay'); break; case 1: // X if (isMetaButtonDown) { execCommand('playbackRate', 2); } else { isRate1ButtonDown = true; execCommand('playbackRate', 0.1); } break; case 0: // A if (isMetaButtonDown) { execCommand('toggle-mute'); } else if (isPauseButtonDown) { execCommand('seekNextFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? +1 : +5); } break; case 4: // SL if (isMetaButtonDown) { speedDown(); } else { execCommand('volumeDown'); } break; case 5: // SR if (isMetaButtonDown) { speedUp(); } else { execCommand('volumeUp'); } break; case 9: //+ if (isMetaButtonDown) { execCommand('playPreviousVideo'); } else { execCommand('playNextVideo'); } break; case 12: // HOME execCommand('screenShot'); break; case 11: // スティック押し込み execCommand('seek', 0); break; case 14: // L R if (isMetaButtonDown) { execCommand('toggle-showComment'); } else { execCommand('toggle-mute'); } break; case 15: // ZL ZR isMetaButtonDown = true; break; } }; var onButtonUp = function(button, deviceId) { if (!isZenzaWatchOpen) { return; } if (deviceId.match(/Vendor: 04b4 Product: 010a/i)) { //USB Gamepad (Vendor: 04b4 Product: 010a)" return onButtonUpSaturn(button, deviceId); } else if (deviceId.match(/Vendor: (3810|05a0|1235|1002)/i)) { // 8Bitdo FC30 Pro (Vendor: 1002 Product: 9000) return onButtonUpFC30(button, deviceId); } else if (deviceId.match(/Vendor: 057e Product: 2006/i)) { return onButtonUpJoyConL(button, deviceId); } else if (deviceId.match(/Vendor: 057e Product: 2007/i)) { return onButtonUpJoyConR(button, deviceId); } switch (button) { case 0: // A isPauseButtonDown = false; break; case 3: // Y isRate1ButtonDown = false; execCommand('playbackRate', 1.0); break; case 7: // RT execCommand('playbackRate', 1.5); break; } }; var onButtonUpSaturn = function(button, deviceId) { switch (button) { case 0: // A isPauseButtonDown = false; break; case 1: // B break; case 2: // C break; case 3: // X break; case 4: // Y isRate1ButtonDown = false; execCommand('playbackRate', 1.0); break; case 5: // Z execCommand('playbackRate', 1.5); break; case 6: // L break; case 7: // R break; case 8: // START break; } }; var onButtonUpFC30 = function(button, deviceId) { if (deviceId.match(/Product: (3232)/i)) { // FC30Zero / FC30 button = swapABXY_FC30(button); } switch (button) { case 0: // B break; case 1: // A isPauseButtonDown = false; break; case 2: // ??? break; case 3: // X isRate1ButtonDown = false; execCommand('playbackRate', 1.0); break; case 4: // Y break; case 5: // break; case 6: // L1 break; case 7: // R1 if (isPauseButtonDown) { return; } execCommand('playbackRate', 1.5); break; case 8: // L2 break; case 9: // R2 break; case 10: // SELECT break; case 11: // START break; case 13: // Lスティック break; case 14: // Rスティック break; } }; var onButtonUpJoyConL = function(button, deviceId) { switch (button) { case 0: // ◀ break; case 1: // ▼ isPauseButtonDown = false; break; case 2: // ▲ isRate1ButtonDown = false; execCommand('playbackRate', 1); break; case 15: // ZL isMetaButtonDown = false; break; } }; var onButtonUpJoyConR = function(button, deviceId) { switch (button) { case 3: // Y break; case 2: // B isPauseButtonDown = false; break; case 1: // X isRate1ButtonDown = false; execCommand('playbackRate', 1); break; case 0: // A break; case 15: // ZR isMetaButtonDown = false; break; } }; var onButtonRepeat = function(button, deviceId) { if (!isZenzaWatchOpen) { return; } if (deviceId.match(/Vendor: 057e Product: 2006/i)) { // Joy-Con (L) return onButtonRepeatJoyConL(button); } else if (deviceId.match(/Vendor: 057e Product: 2007/i)) { // Joy-Con (R) return onButtonRepeatJoyConR(button); } switch (button) { case 12: // up if (isPauseButtonDown) { speedUp(); } else { execCommand('volumeUp'); } break; case 13: // down if (isPauseButtonDown) { speedUp(); } else { execCommand('volumeDown'); } break; case 14: // left if (isPauseButtonDown) { execCommand('seekPrevFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? -1 : -5); } break; case 15: // right if (isPauseButtonDown) { execCommand('seekNextFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? +1 : +5); } break; } }; var onButtonRepeatJoyConL = function(button) { switch (button) { case 0: // Y ◀ if (isMetaButtonDown) { } else if (isPauseButtonDown) { execCommand('seekPrevFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? -1 : -5); } break; case 1: // B ▼ break; case 2: // X ▲ break; case 3: // A ▶ if (isMetaButtonDown) { } else if (isPauseButtonDown) { execCommand('seekNextFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? +1 : +5); } break; case 4: // SL if (isMetaButtonDown) { speedUp(); } else { execCommand('volumeUp'); } break; case 5: // SR if (isMetaButtonDown) { speedDown(); } else { execCommand('volumeDown'); } break; } }; var onButtonRepeatJoyConR = function(button) { switch (button) { case 3: // Y ◀ if (isMetaButtonDown) { } else if (isPauseButtonDown) { execCommand('seekPrevFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? -1 : -5); } break; case 2: // B ▼ break; case 1: // X ▲ break; case 0: // A ▶ if (isMetaButtonDown) { } else if (isPauseButtonDown) { execCommand('seekNextFrame'); } else { execCommand('seekBy', isRate1ButtonDown ? +1 : +5); } break; case 4: // SL if (isMetaButtonDown) { speedDown(); } else { execCommand('volumeDown'); } break; case 5: // SR if (isMetaButtonDown) { speedUp(); } else { execCommand('volumeUp'); } break; } }; var onAxisChange = function(axis, value, deviceId) { if (!isZenzaWatchOpen) { return; } if (Math.abs(value) < 0.1) { return; } var isFC30 = deviceId.match(/Vendor: 3810/i) ? true : false; if (isFC30) { switch (axis) { case 3: // L2なぜか反応する case 4: // R2なぜか反応する return; case 5: // FC30のRスティック上下? axis = 3; break; } } if (deviceId.match(/Vendor: 057e Product: 2006/i)) { // Joy-Con (L) return; } else if (deviceId.match(/Vendor: 057e Product: 2007/i)) { // Joy-Con (R) return; } switch (axis) { case 0: // Lスティック X var step = isRate1ButtonDown ? 1 : 5; execCommand('seekBy', (value < 0 ? -1 : 1) * step); break; case 1: // Lスティック Y if (isPauseButtonDown) { if (value < 0) { speedUp(); } else { speedDown(); } } else { execCommand(value < 0 ? 'volumeUp' : 'volumeDown'); } break; case 2: // Rスティック X break; case 3: // Rスティック Y if (value < 0) { speedUp(); } else { speedDown(); } break; } }; var onAxisRepeat = function(axis, value, deviceId) { if (!isZenzaWatchOpen) { return; } if (Math.abs(value) < 0.1) { return; } if (deviceId.match(/Vendor: 057e Product: 2006/i)) { // Joy-Con (L) return; } else if (deviceId.match(/Vendor: 057e Product: 2007/i)) { // Joy-Con (R) return; } switch (axis) { case 0: // Lスティック X var step = isRate1ButtonDown ? 1 : +5; execCommand('seekBy', (value < 0 ? -1 : 1) * step); break; case 1: // Lスティック Y if (isPauseButtonDown) { if (value < 0) { speedUp(); } else { speedDown(); } } else { execCommand(value < 0 ? 'volumeUp' : 'volumeDown'); } break; case 2: // Rスティック X break; case 3: // Rスティック Y if (value < 0) { speedUp(); } else { speedDown(); } break; } }; var onPovChange = function(pov, deviceId) { if (deviceId.match(/Vendor: 057e Product: 2006/i)) { // Joy-Con (L) return; } else if (deviceId.match(/Vendor: 057e Product: 2007/i)) { // Joy-Con (R) return; } switch(pov) { case 'up': execCommand('volumeUp'); break; case 'down': execCommand('volumeDown'); break; case 'left': execCommand('seekBy', isRate1ButtonDown ? -1 : -5); break; case 'right': execCommand('seekBy', isRate1ButtonDown ? +1 : +5); break; } }; var onPovRepeat = function(pov) { if (deviceId.match(/Vendor: 057e Product: 2006/i)) { // Joy-Con (L) return; } else if (deviceId.match(/Vendor: 057e Product: 2007/i)) { // Joy-Con (R) return; } switch(pov) { case 'up': execCommand('volumeUp'); break; case 'down': execCommand('volumeDown'); break; case 'left': execCommand('seekBy', isRate1ButtonDown ? -1 : -5); break; case 'right': execCommand('seekBy', isRate1ButtonDown ? +1 : +5); break; } }; var PollingTimer = (function() { var id = 0; var PollingTimer = function(callback, interval) { this._id = id++; this.initialize(callback, interval); }; _.assign(PollingTimer.prototype, { initialize: function(callback, interval) { this._timer = null; this._callback = callback; if (typeof interval === 'number') { this.changeInterval(interval); } }, changeInterval: function(interval) { if (this._timer) { if (this._currentInterval === interval) { return; } window.clearInterval(this._timer); } console.log('%cupdate Interval:%s', 'background: lightblue;', interval); this._currentInterval = interval; this._timer = window.setInterval(this._callback, interval); }, pause: function() { window.clearInterval(this._timer); this._timer = null; }, start: function() { if (typeof this._currentInterval !== 'number') { return; } this.changeInterval(this._currentInterval); } }); return PollingTimer; })(); let GamePadModel = (function($, _, emitter) { class GamePadModel extends emitter { constructor(gamepadStatus) { super(); this._gamepadStatus = gamepadStatus; this._buttons = []; this._axes = []; this._pov = ''; this._lastTimestamp = 0; this._povRepeat = 0; this.initialize(gamepadStatus); } } _.assign(GamePadModel.prototype, { initialize: function(gamepadStatus) { this._buttons.length = gamepadStatus.buttons.length; this._axes.length = gamepadStatus.axes.length; this._id = gamepadStatus.id; this._index = gamepadStatus.index; this._isRepeating = false; this.reset(); }, reset: function() { var i, len; this._pov = ''; this._povRepeat = 0; for (i = 0, len = this._gamepadStatus.buttons.length + 16; i < len; i++) { this._buttons[i] = {pressed: false, repeat: 0}; } for (i = 0, len = this._gamepadStatus.axes.length; i < len; i++) { this._axes[i] = {value: null, repeat: 0}; } }, update: function() { var gamepadStatus = (navigator.getGamepads())[this._index]; // gp || this._gamepadStatus; if (!gamepadStatus) { console.log('no status'); return; } if (!this._isRepeating && this._lastTimestamp === gamepadStatus.timestamp) { return; } this._gamepadStatus = gamepadStatus; this._lastTimestamp = gamepadStatus.timestamp; var buttons = gamepadStatus.buttons, axes = gamepadStatus.axes; var i, len, axis, isRepeating = false; for (i = 0, len = Math.min(this._buttons.length, buttons.length); i < len; i++) { var buttonStatus = buttons[i].pressed ? 1 : 0; if (this._buttons[i].pressed !== buttonStatus) { var eventName = (buttonStatus === 1) ? 'onButtonDown' : 'onButtonUp'; //console.log('%cbutton%s:%s', 'background: lightblue;', i, buttonStatus, 0); this.emit(eventName, i, 0); this.emit('onButtonStatusChange', i, buttonStatus); } this._buttons[i].pressed = buttonStatus; if (buttonStatus) { this._buttons[i].repeat++; isRepeating = true; if (this._buttons[i].repeat % 5 === 0) { //console.log('%cbuttonRepeat%s', 'background: lightblue;', i); this.emit('onButtonRepeat', i); } } else { this._buttons[i].repeat = 0; } } for (i = 0, len = Math.min(8, this._axes.length); i < len; i++) { axis = Math.round(axes[i] * 1000) / 1000; if (this._axes[i].value === null) { this._axes[i].value = axis; continue; } var diff = Math.round(Math.abs(axis - this._axes[i].value)); if (diff >= 1) { this.emit('onAxisChange', i, axis); } if (Math.abs(axis) <= 0.1 && this._axes[i].repeat > 0) { this._axes[i].repeat = 0; } else if (Math.abs(axis) > 0.1) { this._axes[i].repeat++; isRepeating = true; } else { this._axes[i].repeat = 0; } this._axes[i].value = axis; } // ハットスイッチ? FC30だけ? if (axes[9]) { var p = Math.round(axes[9] * 1000); var d; if (p < -500) { d = 'up'; } else if (p < 0) { d = 'right'; } else if (p > 3000) { d = ''; } else if (p > 500){ d = 'left'; } else { d = 'down'; } //if (d) { window.console.log('pov?:', axes[9], d, p); } if (this._pov !== d) { this._pov = d; this._povRepeat = 0; this.emit('onPovChange', this._pov); } else if (d !== '') { this._povRepeat++; isRepeating = true; if (this._povRepeat % 5 === 0) { this.emit('onPovRepeat', this._pov); } } } this._isRepeating = isRepeating; //console.log(JSON.stringify(this.dump())); }, dump: function() { var gamepadStatus = this._gamepadStatus, buttons = gamepadStatus.buttons, axes = gamepadStatus.axes; var i, len, btmp = [], atmp = []; for (i = 0, len = axes.length; i < len; i++) { atmp.push('ax' + i + ': ' + axes[i]); } for (i = 0, len = buttons.length; i < len; i++) { btmp.push('bt' + i + ': ' + (buttons[i].pressed ? 1 : 0)); } return atmp.join('\n') + '\n' + btmp.join(', '); }, getX: function() { return this._axes.length > 0 ? this._axes[0] : 0; }, getY: function() { return this._axes.length > 1 ? this._axes[1] : 0; }, getZ: function() { return this._axes.length > 2 ? this._axes[2] : 0; }, getButtonCount: function() { return this._buttons ? this._buttons.length : 0; }, getButtonStatus: function(index) { return this._buttons[index] || 0; }, getAxisCount: function() { return this._axes ? this._axes.length : 0; }, getAxisValue: function(index) { return this._axes[index] || 0; }, isConnected: function() { return !!this._gamepadStatus.connected; }, getDeviceIndex: function() { return this._index; }, getDeviceId: function() { return this._id; }, getPov: function() { return this._pov; }, release: function() { // TODO: clear events } }); return GamePadModel; })($, _, Emitter); var ZenzaGamePad = (function ($, PollingTimer, GamePadModel) { var activeGamepad = null; var pollingTimer = null; var ZenzaGamePad = ZenzaWatch.modules ? new ZenzaWatch.modules.Emitter() : new Emitter(); const padIndex = Config.get('deviceIndex') * 1; var detectGamepad = function() { if (activeGamepad) { return; } var gamepads = navigator.getGamepads(); if (gamepads.length < 1) { return; } var pad = Array.from(gamepads).find(pad => { return pad && pad.connected && pad.id && pad.index === padIndex && // windowsにDualShock4を繋ぐとあらわれる謎のデバイス !pad.id.match(/Vendor: 00ff/i); }); if (!pad) { return; } window.console.log( '%cdetect gamepad index: %s, id: "%s"', 'background: lightgreen; font-weight: bolder;', pad.index, pad.id ); var gamepad = new GamePadModel(pad); activeGamepad = gamepad; var self = ZenzaGamePad; var onButtonDown = function(number) { self.emit('onButtonDown', number, gamepad.getDeviceIndex()); }; var onButtonRepeat = function(number) { self.emit('onButtonRepeat', number, gamepad.getDeviceIndex()); }; var onButtonUp = function(number) { self.emit('onButtonUp', number, gamepad.getDeviceIndex()); }; var onAxisChange = function(number, value) { self.emit('onAxisChange', number, value, gamepad.getDeviceIndex()); }; var onAxisRepeat = function(number, value) { self.emit('onAxisRepeat', number, value, gamepad.getDeviceIndex()); }; var onAxisRelease = function(number) { self.emit('onAxisRelease', number, gamepad.getDeviceIndex()); }; var onPovChange = function(pov) { self.emit('onPovChange', pov, gamepad.getDeviceIndex()); }; var onPovRepeat = function(pov) { self.emit('onPovRepeat', pov, gamepad.getDeviceIndex()); }; gamepad.on('onButtonDown', onButtonDown); gamepad.on('onButtonRepeat', onButtonRepeat); gamepad.on('onButtonUp', onButtonUp); gamepad.on('onAxisChange', onAxisChange); gamepad.on('onAxisRepeat', onAxisRepeat); gamepad.on('onAxisRelease', onAxisRelease); gamepad.on('onPovChange', onPovChange); gamepad.on('onPovRepeat', onPovRepeat); self.emit('onDeviceConnect', gamepad.getDeviceIndex(), gamepad.getDeviceId()); pollingTimer.changeInterval(30); }; var onGamepadConnectStatusChange = function(e, isConnected) { const padIndex = Config.get('deviceIndex') * 1; console.log('onGamepadConnetcStatusChange', e, e.gamepad.index, isConnected); if (e.gamepad.index !== padIndex) { return; } if (isConnected) { console.log('%cgamepad connected id:"%s"', 'background: lightblue;', e.gamepad.id); detectGamepad(); } else { ZenzaGamePad.emit('onDeviceDisconnect', activeGamepad.getDeviceIndex()); if (activeGamepad) { activeGamepad.release(); } activeGamepad = null; console.log('%cgamepad disconneced id:"%s"', 'background: lightblue;', e.gamepad.id); } }; var initializeTimer = function() { console.log('%cinitializeGamepadTimer', 'background: lightgreen;'); let onTimerInterval = function() { if (!Config.get('enabled')) { return; } if (Config.get('needFocus') && !document.hasFocus()) { return; } if (!activeGamepad) { detectGamepad(); return; } if (!activeGamepad.isConnected) { return; } activeGamepad.update(); }; pollingTimer = new PollingTimer(onTimerInterval, 1000); }; var initializeGamepadConnectEvent = function() { console.log('%cinitializeGamepadConnectEvent', 'background: lightgreen;'); window.addEventListener('gamepadconnected', function(e) { onGamepadConnectStatusChange(e, true); }); window.addEventListener('gamepaddisconnected', function(e) { onGamepadConnectStatusChange(e, false); }); if (activeGamepad) { return; } window.setTimeout(detectGamepad, 1000); }; ZenzaGamePad.startDetect = function() { ZenzaGamePad.startDetect = _.noop; initializeTimer(); initializeGamepadConnectEvent(); }; ZenzaGamePad.startPolling = function() { if (pollingTimer) { pollingTimer.start(); } }; ZenzaGamePad.stopPolling = function() { if (pollingTimer) { pollingTimer.pause(); } }; return ZenzaGamePad; })($, PollingTimer, GamePadModel); var initGamePad = function() { initGamePad = _.noop; var isActivated = false; var deviceId, deviceIndex; var notifyDetect = function() { if (!document.hasFocus()) { return; } isActivated = true; notifyDetect = _.noop; // 初めてボタンかキーが押されたタイミングで通知する execCommand( 'notify', 'ゲームパッド "' + deviceId + '" が検出されました' ); }; var _onButtonDown = function(number /*, deviceIndex*/) { notifyDetect(); if (!isActivated) { return; } onButtonDown(number, deviceId); //console.log('%conButtonDown: number=%s, device=%s', 'background: lightblue;', number, deviceIndex); }; var _onButtonRepeat = function(number /*, deviceIndex*/) { if (!isActivated) { return; } onButtonRepeat(number, deviceId); //console.log('%conButtonRepeat: number=%s, device=%s', 'background: lightblue;', number, deviceIndex); }; var _onButtonUp = function(number /*, deviceIndex*/) { //console.log('%conButtonUp: number=%s, device=%s', 'background: lightblue;', number, deviceIndex); if (!isActivated) { return; } onButtonUp(number, deviceId); }; var _onAxisChange = function(number, value /*, deviceIndex */) { notifyDetect(); if (!isActivated) { return; } onAxisChange(number, value, deviceId); //console.log('%conAxisChange: number=%s, value=%s, device=%s', 'background: lightblue;', number, value, deviceIndex); }; var _onAxisRepeat = function(number, value /*, deviceIndex */) { //console.log('%conAxisChange: number=%s, value=%s, device=%s', 'background: lightblue;', number, value, deviceIndex); if (!isActivated) { return; } onAxisRepeat(number, value, deviceId); }; var _onAxisRelease = function(/* number, deviceIndex */) { if (!isActivated) { return; } }; var _onPovChange = function(pov /*, deviceIndex */) { notifyDetect(); if (!isActivated) { return; } onPovChange(pov, deviceId); }; var _onPovRepeat = function(pov /*, deviceIndex */) { if (!isActivated) { return; } onPovRepeat(pov, deviceId); }; var bindEvents = function() { bindEvents = _.noop; ZenzaGamePad.on('onButtonDown', _onButtonDown); ZenzaGamePad.on('onButtonRepeat', _onButtonRepeat); ZenzaGamePad.on('onButtonUp', _onButtonUp); ZenzaGamePad.on('onAxisChange', _onAxisChange); ZenzaGamePad.on('onAxisRepeat', _onAxisRepeat); ZenzaGamePad.on('onAxisRelease', _onAxisRelease); ZenzaGamePad.on('onPovChange', _onPovChange); ZenzaGamePad.on('onPovRepeat', _onPovRepeat); }; var onDeviceConnect = function(index, id) { deviceIndex = index; deviceId = id; bindEvents(); }; ZenzaGamePad.on('onDeviceConnect', onDeviceConnect); //ZenzaGamePad.on('onDeviceDisConnect', onDeviceDisConnect); ZenzaGamePad.startDetect(); window.ZenzaWatch.ZenzaGamePad = ZenzaGamePad; }; var onZenzaWatchOpen = function() { isZenzaWatchOpen = true; initGamePad(); ZenzaGamePad.startPolling(); }; var onZenzaWatchClose = function() { isZenzaWatchOpen = false; ZenzaGamePad.stopPolling(); }; let initialize = function() { ZenzaWatch.emitter.on('DialogPlayerOpen', onZenzaWatchOpen); ZenzaWatch.emitter.on('DialogPlayerClose', onZenzaWatchClose); ZenzaWatch.emitter.on('videoControBar.addonMenuReady', (container, handler) => { ZenzaGamePad.configPanel = new ConfigPanel({parentNode: document.querySelector('#zenzaVideoPlayerDialog')}); let toggleButton = new ToggleButton({parentNode: container}); toggleButton.on('command', handler); toggleButton.refresh(); }); ZenzaWatch.emitter.on('command-toggleZenzaGamePadConfig', () => { ZenzaGamePad.configPanel.toggle(); }); }; initialize(); }; var loadMonkey = function() { var script = document.createElement('script'); script.id = 'ZenzaGamePadLoader'; script.setAttribute('type', 'text/javascript'); script.setAttribute('charset', 'UTF-8'); script.appendChild(document.createTextNode('(' + monkey + ')(window.ZenzaWatch);')); document.body.appendChild(script); }; var waitForZenzaWatch = function() { if (window.ZenzaWatch && window.ZenzaWatch.ready) { window.console.log('ZenzaWatch is Ready'); loadMonkey(); } else { document.body.addEventListener('ZenzaWatchInitialize', () => { //document.body.addEventListener('ZenzaWatchReady', function() { window.console.log('onZenzaWatchInitialize'); loadMonkey(); }, {once: true}); } }; waitForZenzaWatch(); })();