// ==UserScript==
// @name 扇贝单词手柄快捷键+深色模式
// @namespace https://github.com/AHPxLIS/shanbayController
// @version 0.6
// @icon https://static.baydn.com/static/img/shanbay_favicon.png
// @description 为扇贝单词网页添加深色模式支持以及手柄快捷键,可自定义按键映射
// @author AhpGFlis
// @match https://web.shanbay.com/*
// @run-at document-start
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function () {
/*
######深色模式部分######
*/
'use strict';
// 使用滤镜组合实现自然颜色反转
const css = `
html {
background-color: #FFF !important; /* 基础背景设为白色,反转后变黑 */
filter: invert(1) hue-rotate(180deg) !important;
min-height: 100vh;
}
/* 排除不需要反转的元素 */
img,
video,
iframe,
[class*="image"],
[class*="icon"],
[class*="logo"] {
filter: invert(1) hue-rotate(180deg) !important;
}
/* 专门处理输入框的placeholder */
.input::placeholder {
color: #000 !important;
opacity: 0.7 !important;
}
/* 自定义配置窗口样式 */
.gamepad-config-window {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0,0,0,0.3);
z-index: 9999;
color: #000;
max-width: 500px;
width: 90%;
}
.gamepad-config-window h3 {
margin-top: 0;
color: #333;
}
.gamepad-config-window label {
display: block;
margin: 10px 0;
}
.gamepad-config-window select,
.gamepad-config-window input {
margin-left: 10px;
padding: 5px;
border: 1px solid #ccc;
border-radius: 4px;
}
.gamepad-config-buttons {
margin-top: 20px;
display: flex;
justify-content: space-between;
}
.gamepad-config-buttons button {
padding: 8px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.gamepad-config-buttons .save-btn {
background-color: #4CAF50;
color: white;
}
.gamepad-config-buttons .close-btn {
background-color: #f44336;
color: white;
}
.gamepad-config-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 9998;
}
`;
// 尽早注入样式
GM_addStyle(css);
// 监听DOM加载完成,处理动态内容
window.addEventListener('DOMContentLoaded', () => {
// 添加过渡效果避免闪屏
document.documentElement.style.transition = 'filter 0.3s ease';
// 处理可能存在的内联样式冲突
document.querySelectorAll('[style*="background"]').forEach(el => {
el.style.setProperty('background-color', 'transparent', 'important');
});
});
console.log("扇贝单词深色模式已加载");
let isDarkMode = true;
// 禁用深色模式
function disableDarkMode() {
// 移除通过GM_addStyle添加的样式
const styleElements = document.querySelectorAll('style');
styleElements.forEach(style => {
if (style.textContent.includes('invert(1) hue-rotate(180deg)')) {
style.remove();
}
});
// 重置html元素的filter属性
document.documentElement.style.filter = '';
document.documentElement.style.transition = '';
// 重置所有被排除元素的filter属性
document.querySelectorAll('img, video, iframe, [class*="image"], [class*="icon"], [class*="logo"]').forEach(el => {
el.style.filter = '';
});
// 重置内联背景色
document.querySelectorAll('[style*="background"]').forEach(el => {
el.style.removeProperty('background-color');
});
isDarkMode = false;
console.log("扇贝单词深色模式已禁用");
}
/*
######手柄部分######
*/
// 默认手柄按键映射配置
const DEFAULT_KEY_MAPPING = {
0: "2", // A按钮 - 提示一下/没想起来
1: "1", // B按钮 - 认识/想起来了
2: "p", // X按钮 - 单词发音
3: "v", // Y按钮 - 例句发音
4: "9", // LB - 太简单
5: "d", // RB - 下个单词
12: "1", // 上方向键 - 选项1
13: "4", // 下方向键 - 选项4
14: "2", // 左方向键 - 选项2
15: "3" // 右方向键 - 选项3
};
// 扇贝官方快捷键说明
const SHANBAY_SHORTCUTS = [
{ desc: "认识、想起来了", key: "1" },
{ desc: "提示一下、没想起来、撤销", key: "2" },
{ desc: "选择对应选项3", key: "3" },
{ desc: "选择对应选项4", key: "4" },
{ desc: "太简单", key: "9" },
{ desc: "下个单词", key: "d" },
{ desc: "单词发音", key: "p" },
{ desc: "例句发音", key: "v" },
{ desc: "关闭对话框", key: "Escape" }
];
// 手柄按钮标签
const GAMEPAD_BUTTON_LABELS = {
0: "A 按钮 (PS X)",
1: "B 按钮 (PS ○)",
2: "X 按钮 (PS □)",
3: "Y 按钮 (PS △)",
4: "LB (L1)",
5: "RB (R1)",
6: "LT (L2)",
7: "RT (R2)",
8: "Back/Share",
9: "Start/Options",
12: "十字键 上",
13: "十字键 下",
14: "十字键 左",
15: "十字键 右"
};
// 添加样式
GM_addStyle(`
/* 浮动按钮样式 */
.gamepad-float-btn {
position: fixed;
bottom: 20px;
right: 20px;
width: 45px;
height: 45px;
border-radius: 6px;
background-color: #1c9a81;
color: white;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 9999;
user-select: none;
}
/* 配置窗口样式 */
.gamepad-config-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0,0,0,0.3);
z-index: 10000;
color: #333;
max-width: 800px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.gamepad-config-title {
margin-top: 0;
text-align: center;
color: #2c3e50;
}
.gamepad-config-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.gamepad-config-table th,
.gamepad-config-table td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
}
.gamepad-config-table th {
background-color: #f2f2f2;
}
.gamepad-button-cell {
cursor: pointer;
min-width: 120px;
text-align: center !important;
}
.gamepad-button-cell:hover {
background-color: #f0f8ff;
}
.gamepad-button-cell.waiting {
background-color: #fffacd;
font-weight: bold;
}
.gamepad-config-buttons {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.gamepad-config-btn {
padding: 8px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.gamepad-save-btn {
background-color: #4CAF50;
color: white;
}
.gamepad-disableDarkMode-btn {
background-color: #000000;
color: white;
}
.gamepad-reset-btn {
background-color: #f39c12;
color: white;
}
.gamepad-close-btn {
background-color: #e74c3c;
color: white;
}
.gamepad-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 9999;
}
.gamepad-notification {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #e74c3c;
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 10001;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`);
// 获取或初始化按键映射
function getKeyMapping() {
const savedMapping = GM_getValue('keyMapping');
return savedMapping ? JSON.parse(savedMapping) : { ...DEFAULT_KEY_MAPPING };
}
// 存储当前连接的手柄
let gamepad = null;
let buttonStates = [];
let isListeningForButton = false;
let currentListeningCell = null;
// 初始化手柄检测
function initGamepad() {
window.addEventListener("gamepadconnected", (e) => {
console.log("手柄已连接:", e.gamepad.id);
gamepad = e.gamepad;
buttonStates = new Array(gamepad.buttons.length).fill(false);
startListening();
});
window.addEventListener("gamepaddisconnected", (e) => {
console.log("手柄已断开");
gamepad = null;
});
}
// 开始监听手柄输入
function startListening() {
function checkGamepad() {
if (!gamepad) return;
const currentGamepad = navigator.getGamepads()[gamepad.index];
if (!currentGamepad) return;
// 检查按键监听模式
if (isListeningForButton) {
currentGamepad.buttons.forEach((button, index) => {
if (button.pressed && !buttonStates[index]) {
handleButtonPressedWhileListening(index);
}
});
} else {
// 正常按键功能模式
currentGamepad.buttons.forEach((button, index) => {
const pressed = button.pressed;
if (pressed && buttonStates[index] !== pressed) {
const keyMapping = getKeyMapping();
const keyboardKey = keyMapping[index];
if (keyboardKey) {
simulateKeyPress(keyboardKey);
}
}
buttonStates[index] = pressed;
});
}
buttonStates = currentGamepad.buttons.map(btn => btn.pressed);
requestAnimationFrame(checkGamepad);
}
requestAnimationFrame(checkGamepad);
}
// 模拟键盘按键
function simulateKeyPress(key) {
const keyEvent = new KeyboardEvent("keydown", {
key: key,
code: key.length === 1 ? `Key${key.toUpperCase()}` : key,
bubbles: true,
cancelable: true
});
document.activeElement.dispatchEvent(keyEvent);
}
// 创建浮动按钮
function createFloatingButton() {
const floatBtn = document.createElement('div');
floatBtn.className = 'gamepad-float-btn';
floatBtn.title = '配置手柄按键映射';
floatBtn.addEventListener('click', createConfigWindow);
// xbox手柄SVG
floatBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 128 128">
<g transform="translate(12, 12)">
<style>
path { fill: white !important; } /* 强制覆盖所有路径 */
</style>
<path
d="M34 15c2.373.098 4.75.13 7.125.133l2.13.003c1.486 0 2.972-.001 4.458-.006 2.278-.005 4.556 0 6.834.007 1.443 0 2.885-.002 4.328-.004l3.992-.004c3.038.088 3.038.088 5.133-1.129 6.254-.056 11.226.692 16 5 1.625 2.75 1.625 2.75 3 6l1.824 3.406c2.792 5.55 4.527 11.425 6.363 17.344l1.077 3.402c6.55 20.805 6.55 20.805 2.049 30.473-6.056 6.22-6.056 6.22-9.75 6.677-3.171-.374-3.99-1.186-6.122-3.513-.632-.669-1.266-1.336-1.919-2.024a1027.03 1027.03 0 00-1.958-2.14 387.923 387.923 0 00-3.87-4.15c-.566-.617-1.13-1.235-1.713-1.872-5.95-4.822-13.633-3.943-20.865-3.92-2.044.003-4.086-.02-6.129-.045-9.118-.006-14.888.131-21.556 6.715a423.698 423.698 0 00-5.356 5.815c-4.428 4.687-4.428 4.687-7.773 5.428-2.633-.684-3.978-1.524-5.987-3.348l-1.762-1.547c-4.238-4.655-4.084-9.383-3.995-15.46C.185 57.885 3.272 49.86 6 42l.825-2.392c.77-2.209 1.56-4.41 2.362-6.608l.72-1.976C11.214 27.589 12.688 24.848 15 22l1.188-2.562C19.958 14.367 28.21 12.104 34 15zM20 25c-3.414 3.754-5.209 7.53-6.797 12.285l-.723 2.112c-.5 1.472-.993 2.945-1.481 4.42a499.996 499.996 0 01-2.285 6.642C4.452 62.5 4.452 62.5 5 75c2.002 2.62 4.16 4.348 7 6l1.222-1.253a1825 1825 0 015.528-5.622l1.922-1.973 1.875-1.894 1.714-1.746c4.596-3.995 8.807-3.832 14.67-3.848l2.63-.02c1.834-.01 3.668-.015 5.502-.015 2.796-.005 5.591-.041 8.387-.08 1.784-.005 3.57-.01 5.355-.01l2.52-.044c6.313.049 9.663 1.875 14.458 6.06l1.428 1.46 1.583 1.601 1.582 1.633 1.652 1.68A1005.37 1005.37 0 0188 81c2.848-1.656 4.96-3.403 7-6 1.989-9.098-1.47-18.18-4.425-26.675a408.47 408.47 0 01-2.225-6.653c-.483-1.43-.968-2.86-1.455-4.29l-.651-1.979c-2.077-5.87-4.92-10.055-10.37-13.215C62.315 17.313 31.943 15.957 20 25z" />
<path
d="M41 46c2 1 2 1 3 3 .52 3.564.63 6.738-1 10-3.262 1.63-6.436 1.52-10 1-2-1-2-1-3-3-.52-3.564-.63-6.738 1-10 3.262-1.63 6.436-1.52 10-1zM59.875 43.734L62 43.75l2.125-.016C66 44 66 44 68 46c.266 1.875.266 1.875.25 4l.015 2.124C68 54 68 54 66 56c-1.875.265-1.875.265-4 .25l-2.125.014C58 56 58 56 56 54c-.266-1.875-.266-1.875-.25-4l-.015-2.126c.383-2.713 1.424-3.754 4.14-4.14zM23.875 29.734L26 29.75l2.125-.016C30 30 30 30 32 32c.266 1.875.266 1.875.25 4l.015 2.124C32 40 32 40 30 42c-1.875.265-1.875.265-4 .25l-2.125.014C22 42 22 42 20 40c-.266-1.875-.266-1.875-.25-4l-.015-2.126c.383-2.713 1.424-3.754 4.14-4.14zM50 21.875C53 22 53 22 54 23c.125 3 .125 3 0 6-1 1-1 1-4 1.125C47 30 47 30 46 29c-.125-3-.125-3 0-6 1-1 1-1 4-1.125zM70 38h6v6h-6v-6zM76 32h6v6h-6v-6zM64 32h6v6h-6v-6zM70 26h6v6h-6v-6zM46 38h8v4h-8v-4zM56 34h4v4h-4v-4zM40 34h4v4h-4v-4z" />
</g>
</svg>
`;
document.body.appendChild(floatBtn);
}
// 创建配置窗口
function createConfigWindow() {
// 创建遮罩层
const overlay = document.createElement('div');
overlay.className = 'gamepad-overlay';
// 创建配置窗口容器
const container = document.createElement('div');
container.className = 'gamepad-config-container';
// 添加标题
const title = document.createElement('h2');
title.className = 'gamepad-config-title';
title.textContent = '手柄按键映射配置';
container.appendChild(title);
// 创建配置表格
const table = document.createElement('table');
table.className = 'gamepad-config-table';
// 添加表头
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
const header1 = document.createElement('th');
header1.textContent = '功能说明';
const header2 = document.createElement('th');
header2.textContent = '键盘快捷键';
const header3 = document.createElement('th');
header3.textContent = '手柄按键映射';
headerRow.appendChild(header1);
headerRow.appendChild(header2);
headerRow.appendChild(header3);
thead.appendChild(headerRow);
table.appendChild(thead);
// 添加表格内容
const tbody = document.createElement('tbody');
const currentMapping = getKeyMapping();
// 获取反向映射(手柄按钮到功能)
const reverseMapping = {};
Object.keys(currentMapping).forEach(btnIndex => {
const key = currentMapping[btnIndex];
reverseMapping[key] = reverseMapping[key] || [];
reverseMapping[key].push(btnIndex);
});
SHANBAY_SHORTCUTS.forEach(shortcut => {
const row = document.createElement('tr');
// 功能说明列
const descCell = document.createElement('td');
descCell.textContent = shortcut.desc;
row.appendChild(descCell);
// 键盘快捷键列
const keyCell = document.createElement('td');
keyCell.textContent = shortcut.key;
row.appendChild(keyCell);
// 手柄按键映射列
const btnCell = document.createElement('td');
btnCell.className = 'gamepad-button-cell';
// 显示当前映射的手柄按钮
const mappedButtons = reverseMapping[shortcut.key] || [];
if (mappedButtons.length > 0) {
btnCell.textContent = mappedButtons.map(btnIndex =>
GAMEPAD_BUTTON_LABELS[btnIndex]
).join(', ');
} else {
btnCell.textContent = '点击设置';
}
// 添加点击事件监听
btnCell.addEventListener('click', function () {
if (!isListeningForButton) {
startListeningForButton(this, shortcut.key);
}
});
row.appendChild(btnCell);
tbody.appendChild(row);
});
table.appendChild(tbody);
container.appendChild(table);
// 添加操作按钮
const buttonsDiv = document.createElement('div');
buttonsDiv.className = 'gamepad-config-buttons';
// 保存按钮
const saveBtn = document.createElement('button');
saveBtn.className = 'gamepad-config-btn gamepad-save-btn';
saveBtn.textContent = '保存配置';
saveBtn.addEventListener('click', saveConfig);
buttonsDiv.appendChild(saveBtn);
// 临时关闭深色模式按钮
if(isDarkMode==true){
const disableDarkModeBtn = document.createElement('button');
disableDarkModeBtn.className = 'gamepad-config-btn gamepad-disableDarkMode-btn'
disableDarkModeBtn.textContent = '临时关闭深色模式';
disableDarkModeBtn.addEventListener('click', function() {
disableDarkMode();
this.remove(); // 移除按钮
});
buttonsDiv.appendChild(disableDarkModeBtn);
}
// 重置按钮
const resetBtn = document.createElement('button');
resetBtn.className = 'gamepad-config-btn gamepad-reset-btn';
resetBtn.textContent = '恢复默认';
resetBtn.addEventListener('click', resetConfig);
buttonsDiv.appendChild(resetBtn);
// 关闭按钮
const closeBtn = document.createElement('button');
closeBtn.className = 'gamepad-config-btn gamepad-close-btn';
closeBtn.textContent = '关闭';
closeBtn.addEventListener('click', function () {
document.body.removeChild(overlay);
document.body.removeChild(container);
});
buttonsDiv.appendChild(closeBtn);
container.appendChild(buttonsDiv);
// 添加到文档
document.body.appendChild(overlay);
document.body.appendChild(container);
// 保存配置函数
function saveConfig() {
const newMapping = {};
let hasMapping = false;
// 收集所有配置
tbody.querySelectorAll('tr').forEach(row => {
const key = row.querySelector('td:nth-child(2)').textContent;
const btnCell = row.querySelector('td:nth-child(3)');
const btnText = btnCell.textContent;
if (btnText !== '点击设置') {
Object.keys(GAMEPAD_BUTTON_LABELS).forEach(btnIndex => {
if (btnText.includes(GAMEPAD_BUTTON_LABELS[btnIndex])) {
newMapping[btnIndex] = key;
hasMapping = true;
}
});
}
});
if (hasMapping) {
GM_setValue('keyMapping', JSON.stringify(newMapping));
showNotification('配置已保存!');
} else {
showNotification('请至少设置一个手柄按键映射!');
}
}
// 重置配置函数
function resetConfig() {
GM_setValue('keyMapping', JSON.stringify(DEFAULT_KEY_MAPPING));
showNotification('已恢复默认配置,请重新打开配置窗口查看');
document.body.removeChild(overlay);
document.body.removeChild(container);
}
}
// 开始监听手柄按钮输入
function startListeningForButton(cell, key) {
if (!gamepad) {
showNotification('请先连接手柄');
return;
}
isListeningForButton = true;
currentListeningCell = cell;
cell.textContent = '请按下手柄按钮...';
cell.classList.add('waiting');
// 设置超时自动取消
setTimeout(() => {
if (isListeningForButton) {
stopListeningForButton();
showNotification('按键监听已超时取消');
}
}, 5000);
}
// 处理按键监听模式下的按钮按下事件
function handleButtonPressedWhileListening(buttonIndex) {
if (!isListeningForButton || !currentListeningCell) return;
// 检查按钮是否有效(0-15,排除10,11,16)
if (buttonIndex < 0 || buttonIndex > 15 || buttonIndex === 10 || buttonIndex === 11) {
showNotification('不支持映射此按钮,请选择其他按钮');
stopListeningForButton();
return;
}
// 更新单元格显示
currentListeningCell.textContent = GAMEPAD_BUTTON_LABELS[buttonIndex];
currentListeningCell.classList.remove('waiting');
// 重置监听状态
stopListeningForButton();
}
// 停止监听手柄按钮
function stopListeningForButton() {
isListeningForButton = false;
if (currentListeningCell) {
currentListeningCell.classList.remove('waiting');
}
currentListeningCell = null;
}
// 显示通知
function showNotification(message) {
const notification = document.createElement('div');
notification.className = 'gamepad-notification';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
document.body.removeChild(notification);
}, 3000);
}
// 初始化
function init() {
initGamepad();
createFloatingButton();
GM_registerMenuCommand('配置手柄按键映射', createConfigWindow);
console.log("扇贝单词手柄控制已加载");
}
// 等待DOM加载完成后初始化
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(init, 0);
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();