//@ts-check import JadefinIntegrity from '../JadefinIntegrity.js'; import Jadefin from "../Jadefin.js"; import JadefinMod from "../JadefinMod.js"; import JadefinModules from "../JadefinModules.js"; import JadefinUtils from "../JadefinUtils.js"; import { rd, rdom, rd$, RDOMListHelper } from "../utils/rdom.js"; export default JadefinIntegrity("InputEater", import.meta.url, () => new (class InputEater extends JadefinMod { hasShownPopupThisSession = false; canBeEating = false; _isEnabled = true; _isEating = false; /** @type {Element | undefined} */ _lastOsdPage; _btnRemote = rd$()` `; constructor() { super(); this._btnRemote.addEventListener("click", () => this.isEnabled = false); this.initHookDocumentAddEventListener(); this.initHookDocumentRemoveEventListener(); } get isEnabled() { return this._isEnabled; } set isEnabled(value) { this._isEnabled = value; this.updateIsEating(); } get shouldShowPopup() { return this.storage.get("shouldShowPopup", true); } set shouldShowPopup(value) { this.storage.set("shouldShowPopup", value); } get isEating() { return this._isEating; } set isEating(value) { if (this._isEating == value) { return; } this.log.i(`isEating = ${value}`); this._isEating = value; this.update(); } async init(name, url) { await super.init(name, url); await JadefinUtils.waitUntil(() => JadefinModules.syncPlay); await this.initStyle(); this.initHookSyncPlayEnabled(); this.initHookSyncPlayDisabled(); this.initHookMediaSessionHandlers(); this.initHookInputManagerHandler(); document.addEventListener("viewshow", () => { this.updateIsEating(); }); const ExtrasMenu = /** @type {import("./ExtrasMenu.js").default} */ (Jadefin.getMod("ExtrasMenu")); ExtrasMenu.items.push({ name: "Grab remote", secondaryText: "Take control over the party", icon: "settings_remote", in: ExtrasMenu.IN_CUSTOM, inCustom: (current, item) => { if (this.isEnabled) { item.name = "Grab remote"; item.secondaryText = "Take control over the party"; } else { item.name = "Disable controls"; item.secondaryText = "Give up control over the party"; } return this.canBeEating; }, cb: () => { this.isEnabled = !this.isEnabled; } }); this.log.i("Ready"); } initHookSyncPlayEnabled() { const orig = this._enableSyncPlay = JadefinModules.syncPlay.Manager.enableSyncPlay.bind(JadefinModules.syncPlay.Manager); JadefinModules.syncPlay.Manager.enableSyncPlay = (apiClient, groupInfo, showMessage) => { const rv = orig(apiClient, groupInfo, showMessage); this.updateIsEating(); return rv; }; } initHookSyncPlayDisabled() { const orig = this._disableSyncPlay = JadefinModules.syncPlay.Manager.disableSyncPlay.bind(JadefinModules.syncPlay.Manager); JadefinModules.syncPlay.Manager.disableSyncPlay = (showMessage) => { const rv = orig(showMessage); this.updateIsEating(); return rv; }; } initHookDocumentAddEventListener() { const orig = this._addEventListener = document.addEventListener.bind(document); document.addEventListener = (type, listener, options) => { if (type == "keydown") { const listenerStr = listener.toString(); // Anonymous function in playback-video if (listenerStr.indexOf(".btnPause") != -1 && listenerStr.indexOf("32") != -1 && listenerStr.indexOf("playPause") != -1) { this.log.i("Wrapping playback-video keydown listener"); this.log.dir(listener); const origListener = this._playbackKeyDown = listener; listener = this._playbackKeyDownWrap = (e) => { if (this.isEating) { if (e.keyCode == 32) { return; } switch (e.key) { case "ArrowLeft": case "ArrowRight": case "Enter": case "Escape": case "Back": case "k": case "l": case "ArrowRight": case "Right": case "j": case "ArrowLeft": case "Left": case "p": case "P": case "n": case "N": case "NavigationLeft": case "GamepadDPadLeft": case "GamepadLeftThumbstickLeft": case "NavigationRight": case "GamepadDPadRight": case "GamepadLeftThumbstickRight": case "Home": case "End": case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": case ">": case "<": case "PageUp": case "PageDown": return; } } return origListener(e); }; } } return orig(type, listener, options); }; } initHookDocumentRemoveEventListener() { const orig = this._removeEventListener = document.removeEventListener.bind(document); document.removeEventListener = (type, listener, options) => { if (listener == this._playbackKeyDown) { listener = this._playbackKeyDownWrap; } return orig(type, listener, options); }; } initHookMediaSessionHandlers() { const owner = Jadefin.findWebpackRawLoad(e => JadefinUtils.findDeep(e, 1, (_, o) => o.nextTrack && o.fastForward)); if (!owner || !("mediaSession" in navigator)) { this.log.e("Couldn't hook media session handlers"); return; } const basic = (name, e) => { if (this.isEating) { return; } owner[name](owner.getCurrentPlayer()); } const handlerTuples = [ ["previoustrack", (e) => basic("previousTrack", e)], ["nexttrack", (e) => basic("nextTrack", e)], ["play", (e) => basic("unpause", e)], ["pause", (e) => basic("pause", e)], ["seekbackward", (e) => basic("rewind", e)], ["seekforward", (e) => basic("fastForward", e)], ["seekto", (e) => { if (this.isEating) { return; } const player = owner.getCurrentPlayer(); const item = owner.getPlayerState(player).NowPlayingItem; const currentTime = Math.floor(item.RunTimeTicks ? item.RunTimeTicks / 10000 : 0); const seekTime = 1000 * e.seekTime; owner.seekPercent(seekTime / currentTime * 100, player); }] ]; const handlers = this._handlers = {}; for (let handlerTuple of handlerTuples) { this.log.i(`Replacing media session action handler ${handlerTuple[0]}`); // @ts-ignore navigator.mediaSession.setActionHandler(handlerTuple[0], handlers[handlerTuple[0]] = handlerTuple[1].bind(this)); } } initHookInputManagerHandler() { const orig = this._handleCommand = JadefinModules.inputManager.handleCommand.bind(JadefinModules.inputManager); JadefinModules.inputManager.handleCommand = (commandName, options) => { if (this.isEating) { switch (commandName) { case "nextchapter": case "next": case "nexttrack": case "previous": case "previoustrack": case "previouschapter": case "play": case "pause": case "playpause": case "stop": case "increaseplaybackrate": case "decreaseplaybackrate": case "fastforward": case "rewind": case "seek": case "repeatnone": case "repeatall": case "repeatone": return; } } return orig(commandName, options); }; } /** * @param {boolean} [force] */ showPopup(force) { if (!force && (!this.shouldShowPopup || this.hasShownPopupThisSession)) { return false; } this.hasShownPopupThisSession = true; const ExtrasMenu = /** @type {import("./ExtrasMenu.js").default} */ (Jadefin.getMod("ExtrasMenu")); JadefinModules.actionSheet.show({ dialogClass: "inputEaterInfo", positionTo: ExtrasMenu.headerExtrasEl, title: "Controls disabled.", text: "You can toggle it in this corner.", items: [ {name: "Okay", icon: "tune"} ] }); return true; } async updateIsEating() { let isSyncPlayEnabled = JadefinModules.syncPlay.Manager.isSyncPlayEnabled(); let isSyncPlayOwner = true; if (isSyncPlayEnabled) { const groupInfo = JadefinModules.syncPlay.Manager.groupInfo; const currentUser = await JadefinModules.ApiClient.getCurrentUser(); isSyncPlayOwner = groupInfo.GroupName.indexOf(currentUser.Name) != -1; } this.canBeEating = isSyncPlayEnabled && !isSyncPlayOwner && JadefinUtils.routePathIsVideo; this.isEating = this.isEnabled && this.canBeEating; } update() { document.body.setAttribute("input-eaten", this.isEating ? "true" : "false"); if (this.isEating) { this.canBeEating = true; if (!this.showPopup()) { JadefinModules.toast("Controls disabled."); } } this._btnRemote.classList.toggle("hide", !this.isEating); const videoOsdPage = document.querySelector("div#videoOsdPage:not(.hide)"); if (!videoOsdPage) { return; } if (this._lastOsdPage == videoOsdPage) { return; } this._lastOsdPage = videoOsdPage; this.log.i("Adding event listener to videoOsdPage"); this.log.dir(videoOsdPage); videoOsdPage.addEventListener(window.PointerEvent ? "pointerdown" : "click", e => { if (this.isEating) { e.stopPropagation(); } }, true); const buttons = videoOsdPage.querySelector(".osdControls > .buttons"); if (this._btnRemote.parentElement != buttons && buttons) { this.log.i("Adding remote button to osd buttons"); this.log.dir(buttons); buttons.insertBefore(this._btnRemote, buttons.firstChild); } } })());