Note 03/11/2026 17:07:47
// ==UserScript==
// @name XF Find Next Comment
// @namespace http://tampermonkey.net/
// @version 1.4
// @description Thêm nút tìm bình luận tiếp theo + comment có ảnh/đính kèm + auto browse (XenForo: xamvn, rphang)
// @author Alex
// @include https://xamvn.tld/threads/*
// @include https://rphang.tld/t/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
console.log('[XF-Find-Next] Script đang chạy...');
// CSS cho nút action bar
const style = document.createElement('style');
style.textContent = `
.actionBar-action--findNext,
.actionBar-action--findAttach,
.actionBar-action--autoBrowse {
font-weight: 600;
color: inherit;
}
.actionBar-action--findNext:hover,
.actionBar-action--findAttach:hover,
.actionBar-action--autoBrowse:hover {
text-decoration: underline !important;
}
.actionBar-action--findNext.searching,
.actionBar-action--findAttach.searching {
color: #d97706 !important;
cursor: wait;
}
.actionBar-action--autoBrowse.active {
color: #16a34a !important;
animation: autoPulse 1.5s ease-in-out infinite;
}
@keyframes autoPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Floating auto-browse panel */
#xf-auto-browse-panel {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(15, 23, 42, 0.95);
color: #e2e8f0;
padding: 12px 18px;
border-radius: 12px;
font-family: system-ui, sans-serif;
font-size: 14px;
z-index: 999999;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
border: 1px solid rgba(255,255,255,0.1);
display: flex;
align-items: center;
gap: 12px;
transition: opacity 0.3s, transform 0.3s;
}
#xf-auto-browse-panel .auto-status {
display: flex;
flex-direction: column;
gap: 2px;
}
#xf-auto-browse-panel .auto-status-main {
font-weight: 600;
}
#xf-auto-browse-panel .auto-status-sub {
font-size: 12px;
color: #94a3b8;
}
#xf-auto-browse-panel .auto-countdown {
font-size: 20px;
font-weight: 700;
color: #60a5fa;
min-width: 28px;
text-align: center;
}
#xf-auto-browse-panel .auto-stop-btn {
background: #dc2626;
color: #fff;
border: none;
padding: 6px 14px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
transition: background 0.2s;
}
#xf-auto-browse-panel .auto-stop-btn:hover {
background: #b91c1c;
}
#xf-auto-browse-panel .auto-progress {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
background: #60a5fa;
border-radius: 0 0 12px 12px;
transition: width 1s linear;
}
#xf-auto-browse-panel .auto-manual-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #94a3b8;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
#xf-auto-browse-panel .auto-manual-label input {
cursor: pointer;
accent-color: #60a5fa;
}
`;
document.head.appendChild(style);
// ================================================================
// AUTO-BROWSE STATE
// ================================================================
let autoBrowseActive = false;
let autoBrowseTimer = null;
let autoScrollTimer = null;
let autoPanel = null;
let autoCurrentPost = null; // Post đang xem hiện tại (để hỗ trợ ← →)
let autoNavigating = false; // Đang trong quá trình điều hướng (tránh spam phím)
let isManualMode = localStorage.getItem('xf-auto-manual') === 'true'; // Chế độ manual (chỉ dùng mũi tên)
const AUTO_DELAY_SEC = 5;
const AUTO_SCROLL_SPEED = 60;
function createAutoPanel() {
if (autoPanel) return autoPanel;
autoPanel = document.createElement('div');
autoPanel.id = 'xf-auto-browse-panel';
autoPanel.innerHTML = `
<div class="auto-status">
<span class="auto-status-main">📎 Auto Browse</span>
<span class="auto-status-sub">Đang chờ...</span>
</div>
<span class="auto-countdown">${AUTO_DELAY_SEC}</span>
<label class="auto-manual-label">
<input type="checkbox" id="xf-auto-manual-cb" ${isManualMode ? 'checked' : ''}>
Manual
</label>
<button class="auto-stop-btn">Dừng (Esc)</button>
<div class="auto-progress" style="width:0%"></div>
<div style="font-size:11px;color:#64748b;margin-left:4px;">← → Esc</div>
`;
autoPanel.querySelector('.auto-stop-btn').onclick = () => stopAutoBrowse();
autoPanel.querySelector('#xf-auto-manual-cb').onchange = (e) => {
isManualMode = e.target.checked;
localStorage.setItem('xf-auto-manual', isManualMode);
if (isManualMode) {
// Đang countdown thì hủy, chuyển sang chờ mũi tên
cancelCurrentAutoAction();
updateAutoPanel('📎 Manual', 'Nhấn ← → để chuyển', '⏸');
updateAutoProgress(0);
} else if (autoCurrentPost) {
// Bỏ manual → bắt đầu countdown lại
autoCountdown(AUTO_DELAY_SEC, () => {
if (!autoBrowseActive) return;
autoFindNextFrom(autoCurrentPost);
});
}
};
document.body.appendChild(autoPanel);
return autoPanel;
}
function updateAutoPanel(statusText, subText, countdown) {
if (!autoPanel) return;
autoPanel.querySelector('.auto-status-main').textContent = statusText;
autoPanel.querySelector('.auto-status-sub').textContent = subText || '';
if (countdown !== undefined) {
autoPanel.querySelector('.auto-countdown').textContent = countdown;
}
}
function updateAutoProgress(percent) {
if (!autoPanel) return;
autoPanel.querySelector('.auto-progress').style.width = percent + '%';
}
function removeAutoPanel() {
if (autoPanel) {
autoPanel.style.opacity = '0';
autoPanel.style.transform = 'translateY(20px)';
setTimeout(() => { autoPanel.remove(); autoPanel = null; }, 300);
}
}
function stopAutoBrowse() {
autoBrowseActive = false;
autoCurrentPost = null;
autoNavigating = false;
if (autoBrowseTimer) { clearTimeout(autoBrowseTimer); autoBrowseTimer = null; }
if (autoScrollTimer) { cancelAnimationFrame(autoScrollTimer); autoScrollTimer = null; }
removeAutoPanel();
document.querySelectorAll('.actionBar-action--autoBrowse.active').forEach(b => {
b.classList.remove('active');
b.innerHTML = 'Auto 📎 ▶';
});
console.log('[XF-Find-Next] Auto browse đã dừng.');
}
// Hủy countdown/scroll hiện tại (dùng khi nhấn mũi tên)
function cancelCurrentAutoAction() {
if (autoBrowseTimer) { clearTimeout(autoBrowseTimer); autoBrowseTimer = null; }
if (autoScrollTimer) { cancelAnimationFrame(autoScrollTimer); autoScrollTimer = null; }
}
// Cuộn mượt xuống cuối post, trả về Promise resolve khi xong
function smoothScrollToBottom(postElement) {
return new Promise(resolve => {
const postRect = postElement.getBoundingClientRect();
const postBottom = postRect.bottom + window.scrollY;
const viewportH = window.innerHeight;
// Nếu post nằm gọn trong viewport → không cần cuộn thêm
if (postBottom <= window.scrollY + viewportH + 50) {
resolve();
return;
}
updateAutoPanel('📎 Đang cuộn...', 'Comment dài, cuộn xuống cuối', '⬇');
function scrollStep() {
if (!autoBrowseActive) { resolve(); return; }
const currentBottom = window.scrollY + viewportH;
if (currentBottom >= postBottom - 30) {
resolve();
return;
}
window.scrollBy(0, AUTO_SCROLL_SPEED);
autoScrollTimer = requestAnimationFrame(scrollStep);
}
autoScrollTimer = requestAnimationFrame(scrollStep);
});
}
// Countdown rồi gọi callback
function autoCountdown(seconds, callback) {
let remaining = seconds;
updateAutoPanel('📎 Auto Browse', `Chuyển tiếp sau...`, remaining);
updateAutoProgress(0);
function tick() {
if (!autoBrowseActive) return;
remaining--;
const progress = ((seconds - remaining) / seconds) * 100;
updateAutoProgress(progress);
updateAutoPanel('📎 Auto Browse', `Chuyển tiếp sau...`, remaining);
if (remaining <= 0) {
callback();
} else {
autoBrowseTimer = setTimeout(tick, 1000);
}
}
autoBrowseTimer = setTimeout(tick, 1000);
}
// Điều hướng + cuộn + countdown cho một post trên trang hiện tại
async function navigateToAttachPost(postElement) {
if (!autoBrowseActive) return;
autoCurrentPost = postElement; // Lưu vị trí hiện tại
autoNavigating = true;
const targetId = postElement.getAttribute('id');
if (targetId) {
postElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
await new Promise(r => setTimeout(r, 500));
}
if (!autoBrowseActive) { autoNavigating = false; return; }
highlightElement(postElement);
// Cuộn mượt xuống cuối post nếu dài
await smoothScrollToBottom(postElement);
autoNavigating = false;
if (!autoBrowseActive) return;
if (isManualMode) {
// Chế độ manual: chờ mũi tên
updateAutoPanel('📎 Manual', 'Nhấn ← → để chuyển', '⏸');
updateAutoProgress(0);
} else {
// Chế độ auto: countdown rồi tìm tiếp
autoCountdown(AUTO_DELAY_SEC, () => {
if (!autoBrowseActive) return;
autoFindNextFrom(postElement);
});
}
}
// Tìm attachment tiếp theo từ một post (dùng cho auto-browse)
async function autoFindNextFrom(startPost) {
if (!autoBrowseActive) return;
// Tìm trên trang hiện tại
const allPosts = Array.from(document.querySelectorAll('.message'));
const startIndex = allPosts.indexOf(startPost);
for (let i = startIndex + 1; i < allPosts.length; i++) {
if (hasAttachment(allPosts[i])) {
await navigateToAttachPost(allPosts[i]);
return;
}
}
// Không còn trên trang hiện tại → chuyển trang
let nextButton = document.querySelector('a.pageNav-jump--next');
let nextUrl = nextButton ? nextButton.href : null;
if (!nextUrl) {
updateAutoPanel('📎 Hoàn tất', 'Đã đến trang cuối, không còn ảnh/đính kèm', '✅');
setTimeout(() => stopAutoBrowse(), 2000);
return;
}
updateAutoPanel('📎 Đang quét...', 'Chuyển sang trang tiếp theo', '🔍');
let pagesScanned = 0;
const MAX_PAGES = 50;
while (nextUrl && autoBrowseActive && pagesScanned < MAX_PAGES) {
pagesScanned++;
updateAutoPanel('📎 Đang quét...', `Trang tiếp #${pagesScanned}`, '🔍');
try {
const response = await fetch(nextUrl);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const posts = doc.querySelectorAll('.message');
for (let i = 0; i < posts.length; i++) {
if (hasAttachment(posts[i])) {
const postId = posts[i].getAttribute('id');
if (!postId) continue;
// Chuyển trang (sẽ reload, auto-browse sẽ dừng)
// Lưu trạng thái auto vào sessionStorage để tiếp tục sau reload
sessionStorage.setItem('xf-auto-browse', JSON.stringify({
active: true,
targetId: postId
}));
window.location.href = nextUrl + '#' + postId;
return;
}
}
const nextBtnDoc = doc.querySelector('a.pageNav-jump--next');
nextUrl = nextBtnDoc ? nextBtnDoc.href : null;
await new Promise(r => setTimeout(r, 300));
} catch (err) {
console.error('[XF-Find-Next] Auto browse lỗi:', err);
updateAutoPanel('❌ Lỗi', err.message, '!');
setTimeout(() => stopAutoBrowse(), 3000);
return;
}
}
updateAutoPanel('📎 Hoàn tất', 'Không còn comment có ảnh/đính kèm', '✅');
setTimeout(() => stopAutoBrowse(), 2000);
}
// Tìm attachment TRƯỚC ĐÓ từ một post (dùng cho mũi tên trái)
async function autoFindPrevFrom(startPost) {
if (!autoBrowseActive) return;
const allPosts = Array.from(document.querySelectorAll('.message'));
const startIndex = allPosts.indexOf(startPost);
// Tìm ngược trên trang hiện tại
for (let i = startIndex - 1; i >= 0; i--) {
if (hasAttachment(allPosts[i])) {
cancelCurrentAutoAction();
await navigateToAttachPost(allPosts[i]);
return;
}
}
// Không còn trên trang hiện tại → thông báo
updateAutoPanel('📎 Auto Browse', 'Đã ở đầu trang, không có ảnh trước đó', '⬆');
// Reset countdown tại post hiện tại
setTimeout(() => {
if (!autoBrowseActive || autoCurrentPost !== startPost) return;
autoCountdown(AUTO_DELAY_SEC, () => {
if (!autoBrowseActive) return;
autoFindNextFrom(startPost);
});
}, 1500);
}
// Bắt đầu auto-browse từ post hiện tại
function startAutoBrowse(startPost) {
if (autoBrowseActive) {
stopAutoBrowse();
return;
}
autoBrowseActive = true;
createAutoPanel();
updateAutoPanel('📎 Auto Browse', 'Đang tìm ảnh/đính kèm đầu tiên...', '🔍');
console.log('[XF-Find-Next] Auto browse bắt đầu.');
// Mark nút active
document.querySelectorAll('.actionBar-action--autoBrowse').forEach(b => {
b.classList.add('active');
b.innerHTML = 'Auto 📎 ⏸';
});
autoFindNextFrom(startPost);
}
// Phím tắt: Escape = dừng, ArrowRight = tiến, ArrowLeft = lùi
document.addEventListener('keydown', (e) => {
if (!autoBrowseActive) return;
if (e.key === 'Escape') {
stopAutoBrowse();
return;
}
// Chỉ xử lý mũi tên khi không đang trong input/textarea
const tag = document.activeElement?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || document.activeElement?.isContentEditable) return;
if (e.key === 'ArrowRight' && autoCurrentPost) {
e.preventDefault();
if (autoNavigating) return; // Đang điều hướng, bỏ qua
cancelCurrentAutoAction();
updateAutoPanel('📎 Tiến ▶', 'Nhấn → để next nhanh', '▶');
autoFindNextFrom(autoCurrentPost);
}
if (e.key === 'ArrowLeft' && autoCurrentPost) {
e.preventDefault();
if (autoNavigating) return;
cancelCurrentAutoAction();
updateAutoPanel('📎 Lùi ◀', 'Nhấn ← để quay lại', '◀');
autoFindPrevFrom(autoCurrentPost);
}
});
// Khôi phục auto-browse sau khi chuyển trang
function resumeAutoBrowse() {
const saved = sessionStorage.getItem('xf-auto-browse');
if (!saved) return;
sessionStorage.removeItem('xf-auto-browse');
const data = JSON.parse(saved);
if (!data.active) return;
// Chờ DOM load xong rồi tìm post target
setTimeout(() => {
const targetPost = data.targetId ? document.getElementById(data.targetId) : null;
if (targetPost && hasAttachment(targetPost)) {
autoBrowseActive = true;
createAutoPanel();
document.querySelectorAll('.actionBar-action--autoBrowse').forEach(b => {
b.classList.add('active');
b.innerHTML = 'Auto 📎 ⏸';
});
navigateToAttachPost(targetPost);
} else {
// Target không tìm thấy, thử tìm post đầu tiên có attachment
autoBrowseActive = true;
createAutoPanel();
document.querySelectorAll('.actionBar-action--autoBrowse').forEach(b => {
b.classList.add('active');
b.innerHTML = 'Auto 📎 ⏸';
});
const firstPost = document.querySelector('.message');
if (firstPost) {
// Tìm từ đầu trang, dùng post giả trước firstPost
const allPosts = Array.from(document.querySelectorAll('.message'));
for (const p of allPosts) {
if (hasAttachment(p)) {
navigateToAttachPost(p);
return;
}
}
// Không có attachment nào, tìm trang tiếp
autoFindNextFrom(allPosts[allPosts.length - 1]);
}
}
}, 1500);
}
// Hàm chèn nút vào mỗi post
function addButtons() {
const posts = document.querySelectorAll('.message');
let addedCount = 0;
posts.forEach(post => {
// Tránh chèn trùng lặp
if (post.dataset.hasFindNext) return;
const username = post.getAttribute('data-author');
if (!username) return;
post.dataset.hasFindNext = 'true';
let added = false;
// Chèn nút vào Action Bar (góc dưới cùng bên phải của bài viết)
let targetActionBar = post.querySelector('.message-actionBar .actionBar-set--external');
if (!targetActionBar) targetActionBar = post.querySelector('.message-actionBar .actionBar-set');
if (!targetActionBar) targetActionBar = post.querySelector('.message-actionBar');
if (targetActionBar) {
// Nút tìm bình luận tiếp theo của cùng user
const btnAction = document.createElement('a');
btnAction.href = '#';
btnAction.className = 'actionBar-action actionBar-action--findNext';
btnAction.innerHTML = 'Next Comment ⬇️';
btnAction.title = 'Tìm bình luận tiếp theo của ' + username;
btnAction.onclick = (e) => {
e.preventDefault();
findNextComment(post, username, btnAction);
};
targetActionBar.appendChild(btnAction);
// Nút tìm comment có ảnh/đính kèm (không phải emoji)
const btnAttach = document.createElement('a');
btnAttach.href = '#';
btnAttach.className = 'actionBar-action actionBar-action--findAttach';
btnAttach.innerHTML = 'Next Attach 📎';
btnAttach.title = 'Tìm comment tiếp theo có ảnh hoặc đính kèm';
btnAttach.onclick = (e) => {
e.preventDefault();
findNextAttachment(post, btnAttach);
};
targetActionBar.appendChild(btnAttach);
// Nút auto-browse (tự động next ảnh/đính kèm)
const btnAuto = document.createElement('a');
btnAuto.href = '#';
btnAuto.className = 'actionBar-action actionBar-action--autoBrowse';
btnAuto.innerHTML = 'Auto 📎 ▶';
btnAuto.title = 'Tự động duyệt ảnh/đính kèm (5s mỗi comment)';
btnAuto.onclick = (e) => {
e.preventDefault();
startAutoBrowse(post);
};
targetActionBar.appendChild(btnAuto);
added = true;
}
if (added) addedCount++;
});
if (addedCount > 0) {
console.log('[XF-Find-Next] Đã chèn thêm nút Next Comment vào ' + addedCount + ' bài viết.');
}
}
// Hàm xử lý tìm kiếm
async function findNextComment(startPost, username, btn) {
if (btn.classList.contains('searching')) return;
btn.classList.add('searching');
btn.innerHTML = 'Đang tìm... ⏳';
try {
// 1. Quét tìm trên trang HIỆN TẠI
const allPostsOnPage = Array.from(document.querySelectorAll('.message'));
const startIndex = allPostsOnPage.indexOf(startPost);
for (let i = startIndex + 1; i < allPostsOnPage.length; i++) {
if (allPostsOnPage[i].getAttribute('data-author') === username) {
const targetId = allPostsOnPage[i].getAttribute('id');
if (targetId) {
window.location.hash = targetId;
highlightElement(allPostsOnPage[i]);
}
resetBtn(btn);
return;
}
}
// 2. Không có ở trang hiện tại, quét ngầm các TRANG TIẾP THEO
let nextButton = document.querySelector('a.pageNav-jump--next');
let nextUrl = nextButton ? nextButton.href : null;
if (!nextUrl) {
alert(`Không tìm thấy bình luận nào khác của ${username} ở bên dưới, và đây đã là trang cuối.`);
resetBtn(btn);
return;
}
let foundPostId = null;
let foundUrl = null;
let pagesScanned = 0;
const MAX_PAGES_TO_SCAN = 50;
while (nextUrl && !foundUrl && pagesScanned < MAX_PAGES_TO_SCAN) {
pagesScanned++;
btn.innerHTML = `Đang quét trang tiếp (${pagesScanned})... ⏳`;
const response = await fetch(nextUrl);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const posts = doc.querySelectorAll('.message');
for (let i = 0; i < posts.length; i++) {
if (posts[i].getAttribute('data-author') === username) {
foundPostId = posts[i].getAttribute('id');
if (!foundPostId) continue;
foundUrl = nextUrl + '#' + foundPostId;
break;
}
}
if (foundUrl) {
btn.innerHTML = 'Đã thấy! Đang chuyển... ✅';
window.location.href = foundUrl;
return;
}
const nextBtnDoc = doc.querySelector('a.pageNav-jump--next');
nextUrl = nextBtnDoc ? nextBtnDoc.href : null;
// Nghỉ xíu tránh spam request server
await new Promise(r => setTimeout(r, 300));
}
if (!foundUrl) {
if (pagesScanned >= MAX_PAGES_TO_SCAN) {
alert(`Đã quét ${MAX_PAGES_TO_SCAN} trang nhưng không tìm thấy. Có thể bình luận tiếp theo ở quá xa.`);
} else {
alert(`Đã quét tới trang cuối cùng nhưng không tìm thấy thêm bình luận nào của ${username}.`);
}
}
} catch (err) {
console.error(err);
alert('Lỗi trong quá trình quét: ' + err.message);
}
resetBtn(btn);
}
// === Kiểm tra post có ảnh/đính kèm thực sự (không phải emoji) ===
// Bỏ qua nội dung nằm trong blockquote (trích dẫn bài người khác)
function hasAttachment(postElement) {
const body = postElement.querySelector('.message-body, .message-content');
if (!body) return false;
// Clone body và xóa hết blockquote/quote block để không tính ảnh/video trong quote
const cleanBody = body.cloneNode(true);
cleanBody.querySelectorAll('blockquote, .bbCodeBlock--quote, .quoteContainer, .Quote').forEach(bq => bq.remove());
// 1. Ảnh thực sự (loại trừ emoji/smilie)
const imgs = cleanBody.querySelectorAll('img');
for (const img of imgs) {
// Bỏ qua emoji/smilie
if (img.classList.contains('smilie') ||
img.closest('.smilie') ||
img.dataset.shortname ||
(img.src && /smilies|emoji/i.test(img.src)) ||
(img.alt && /^:[a-zA-Z0-9_]+:$/.test(img.alt.trim()))) {
continue;
}
// Ảnh thực sự
return true;
}
// 2. Video embed
if (cleanBody.querySelector('video, .bbMediaWrapper video')) return true;
// 3. Section đính kèm có link file
const attachSection = postElement.querySelector('section.message-attachments');
if (attachSection) {
const links = attachSection.querySelectorAll('a[href]');
for (const link of links) {
const href = link.href.toLowerCase();
if (/\.(jpg|jpeg|png|gif|webp|bmp|svg|mp4|mov|webm|avi|mkv|m4v|zip|rar|pdf|doc|docx|xls|xlsx)/.test(href)) {
return true;
}
}
// Nếu có attachment list thì cũng tính
if (attachSection.querySelector('.attachmentList')) return true;
}
// 4. Attachment list ngoài section
const attachList = postElement.querySelector('.attachmentList');
if (attachList && attachList.querySelectorAll('a[href]').length > 0) return true;
return false;
}
// === Tìm comment tiếp theo có ảnh/đính kèm ===
async function findNextAttachment(startPost, btn) {
if (btn.classList.contains('searching')) return;
btn.classList.add('searching');
btn.innerHTML = 'Đang tìm... ⏳';
try {
// 1. Quét trang hiện tại
const allPosts = Array.from(document.querySelectorAll('.message'));
const startIndex = allPosts.indexOf(startPost);
for (let i = startIndex + 1; i < allPosts.length; i++) {
if (hasAttachment(allPosts[i])) {
const targetId = allPosts[i].getAttribute('id');
if (targetId) {
window.location.hash = targetId;
highlightElement(allPosts[i]);
}
resetAttachBtn(btn);
return;
}
}
// 2. Quét các trang tiếp theo
let nextButton = document.querySelector('a.pageNav-jump--next');
let nextUrl = nextButton ? nextButton.href : null;
if (!nextUrl) {
alert('Không tìm thấy comment có ảnh/đính kèm phía dưới, và đây đã là trang cuối.');
resetAttachBtn(btn);
return;
}
let foundUrl = null;
let pagesScanned = 0;
const MAX_PAGES = 50;
while (nextUrl && !foundUrl && pagesScanned < MAX_PAGES) {
pagesScanned++;
btn.innerHTML = `Đang quét trang (${pagesScanned})... ⏳`;
const response = await fetch(nextUrl);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const posts = doc.querySelectorAll('.message');
for (let i = 0; i < posts.length; i++) {
if (hasAttachment(posts[i])) {
const postId = posts[i].getAttribute('id');
if (!postId) continue;
foundUrl = nextUrl + '#' + postId;
break;
}
}
if (foundUrl) {
btn.innerHTML = 'Đã thấy! Đang chuyển... ✅';
window.location.href = foundUrl;
return;
}
const nextBtnDoc = doc.querySelector('a.pageNav-jump--next');
nextUrl = nextBtnDoc ? nextBtnDoc.href : null;
await new Promise(r => setTimeout(r, 300));
}
if (!foundUrl) {
if (pagesScanned >= MAX_PAGES) {
alert(`Đã quét ${MAX_PAGES} trang nhưng không tìm thấy comment có ảnh/đính kèm.`);
} else {
alert('Đã quét tới trang cuối nhưng không tìm thấy comment có ảnh/đính kèm.');
}
}
} catch (err) {
console.error(err);
alert('Lỗi trong quá trình quét: ' + err.message);
}
resetAttachBtn(btn);
}
function highlightElement(element) {
element.style.transition = 'background-color 0.5s ease';
const origBg = element.style.backgroundColor;
element.style.backgroundColor = 'rgba(59, 130, 246, 0.2)';
setTimeout(() => {
element.style.backgroundColor = origBg;
}, 1500);
}
function resetBtn(btn) {
btn.classList.remove('searching');
btn.innerHTML = 'Next Comment ⬇️';
}
function resetAttachBtn(btn) {
btn.classList.remove('searching');
btn.innerHTML = 'Next Attach 📎';
}
addButtons();
// Khôi phục auto-browse nếu đang chuyển trang
resumeAutoBrowse();
// MutationObserver quan sát những comment load ajax / load chậm
const observer = new MutationObserver((mutations) => {
let shouldCheck = false;
for (const m of mutations) {
if (m.addedNodes.length > 0) {
shouldCheck = true;
break;
}
}
if (shouldCheck) {
if (window.addBtnTimer) clearTimeout(window.addBtnTimer);
window.addBtnTimer = setTimeout(addButtons, 1000);
}
});
observer.observe(document.body, { childList: true, subtree: true });
// Đảm bảo nút được thêm kể cả khi trang load chậm
setTimeout(addButtons, 1500);
setTimeout(addButtons, 3000);
})();
Public Last updated: 2026-03-11 05:07:47 PM
