使用原生 JS 批量打開連結,保存進度。原本的 vue 版會被 CSP 擋。
// ==UserScript==
// @name 批量打開 reddit 的 r/udemyfreeebies 連結(原生JS版)
// @namespace http://tampermonkey.net/
// @version 0.14
// @description 使用原生 JS 批量打開連結,保存進度。原本的 vue 版會被 CSP 擋。
// @license GPL-3.0
// @match https://www.reddit.com/r/udemyfreeebies/*
// @grant GM_registerMenuCommand
// @author twozwu
// ==/UserScript==
let links = [];
let currentIndex = 0;
let perClick = parseInt(localStorage.getItem('defaultPerClick')) || 15;
let indexKey = parseInt(localStorage.getItem('indexKey')) || 0;
let controlVisible = false;
let statusText = '';
function createPanel() {
if (document.getElementById('bulk-opener-panel')) return;
const panel = document.createElement('div');
panel.id = 'bulk-opener-panel';
panel.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
background: #fff;
border: 2px solid #ccc;
border-radius: 12px;
padding: 15px;
z-index: 9999;
font-family: sans-serif;
width: 260px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
`;
panel.innerHTML = `
<h3 style="margin-top: 0;">🔗 批量開啟設定</h3>
<div>
<label>每批數量:
<input type="number" id="perClickInput" value="${perClick}" min="1" style="width:4rem;">
</label>
</div>
<div style="margin-top: 6px;">
<label>從第
<input type="number" id="indexKeyInput" value="${indexKey}" style="width:auto;">
個開始
</label>
<small style="display:block;color:#666;">(預設為上次進度)</small>
</div>
<div style="margin-top:10px;">
<button id="runBtn">🚀 開始</button>
<button id="closeBtn" style="float:right;">❌</button>
</div>
<div id="controlDiv" style="margin-top:15px; display:none;">
<p id="statusText">${statusText}</p>
<button id="continueBtn">▶️ 繼續</button>
<button id="stopBtn">🛑 停止</button>
</div>
`;
document.body.appendChild(panel);
// 綁定事件
document.getElementById('closeBtn').addEventListener('click', () => {
panel.remove();
});
document.getElementById('runBtn').addEventListener('click', runOpener);
document.getElementById('continueBtn').addEventListener('click', openBatch);
document.getElementById('stopBtn').addEventListener('click', stopBatch);
}
function gatherLinks() {
links = [];
document.querySelectorAll('.text-neutral-content ul li a').forEach(a => {
if (a.href) links.push(a.href);
});
if (links.length === 0) {
alert("⚠️ 找不到連結!");
}
}
function updateStatus() {
const controlDiv = document.getElementById('controlDiv');
const statusP = document.getElementById('statusText');
if (currentIndex >= links.length) {
alert("✅ 所有連結已打開完畢!");
controlDiv.style.display = 'none';
indexKey = 0;
localStorage.removeItem('indexKey');
} else {
statusP.textContent = `已打開 ${currentIndex} / ${links.length},繼續下一批?`;
controlDiv.style.display = 'block';
}
}
function openBatch() {
const perClickInput = document.getElementById('perClickInput');
perClick = parseInt(perClickInput.value) || 15;
const end = Math.min(currentIndex + perClick, links.length);
for (let i = currentIndex; i < end; i++) {
const win = window.open(links[i], "_blank", "noopener,noreferrer");
if (win) {
win.blur();
window.focus();
}
}
currentIndex = end;
indexKey = end;
localStorage.setItem('indexKey', end.toString());
updateStatus();
}
function runOpener() {
const perClickInput = document.getElementById('perClickInput');
const indexKeyInput = document.getElementById('indexKeyInput');
perClick = parseInt(perClickInput.value) || 15;
indexKey = parseInt(indexKeyInput.value) || 0;
localStorage.setItem('defaultPerClick', perClick.toString());
currentIndex = indexKey;
gatherLinks();
controlVisible = true;
openBatch();
}
function stopBatch() {
alert("⏹️ 已停止,進度已保存");
}
// GM 菜單命令
GM_registerMenuCommand("📂 開啟連結設定面板", createPanel);