// ==UserScript==
// @name NTR ToolBox
// @namespace http://tampermonkey.net/
// @version v0.2.4-20250223
// @author TheNano(百合仙人)
// @description ToolBox for Novel Translate bot website
// @match https://books.fishhawk.top/*
// @match https://books1.fishhawk.top/*
// @grant GM_openInTab
// @icon https://raw.githubusercontent.com/LittleSurvival/NTRTools/refs/heads/main/icon.jpg
// @license All Rights Reserved
//
// ==/UserScript==
(async function () {
'use strict';
const style = document.createElement('style');
style.textContent = `
#ntr-panel {
position: fixed;
left: 20px;
top: 70px;
z-index: 9999;
background: #1E1E1E;
color: #BBB;
padding: 8px;
border-radius: 8px;
font-family: Arial, sans-serif;
width: 320px;
box-shadow: 2px 2px 12px rgba(0,0,0,0.5);
border: 1px solid #333;
}
.ntr-titlebar {
font-weight: bold;
padding: 10px;
cursor: move;
background: #292929;
border-radius: 6px;
color: #CCC;
}
.ntr-panel-body {
padding: 6px;
background: #232323;
border-radius: 4px;
}
.ntr-module-container {
margin-bottom: 12px;
border: 1px solid #444;
border-radius: 4px;
}
.ntr-module-header {
display: flex;
align-items: center;
justify-content: space-between;
background: #2E2E2E;
padding: 6px 8px;
border-radius: 3px 3px 0 0;
border-bottom: 1px solid #333;
cursor: pointer;
transition: background 0.3s;
}
.ntr-module-header:hover {
background: #3a3a3a;
}
.ntr-settings-container {
padding: 6px;
background: #1C1C1C;
display: none;
}
.ntr-input {
width: 120px;
padding: 4px;
border: 1px solid #555;
border-radius: 4px;
background: #2A2A2A;
color: #FFF;
}
.ntr-number-input {
width: 60px;
padding: 4px;
border: 1px solid #555;
border-radius: 4px;
background: #2A2A2A;
color: #FFF;
}
.ntr-bind-button {
padding: 4px 8px;
border: 1px solid #555;
border-radius: 4px;
background: #2A2A2A;
color: #FFF;
cursor: pointer;
}
.ntr-info {
text-align: center;
font-size: 10px;
color: #888;
margin-top: 8px;
}
.ntr-module-header.active {
background: #63E2B7 !important;
color: #fff !important;
}
`;
document.head.appendChild(style);
function newBooleanSetting(nameDefault, boolDefault) {
return { name: nameDefault, type: 'boolean', value: Boolean(boolDefault) };
}
function newNumberSetting(nameDefault, numDefault) {
return { name: nameDefault, type: 'number', value: Number(numDefault || 0) };
}
function newStringSetting(nameDefault, strDefault) {
return { name: nameDefault, type: 'string', value: String(strDefault == null ? '' : strDefault) };
}
function newSelectSetting(nameDefault, arrOptions, valDefault) {
return { name: nameDefault, type: 'select', value: valDefault, options: arrOptions };
}
function getModuleSetting(moduleObj, key) {
if (!moduleObj.settings) return;
const found = moduleObj.settings.find(x => x.name === key);
return found ? found.value : undefined;
}
const CONFIG_VERSION = 10;
const CONFIG_STORAGE_KEY = 'NTR_ToolBox_Config';
const domainAllowed = ['books.fishhawk.top', 'books1.fishhawk.top'].includes(location.hostname);
const moduleAddSakuraTranslator = {
name: '添加Sakura翻譯器',
type: 'onclick',
whitelist: '/workspace/sakura',
settings: [
newNumberSetting('數量', 5),
newNumberSetting('延遲', 5),
newStringSetting('名稱', 'NTR translator '),
newStringSetting('鏈接', 'https://sakura-share.one'),
newStringSetting('bind', 'none'),
],
run: async function (configObj) {
const totalCount = getModuleSetting(configObj, '數量') || 1;
const namePrefix = getModuleSetting(configObj, '名稱') || '';
const linkValue = getModuleSetting(configObj, '鏈接') || '';
const delayValue = getModuleSetting(configObj, '延遲') || 5;
const delay = ms => new Promise(r => setTimeout(r, ms));
let currentIndex = 1;
async function closeTab() {
const closeButton = document.querySelector(
'button[aria-label="close"].n-base-close,button.n-base-close[aria-label="close"],button.n-base-close.n-base-close--absolute.n-card-header__close'
);
if (closeButton) closeButton.click();
}
async function openAddTab() {
const addBtn = [...document.querySelectorAll('button.n-button')]
.find(btn => {
const txt = (btn.querySelector('.n-button__content') || {}).textContent || '';
return txt.trim().indexOf('添加翻译器') !== -1;
});
if (addBtn) addBtn.click();
}
async function fillForm() {
const nameInput = document.querySelector('input.n-input__input-el[placeholder="给你的翻译器起个名字"]');
const linkInput = document.querySelector('input.n-input__input-el[placeholder="翻译器的链接"]');
const segInput = document.querySelectorAll('input.n-input__input-el[placeholder="请输入"]')[0];
const preInput = document.querySelectorAll('input.n-input__input-el[placeholder="请输入"]')[1];
const addBtn = [...document.querySelectorAll('button.n-button.n-button--primary-type')]
.find(btn => {
const txt = (btn.querySelector('.n-button__content') || {}).textContent || '';
return txt.trim().indexOf('添加') !== -1;
});
if (nameInput && linkInput && segInput && preInput && addBtn) {
nameInput.value = namePrefix + currentIndex;
nameInput.dispatchEvent(new Event('input', { bubbles: true }));
linkInput.value = linkValue;
linkInput.dispatchEvent(new Event('input', { bubbles: true }));
segInput.dispatchEvent(new InputEvent('input', { data: '500' }));
preInput.dispatchEvent(new InputEvent('input', { data: '500' }));
addBtn.click();
currentIndex++;
if (currentIndex <= totalCount) {
await delay(delayValue);
await fillForm();
}
}
}
await openAddTab();
await delay(300);
await fillForm();
await delay(100);
await closeTab();
}
};
const moduleAddGPTTranslator = {
name: '添加GPT翻譯器',
type: 'onclick',
whitelist: '/workspace/gpt',
settings: [
newNumberSetting('數量', 5),
newNumberSetting('延遲', 5),
newStringSetting('名稱', 'NTR translator '),
newStringSetting('模型', 'deepseek-chat'),
newStringSetting('鏈接', 'https://api.deepseek.com'),
newStringSetting('Key', 'sk-wait-for-input'),
newStringSetting('bind', 'none'),
],
run: async function (configObj) {
const countVal = getModuleSetting(configObj, '數量') || 1;
const namePrefixVal = getModuleSetting(configObj, '名稱') || '';
const modelVal = getModuleSetting(configObj, '模型') || '';
const apiKeyVal = getModuleSetting(configObj, 'Key') || '';
const apiUrlVal = getModuleSetting(configObj, '鏈接') || '';
const delayValue = getModuleSetting(configObj, '延遲') || 5;
const delay = ms => new Promise(r => setTimeout(r, ms));
let currentIndex = 1;
async function closeTab() {
const cBtn = document.querySelector(
'button[aria-label="close"].n-base-close,button.n-base-close[aria-label="close"],button.n-base-close.n-base-close--absolute.n-card-header__close'
);
if (cBtn) cBtn.click();
}
async function openAddTab() {
const addBtn = [...document.querySelectorAll('button.n-button')]
.find(btn => {
const txt = (btn.querySelector('.n-button__content') || {}).textContent || '';
return txt.trim().indexOf('添加翻译器') !== -1;
});
if (addBtn) addBtn.click();
}
async function fillForm() {
const nameInput = document.querySelector('input.n-input__input-el[placeholder="给你的翻译器起个名字"]');
const modelInput = document.querySelector('input.n-input__input-el[placeholder="模型名称"]');
const urlInput = document.querySelector('input.n-input__input-el[placeholder="兼容OpenAI的API链接,默认使用deepseek"]');
const keyInput = document.querySelector('input.n-input__input-el[placeholder="请输入Api key"]');
const confirmBtn = [...document.querySelectorAll('button.n-button.n-button--primary-type')]
.find(btn => {
const txt = (btn.querySelector('.n-button__content') || {}).textContent || '';
return txt.trim().indexOf('添加') !== -1;
});
if (nameInput && modelInput && urlInput && keyInput && confirmBtn) {
nameInput.value = namePrefixVal + currentIndex;
nameInput.dispatchEvent(new Event('input', { bubbles: true }));
modelInput.value = modelVal;
modelInput.dispatchEvent(new Event('input', { bubbles: true }));
urlInput.value = apiUrlVal;
urlInput.dispatchEvent(new Event('input', { bubbles: true }));
keyInput.value = apiKeyVal;
keyInput.dispatchEvent(new Event('input', { bubbles: true }));
confirmBtn.click();
currentIndex++;
if (currentIndex <= countVal) {
await delay(delayValue);
await fillForm();
}
}
}
await openAddTab();
await delay(300);
await fillForm();
await delay(100);
await closeTab();
}
};
const moduleDeleteTranslator = {
name: '刪除翻譯器',
type: 'onclick',
whitelist: '/workspace',
settings: [
newStringSetting('排除', '共享,本机,AutoDL'),
newStringSetting('bind', 'none'),
],
run: async function (configObj) {
const excludeStr = getModuleSetting(configObj, '排除') || '';
const excludeArr = excludeStr.split(',').filter(x => x);
const listItems = document.querySelectorAll('.n-list-item');
[...listItems].forEach(li => {
const titleEl = li.querySelector('.n-thing-header__title');
if (!titleEl) return;
const titleText = titleEl.textContent.trim();
const keep = excludeArr.some(x => titleText.indexOf(x) !== -1);
if (!keep) {
const delBtn = li.querySelector('.n-button--error-type');
const parentEl = delBtn?.parentElement;
if (parentEl) {
const siblingBtns = parentEl.querySelectorAll('button');
if (siblingBtns.length === 5 && delBtn) delBtn.click();
}
}
});
}
};
const moduleLaunchTranslator = {
name: '啟動翻譯器',
type: 'onclick',
whitelist: '/workspace',
settings: [
newNumberSetting('延遲間隔', 50),
newStringSetting('bind', 'none'),
],
run: async function (configObj) {
const intervalVal = getModuleSetting(configObj, '延遲間隔') || 50;
const allBtns = document.querySelectorAll('button');
const delay = ms => new Promise(r => setTimeout(r, ms));
let idx = 0;
async function nextClick() {
while (idx < allBtns.length) {
const btn = allBtns[idx];
idx++;
if (btn.textContent.indexOf('启动') !== -1 || btn.textContent.indexOf('啟動') !== -1) {
btn.click();
await delay(intervalVal);
}
}
}
await nextClick();
}
};
const moduleQueueSakura = {
name: '排隊Sakura',
type: 'onclick',
whitelist: '/wenku',
settings: [
newSelectSetting('模式', ['常規', '過期', '重翻'], '常規'),
newNumberSetting('延遲間隔', 50),
newNumberSetting('並行延遲', 1000),
newNumberSetting('並行數量', 5),
newStringSetting('bind', 'none'),
],
run: async function(configObj) {
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const pollInterval = getModuleSetting(configObj, '並行延遲') || 300;
const concurrentLimit = getModuleSetting(configObj, '並行數量') || 5;
const mode = getModuleSetting(configObj, '模式');
const modeMap = { '常規': '常规', '過期': '过期', '重翻': '重翻' };
const cnMode = modeMap[mode] || '常规';
function setMode(doc) {
const tags = doc.querySelectorAll('.n-tag__content');
for (const tag of tags) {
if (tag.textContent.trim() === cnMode) {
tag.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
break;
}
}
}
async function clickSakuraButtons(doc) {
const btns = Array.from(doc.querySelectorAll('button')).filter(b => b.textContent.includes('排队Sakura'));
btns.forEach(btn => btn.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true})));
}
if (location.pathname !== '/wenku') {
setMode(document);
await clickSakuraButtons(document);
return;
}
const domain = window.location.origin;
const allLinks = Array.from(document.querySelectorAll('a[href]'))
.map(a => a.href)
.filter(href => href.startsWith(domain) && /\/wenku\/[^/]+/.test(href));
const uniqueLinks = [...new Set(allLinks)];
async function waitForTabLoad(newTab) {
const maxWait = 10000;
const startTime = Date.now();
while (true) {
await delay(pollInterval);
if (!newTab || newTab.closed) {
throw new Error('New tab was closed or blocked before loading.');
}
if (newTab.document && (newTab.document.readyState === 'complete' || newTab.document.querySelector('.n-tag__content'))) {
break;
}
if (Date.now() - startTime > maxWait) {
throw new Error('Timed out waiting for new tab to load.');
}
}
}
async function processUrl(url) {
const newTab = window.open(url, '_blank');
if (!newTab) {
throw new Error(`Failed to open new tab for: ${url}`);
}
await waitForTabLoad(newTab);
setMode(newTab.document);
await clickSakuraButtons(newTab.document);
newTab.close();
}
let activeCount = 0;
let index = 0;
async function spawnNext() {
if (index >= uniqueLinks.length) return;
const url = uniqueLinks[index++];
activeCount++;
try {
await processUrl(url);
} catch (err) {
console.error('Failed to process:', url, err);
} finally {
activeCount--;
}
}
while (index < uniqueLinks.length) {
if (activeCount < concurrentLimit) {
spawnNext();
} else {
await delay(50);
}
}
while (activeCount > 0) {
await delay(50);
}
console.log('All Sakura tasks complete.');
}
};
const moduleQueueGPT = {
name: '排隊GPT',
type: 'onclick',
whitelist: '/wenku',
settings: [
newSelectSetting('模式', ['常規', '過期', '重翻'], '常規'),
newNumberSetting('延遲間隔', 5),
newNumberSetting('並行延遲', 300),
newNumberSetting('並行數量', 5),
newStringSetting('bind', 'none'),
],
run: async function(configObj) {
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const pollInterval = getModuleSetting(configObj, '並行延遲') || 300;
const concurrentLimit = getModuleSetting(configObj, '並行數量') || 5;
const mode = getModuleSetting(configObj, '模式');
const modeMap = { '常規': '常规', '過期': '过期', '重翻': '重翻' };
const cnMode = modeMap[mode] || '常规';
function setMode(doc) {
const tags = doc.querySelectorAll('.n-tag__content');
for (const tag of tags) {
if (tag.textContent.trim() === cnMode) {
tag.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
break;
}
}
}
async function clickGPTButtons(doc) {
const btns = Array.from(doc.querySelectorAll('button')).filter(b => b.textContent.includes('排队GPT'));
btns.forEach(btn => btn.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true})));
}
if (location.pathname !== '/wenku') {
setMode(document);
await clickGPTButtons(document);
return;
}
const domain = window.location.origin;
const allLinks = Array.from(document.querySelectorAll('a[href]'))
.map(a => a.href)
.filter(href => href.startsWith(domain) && /\/wenku\/[^/]+/.test(href));
const uniqueLinks = [...new Set(allLinks)];
async function waitForTabLoad(newTab) {
const maxWait = 10000;
const startTime = Date.now();
while (true) {
await delay(pollInterval);
if (!newTab || newTab.closed) {
throw new Error('New tab was closed or blocked before loading.');
}
if (newTab.document && (newTab.document.readyState === 'complete' || newTab.document.querySelector('.n-tag__content'))) {
break;
}
if (Date.now() - startTime > maxWait) {
throw new Error('Timed out waiting for new tab to load.');
}
}
}
async function processUrl(url) {
const newTab = window.open(url, '_blank');
if (!newTab) {
throw new Error(`Failed to open new tab for: ${url}`);
}
await waitForTabLoad(newTab);
setMode(newTab.document);
await clickGPTButtons(newTab.document);
newTab.close();
}
let activeCount = 0;
let index = 0;
async function spawnNext() {
if (index >= uniqueLinks.length) return;
const url = uniqueLinks[index++];
activeCount++;
try {
await processUrl(url);
} catch (err) {
console.error('Failed to process:', url, err);
} finally {
activeCount--;
}
}
while (index < uniqueLinks.length) {
if (activeCount < concurrentLimit) {
spawnNext();
} else {
await delay(50);
}
}
while (activeCount > 0) {
await delay(50);
}
console.log('All GPT tasks complete.');
}
};
const moduleAutoRetry = {
name: '自動重試',
type: 'keep',
whitelist: '/workspace/*',
settings: [
newNumberSetting('最大重試次數', 3),
newStringSetting('bind', 'none'),
],
_keepIntervalId: null,
_keepActive: false,
_attempts: 0,
run: function (configObj) {
if (!this._keepActive) {
this._keepActive = true;
const maxAttempts = getModuleSetting(configObj, '最大重試次數');
document.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
this._attempts = 0;
}
});
this._keepIntervalId = setInterval(() => {
const listItems = document.querySelectorAll('.n-list-item');
const unfinishedItems = Array.from(listItems).filter(item => {
const descriptionText = item.querySelector('.n-thing-main__description');
return descriptionText && descriptionText.textContent.includes('未完成');
});
if (unfinishedItems.length > 0 && this._attempts < maxAttempts) {
const hasStopButton = Array.from(document.querySelectorAll('button')).some(btn =>
btn.textContent === '停止'
);
if (!hasStopButton) {
const retryButtons = Array.from(document.querySelectorAll('button')).filter(btn =>
btn.textContent.includes('重试未完成任务')
);
const clickCount = Math.min(unfinishedItems.length, listItems.length);
if (retryButtons[0]) {
for (let i = 0; i < clickCount; i++) {
retryButtons[0].click();
}
this._attempts++;
}
}
}
}, 1000);
} else {
this._keepActive = false;
if (this._keepIntervalId) {
clearInterval(this._keepIntervalId);
this._keepIntervalId = null;
}
}
}
};
const defaultModules = [
moduleAddSakuraTranslator,
moduleAddGPTTranslator,
moduleDeleteTranslator,
moduleLaunchTranslator,
moduleQueueSakura,
moduleQueueGPT,
moduleAutoRetry,
];
function loadConfiguration() {
let tempStorage;
try {
tempStorage = JSON.parse(localStorage.getItem(CONFIG_STORAGE_KEY));
} catch (e) { }
if (!tempStorage || tempStorage.version !== CONFIG_VERSION) {
return { version: CONFIG_VERSION, modules: JSON.parse(JSON.stringify(defaultModules)) };
}
return tempStorage;
}
function saveConfiguration(obj) {
localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(obj));
}
let configuration = loadConfiguration();
if (configuration.modules.length !== defaultModules.length) {
configuration = { version: CONFIG_VERSION, modules: JSON.parse(JSON.stringify(defaultModules)) };
saveConfiguration(configuration);
} else {
const defaultModuleNames = defaultModules.map(x => x.name).sort().join(',');
const storedModuleNames = configuration.modules.map(x => x.name).sort().join(',');
if (defaultModuleNames !== storedModuleNames) {
configuration = { version: CONFIG_VERSION, modules: JSON.parse(JSON.stringify(defaultModules)) };
saveConfiguration(configuration);
}
}
configuration.modules.forEach(moduleObj => {
const foundDefaultModule = defaultModules.find(x => x.name === moduleObj.name);
if (foundDefaultModule && typeof foundDefaultModule.run === 'function') {
for (const prop in foundDefaultModule) {
if (!moduleObj.hasOwnProperty(prop)) {
moduleObj[prop] = foundDefaultModule[prop];
}
}
moduleObj.run = foundDefaultModule.run;
}
});
const keepIntervals = new Map();
const keepActiveSet = new Set();
function startKeepModule(modItem, modHeader) {
if (keepIntervals.has(modItem.name)) return;
modHeader.classList.add('active');
keepActiveSet.add(modItem.name);
const intervalId = setInterval(() => {
if (typeof modItem.run === 'function') {
modItem.run(modItem);
console.log('[Keep Module] ' + modItem.name + ' is running...');
}
}, 2000);
keepIntervals.set(modItem.name, intervalId);
}
function stopKeepModule(modItem, modHeader) {
const intervalId = keepIntervals.get(modItem.name);
if (intervalId) {
clearInterval(intervalId);
keepIntervals.delete(modItem.name);
}
modHeader.classList.remove('active');
keepActiveSet.delete(modItem.name);
}
document.addEventListener('keydown', keyEvent => {
if (keyEvent.ctrlKey || keyEvent.altKey || keyEvent.metaKey) return;
const pressedKey = keyEvent.key.toLowerCase();
for (const modItem of configuration.modules) {
const bindVal = getModuleSetting(modItem, 'bind');
if (!bindVal || bindVal === 'none') continue;
if (bindVal.toLowerCase() === pressedKey) {
if (!isModuleEnabledByWhitelist(modItem)) continue;
keyEvent.preventDefault();
handleModuleClick(modItem, null);
}
}
});
const panel = document.createElement('div');
panel.id = 'ntr-panel';
const savedPosition = localStorage.getItem('ntr-panel-position');
if (savedPosition) {
try {
const parsedPosition = JSON.parse(savedPosition);
if (parsedPosition.left && parsedPosition.top) {
panel.style.left = parsedPosition.left;
panel.style.top = parsedPosition.top;
}
} catch (e) { }
}
let dragging = false, dragOffsetX = 0, dragOffsetY = 0;
function mouseDownHandler(e) {
dragging = true;
dragOffsetX = e.clientX - panel.offsetLeft;
dragOffsetY = e.clientY - panel.offsetTop;
e.preventDefault();
}
function mouseMoveHandler(e) {
if (!dragging) return;
panel.style.left = (e.clientX - dragOffsetX) + 'px';
panel.style.top = (e.clientY - dragOffsetY) + 'px';
}
function mouseUpHandler() {
dragging = false;
localStorage.setItem('ntr-panel-position', JSON.stringify({
left: panel.style.left,
top: panel.style.top
}));
}
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
const titleBar = document.createElement('div');
titleBar.className = 'ntr-titlebar';
titleBar.textContent = 'NTR ToolBox';
titleBar.addEventListener('mousedown', mouseDownHandler);
panel.appendChild(titleBar);
const panelBody = document.createElement('div');
panelBody.className = 'ntr-panel-body';
panel.appendChild(panelBody);
function isModuleEnabledByWhitelist(modItem) {
if (!modItem.whitelist || modItem.whitelist.trim() === '') return domainAllowed;
const parts = modItem.whitelist.split(',').map(s => s.trim()).filter(Boolean);
return domainAllowed && parts.some(p => {
if (p.endsWith('/*')) {
const basePath = p.slice(0, -2);
return location.pathname.startsWith(basePath + '/');
}
return location.pathname.includes(p);
});
}
function handleModuleClick(modItem, modHeader) {
if (!domainAllowed || !isModuleEnabledByWhitelist(modItem)) return;
if (modItem.type === 'onclick') {
if (typeof modItem.run === 'function') {
modItem.run(modItem);
}
} else if (modItem.type === 'keep') {
const isActive = keepActiveSet.has(modItem.name);
if (isActive) {
if (modHeader) stopKeepModule(modItem, modHeader);
} else {
if (modHeader) startKeepModule(modItem, modHeader);
}
}
}
const headerMap = new Map();
configuration.modules.forEach(modItem => {
const moduleContainer = document.createElement('div');
moduleContainer.className = 'ntr-module-container';
const moduleHeader = document.createElement('div');
moduleHeader.className = 'ntr-module-header';
const nameSpan = document.createElement('span');
nameSpan.textContent = modItem.name;
moduleHeader.appendChild(nameSpan);
const iconSpan = document.createElement('span');
iconSpan.textContent = modItem.type === 'keep' ? '⇋' : '▶';
iconSpan.style.marginLeft = '8px';
moduleHeader.appendChild(iconSpan);
const settingsContainer = document.createElement('div');
settingsContainer.className = 'ntr-settings-container';
settingsContainer.style.display = 'none';
moduleHeader.oncontextmenu = function (e) {
e.preventDefault();
const currentDisplay = settingsContainer.style.display || window.getComputedStyle(settingsContainer).display;
settingsContainer.style.display = currentDisplay === 'none' ? 'block' : 'none';
};
moduleHeader.onclick = function (e) {
if (e.button === 0 && !e.ctrlKey && !e.altKey && !e.shiftKey) {
if (e.target.classList.contains('ntr-bind-button')) return;
handleModuleClick(modItem, moduleHeader);
}
};
if (modItem.type === 'keep' && keepActiveSet.has(modItem.name)) {
moduleHeader.classList.add('active');
}
if (Array.isArray(modItem.settings)) {
modItem.settings.forEach(setObj => {
const row = document.createElement('div');
row.style.marginBottom = '8px';
const label = document.createElement('label');
label.style.display = 'inline-block';
label.style.minWidth = '70px';
label.style.color = '#ccc';
label.textContent = setObj.name + ': ';
row.appendChild(label);
let inputElement;
if (setObj.type === 'boolean') {
inputElement = document.createElement('input');
inputElement.type = 'checkbox';
inputElement.checked = Boolean(setObj.value);
inputElement.onchange = function () {
setObj.value = inputElement.checked;
saveConfiguration(configuration);
};
} else if (setObj.type === 'number') {
inputElement = document.createElement('input');
inputElement.type = 'number';
inputElement.value = setObj.value;
inputElement.className = 'ntr-number-input';
inputElement.onchange = function () {
setObj.value = Number(inputElement.value) || 0;
saveConfiguration(configuration);
};
} else if (setObj.type === 'string' && setObj.name === 'bind') {
inputElement = document.createElement('button');
inputElement.className = 'ntr-bind-button';
inputElement.textContent = setObj.value === 'none' ? '(None)' : '[' + setObj.value.toUpperCase() + ']';
inputElement.onclick = function () {
inputElement.textContent = '(Press any key)';
function handleKey(keyEvent) {
keyEvent.preventDefault();
if (keyEvent.key === 'Escape') {
setObj.value = 'none';
inputElement.textContent = '(None)';
saveConfiguration(configuration);
document.removeEventListener('keydown', handleKey, true);
keyEvent.stopPropagation();
return;
}
const pressedKey = keyEvent.key.toLowerCase();
setObj.value = pressedKey;
inputElement.textContent = '[' + pressedKey.toUpperCase() + ']';
saveConfiguration(configuration);
document.removeEventListener('keydown', handleKey, true);
keyEvent.stopPropagation();
}
document.addEventListener('keydown', handleKey, true);
};
} else if (setObj.type === 'select' && Array.isArray(setObj.options)) {
inputElement = document.createElement('select');
setObj.options.forEach(opt => {
const optionEl = document.createElement('option');
optionEl.value = opt;
optionEl.textContent = opt;
if (opt === setObj.value) {
optionEl.selected = true;
}
inputElement.appendChild(optionEl);
});
inputElement.onchange = function () {
setObj.value = inputElement.value;
saveConfiguration(configuration);
};
} else if (setObj.type === 'string') {
inputElement = document.createElement('input');
inputElement.type = 'text';
inputElement.value = setObj.value;
inputElement.className = 'ntr-input';
inputElement.onchange = function () {
setObj.value = inputElement.value;
saveConfiguration(configuration);
};
} else {
inputElement = document.createElement('span');
inputElement.style.color = '#999';
inputElement.textContent = String(setObj.value);
}
row.appendChild(inputElement);
settingsContainer.appendChild(row);
});
}
moduleContainer.appendChild(moduleHeader);
moduleContainer.appendChild(settingsContainer);
panelBody.appendChild(moduleContainer);
headerMap.set(modItem, moduleHeader);
});
const infoText = document.createElement('div');
infoText.className = 'ntr-info';
infoText.textContent = '左鍵執行/切換 | 右鍵設定';
panel.appendChild(infoText);
document.body.appendChild(panel);
setInterval(() => {
configuration.modules.forEach(m => {
const moduleHeader = headerMap.get(m);
if (!moduleHeader) return;
const moduleContainer = moduleHeader.parentElement;
const allow = domainAllowed && isModuleEnabledByWhitelist(m);
if (!allow) {
moduleContainer.style.display = 'none';
if (m.type === 'keep' && keepActiveSet.has(m.name)) stopKeepModule(m, moduleHeader);
} else {
moduleContainer.style.display = 'block';
}
});
}, 1000);
})();