Weread gamepad control

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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)
    });

})();