Weread gamepad control

使用xbox 游戏手柄控制微信读书翻页、滚动、全屏

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Weread gamepad control
// @namespace   Violentmonkey Scripts
// @match       https://weread.qq.com/web/reader/*
// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @version     1.2
// @author      -
// @license     GPL 3.0
// @description 使用xbox 游戏手柄控制微信读书翻页、滚动、全屏
// @update        update v1.1 增加Y切换全屏功能
// @update        update v1.2 增加LB RB 切换主题,X切换亮度功能
// ==/UserScript==

var gamepadAPI = {
    job: 0,

    update: function () {
        // 清除按钮缓存
        gamepadAPI.buttonsCache = [];
        // 从上一帧中移动按钮状态到缓存中
        for (var k = 0; k < gamepadAPI.buttonsStatus.length; k++) {
            gamepadAPI.buttonsCache[k] = gamepadAPI.buttonsStatus[k];
        }
        // 清除按钮状态
        gamepadAPI.buttonsStatus = [];
        // 获取 gamepad 对象
        var c = navigator.getGamepads()[0];

        // 遍历按键,并将按下的按钮加到数组中
        var pressed = [];
        if (c.buttons) {
            for (var b = 0, t = c.buttons.length; b < c.buttons.length; b++) {
                if (c.buttons[b].pressed) {
                    // console.log("buttons pressed " + gamepadAPI.buttons[b])
                    pressed.push(gamepadAPI.buttons[b]);
                }
            }
        }
        // 遍历坐标值并加到数组中
        var axes = [];
        if (c.axes) {
            for (var a = 0, x = c.axes.length; a < x; a++) {
                if (Math.abs(c.axes[a]) > 0.7) {
                    console.log("axes pressed " + gamepadAPI.axes[a])
                }
                axes.push(c.axes[a].toFixed(2));
            }
        }
        // 分配接收到的值
        gamepadAPI.axesStatus = axes;
        gamepadAPI.buttonsStatus = pressed;
        // 返回按钮以便调试
        return pressed;
    },

    buttonPressed: function (button, hold, released) {
        var newPress = false;
        var newRelease = false;

        // 轮询按下的按钮
        for (var i = 0, s = gamepadAPI.buttonsStatus.length; i < s; i++) {
            // 如果我们找到我们想要的按钮
            if (gamepadAPI.buttonsStatus[i] == button) {
                // 设置布尔变量(newPress)为 true
                newPress = true;
                // 如果我们想检查按住还是单次按下
                if (!hold) {
                    // 从上一帧轮询缓存状态
                    for (var j = 0, p = gamepadAPI.buttonsCache.length; j < p; j++) {
                        // 如果按钮(之前)已经被按下了则忽略新的按下状态
                        if (gamepadAPI.buttonsCache[j] == button) {
                            newPress = false;
                        }
                    }
                }
            }
        }

        // 检查是不是放开按钮
        if (released) {
            for (var j = 0, p = gamepadAPI.buttonsCache.length; j < p; j++) {
                if (gamepadAPI.buttonsCache[j] == button) {
                    // 检查当前帧中按钮是否未被按下
                    if (!gamepadAPI.buttonsStatus.includes(button)) {
                        newRelease = true;
                    }
                }
            }
        }

        return { pressed: newPress, released: newRelease };
    },

    buttons: [
        'A', 'B', 'X', 'Y',
        'LB', 'RB', 'LT', 'RT',
        'Option', 'Start',
        'Axis-Left', 'Axis-Right',
        'DPad-Up', 'DPad-Down', 'DPad-Left', 'DPad-Right',
    ],
    axes: [
        'Left H', 'Left V',
        'Right H', 'Right V',
    ],
    buttonsCache: [],
    buttonsStatus: [],
    axesStatus: [],
};


var themes = [
    { name: "白雪", value: ["#ffffff", "#000000"] },
    { name: "灰绿", value: ["#d8e7eb", "#000000"] },
    { name: "浅绿", value: ["#e9faff", "#000000"] },
    { name: "明黄", value: ["#ffffed", "#000000"] },
    { name: "淡绿", value: ["#eefaee", "#000000"] },
    { name: "草绿", value: ["#cce8cf", "#000000"] },
    { name: "红粉", value: ["#fcefff", "#000000"] },
    { name: "米黄", value: ["#f5f5dc", "#000000"] },
    { name: "茶色", value: ["#d2b48c", "#000000"] },
    { name: "银色", value: ["#c0c0c0", "#000000"] },
    { name: "浅黄", value: ["#f5f1e8", "#000000"] },
    { name: "浅灰", value: ["#d9e0e8", "#000000"] },
    { name: "午夜", value: ["#002b36", "#839496"] },
    { name: "墨水屏", value: ["#c0d3d7", "#111111"] },
    { name: "漆黑", value: ["#000000", "#555555"] },
];

/**
 * 
 * @param {string} color hex color
 * @param {number} factor 0-1
 * @returns css color
 */
function darkenColor(color, factor) {

    function hex2rgb(color) {
        hex = color.replace('#', '');
        let r = parseInt(hex.substr(0, 2), 16);
        let g = parseInt(hex.substr(2, 2), 16);
        let b = parseInt(hex.substr(4, 2), 16);
        return [r, g, b];
    }

    function lab2rgb(lab) {
        var y = (lab[0] + 16) / 116,
            x = lab[1] / 500 + y,
            z = y - lab[2] / 200,
            r, g, b;

        x = 0.95047 * ((x * x * x > 0.008856) ? x * x * x : (x - 16 / 116) / 7.787);
        y = 1.00000 * ((y * y * y > 0.008856) ? y * y * y : (y - 16 / 116) / 7.787);
        z = 1.08883 * ((z * z * z > 0.008856) ? z * z * z : (z - 16 / 116) / 7.787);

        r = x * 3.2406 + y * -1.5372 + z * -0.4986;
        g = x * -0.9689 + y * 1.8758 + z * 0.0415;
        b = x * 0.0557 + y * -0.2040 + z * 1.0570;

        r = (r > 0.0031308) ? (1.055 * Math.pow(r, 1 / 2.4) - 0.055) : 12.92 * r;
        g = (g > 0.0031308) ? (1.055 * Math.pow(g, 1 / 2.4) - 0.055) : 12.92 * g;
        b = (b > 0.0031308) ? (1.055 * Math.pow(b, 1 / 2.4) - 0.055) : 12.92 * b;

        return [Math.max(0, Math.min(1, r)) * 255,
        Math.max(0, Math.min(1, g)) * 255,
        Math.max(0, Math.min(1, b)) * 255]
    }


    function rgb2lab(rgb) {
        var r = rgb[0] / 255,
            g = rgb[1] / 255,
            b = rgb[2] / 255,
            x, y, z;

        r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
        g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
        b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;

        x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
        y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
        z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;

        x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116;
        y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + 16 / 116;
        z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + 16 / 116;

        return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)]
    }

    let lab = rgb2lab(hex2rgb(color));

    console.log("lab color", lab);

    lab[0] = Math.max(0, lab[0] - lab[0] * factor);

    let rgb = lab2rgb(lab);

    console.log("rgb color", rgb);

    return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
}


let wechatControl = {
    next: () => document.querySelector(".renderTarget_pager_button_right").click(),
    prev: () => document.querySelector(".renderTarget_pager_button:not(.renderTarget_pager_button_right)").click(),

    readerView: () => document.querySelector(".readerChapterContent"),
    readerWrap: () => document.querySelector(".readerChapterContent_container"),

    readerContent: () => document.querySelector(".wr_whiteTheme .readerChapterContent"),
    topBar: () => document.querySelector(".readerTopBar"),

    controls: () => document.querySelectorAll(".readerControls_item"),

    /** @type {number} */
    currentTheme: -1,
    /** @type {Node | null} */
    currentCss: null,

    updateStyle: (direction) => {
        let prevTheme = null
        if (wechatControl.currentTheme != -1) {
            prevTheme = themes[wechatControl.currentTheme]
        }
        wechatControl.currentTheme += direction
        if (wechatControl.currentTheme >= themes.length) wechatControl.currentTheme = 0

        let theme = themes[wechatControl.currentTheme].value
        wechatControl.setTheme(theme)
        wechatControl.saveTheme()
        const renderFontColor = () => {
            const resizeEvent = new Event('resize');
            window.dispatchEvent(resizeEvent);
        }

        if (!prevTheme || prevTheme.value[1] != theme[1]) {
            renderFontColor();
        }
    },

    dim: 0,
    darkMode: () => {
        if (wechatControl.currentTheme == -1) {
            wechatControl.currentTheme = 0
            wechatControl.saveTheme()
        }
        if (wechatControl.dim == 0) {
            wechatControl.dim = 0.3
        } else {
            wechatControl.dim = 0
        }
        wechatControl.updateStyle(0)
    },

    /**
     * 设置主题
     * @param {Array<string>} theme - 主题数组,包含背景色和字体颜色
     */
    setTheme: (theme) => {
        console.log("update theme")
        console.log(theme[0], theme[1])
        let color = theme[0]
        const dim = wechatControl.dim
        const backgroundColor = darkenColor(color, 0.1 + dim);

        if (dim != 0) {
            color = darkenColor(color, dim)
        }
        wechatControl.readerView().style.backgroundColor = color;


        let control_items = wechatControl.controls()
        for (let i = 0; i < control_items.length; i++) {
            control_items[i].style.backgroundColor = color;
        }

        wechatControl.readerWrap().style.backgroundColor = backgroundColor;
        wechatControl.topBar().style.backgroundColor = `#00000000`;

        if (wechatControl.currentCss) {
            wechatControl.currentCss.remove();
        }

        wechatControl.currentCss = GM_addStyle(`
            .wr_whiteTheme .readerChapterContent {
                color: ${theme[1]} !important;
            }
        ` );
    },

    saveTheme: () => {
        let theme = themes[wechatControl.currentTheme]
        GM_setValue("theme", theme.name)
    },

    loadTheme: () => {
        let themeName = GM_getValue("theme")
        if (themeName) {
            let theme
            let index = 0
            for (; index < themes.length; index++) {
                if (themes[index].name == themeName) {
                    theme = themes[index]
                    break
                }
            }
            wechatControl.currentTheme = index
            wechatControl.setTheme(theme.value)
        }
    },

    toggleFullScreen: () => {
        if (!document.fullscreenElement &&    // 标准方法
            !document.mozFullScreenElement && // Firefox
            !document.webkitFullscreenElement && // Chrome, Safari and Opera
            !document.msFullscreenElement) {  // IE/Edge

            // 请求全屏
            if (document.documentElement.requestFullscreen) {
                document.documentElement.requestFullscreen();
            } else if (document.documentElement.mozRequestFullScreen) { // Firefox
                document.documentElement.mozRequestFullScreen();
            } else if (document.documentElement.webkitRequestFullscreen) { // Chrome, Safari and Opera
                document.documentElement.webkitRequestFullscreen();
            } else if (document.documentElement.msRequestFullscreen) { // IE/Edge
                document.documentElement.msRequestFullscreen();
            }
        } else {
            // 退出全屏
            if (document.exitFullscreen) {
                document.exitFullscreen();
            } else if (document.mozCancelFullScreen) { // Firefox
                document.mozCancelFullScreen();
            } else if (document.webkitExitFullscreen) { // Chrome, Safari and Opera
                document.webkitExitFullscreen();
            } else if (document.msExitFullscreen) { // IE/Edge
                document.msExitFullscreen();
            }
        }
    },
    scroll: (direction) => {
        const hasVerticalScrollbar = document.documentElement.scrollHeight > document.documentElement.clientHeight;
        if (hasVerticalScrollbar) {
            window.scrollTo({
                //60 fps
                top: window.scrollY + ((300 / 60) * direction),
                behavior: 'instant'
            });
        }
    }
};

(function () {
    'use strict';

    wechatControl.loadTheme()

    window.addEventListener("gamepadconnected", (evt) => {
        console.log('控制器已连接。');



        gamepadAPI.job = setInterval(() => {
            gamepadAPI.update()
            // 明确调用 buttonPressed 并传递按钮名称
            if (gamepadAPI.buttonPressed('A').pressed) {
                wechatControl.next()
            }
            // if (gamepadAPI.buttonPressed('A', true).pressed) {
            //     console.log('A 按钮被持续按下');
            // }
            // if (gamepadAPI.buttonPressed('A', false, true).released) {
            //     console.log('A 按钮被放开');
            // }

            if (gamepadAPI.buttonPressed('B').pressed) {
                wechatControl.prev();
            }

            if (gamepadAPI.buttonPressed('Y').pressed) {
                wechatControl.toggleFullScreen();
            }

            if (gamepadAPI.buttonPressed('X').pressed) {
                wechatControl.darkMode()
            }

            if (gamepadAPI.buttonPressed('LB').pressed) {
                wechatControl.updateStyle(-1)
            }
            if (gamepadAPI.buttonPressed('RB').pressed) {
                wechatControl.updateStyle(1)
            }

            if (gamepadAPI.buttonPressed('DPad-Up').pressed) {
                wechatControl.prev();
            }
            if (gamepadAPI.buttonPressed('DPad-Up', true).pressed) {
                wechatControl.scroll(-1)
            }
            if (gamepadAPI.buttonPressed('DPad-Down').pressed) {
                wechatControl.next();
            }
            if (gamepadAPI.buttonPressed('DPad-Down', true).pressed) {
                wechatControl.scroll(1)
            }
            if (gamepadAPI.buttonPressed('DPad-Left').pressed) {
                wechatControl.prev();
            }
            if (gamepadAPI.buttonPressed('DPad-Right').pressed) {
                wechatControl.next();
            }
        }, 16)
    });

    window.addEventListener("gamepaddisconnected", (evt) => {
        console.log('控制器已断开。');
        clearInterval(gamepadAPI.job)
    });

})();