Files
twpkinfo-adblocker/twpkinfo-ad-blocker-sniper.user.js
Timmy 2cd3031560 改善 Leaflet popup 樣式並強化清理保護
- 新增 isLeafletDescendant 輔助函式,統一所有清理流程的地圖元素豁免邏輯
- 加入 popup / pane 內容保護 CSS,避免被廣告清除規則誤殺
- 移除 popup 預設的白色三角 tip,wrapper 改用自訂 padding 控制上下留白
- MutationObserver 略過 Leaflet 後代節點變動,避免點 marker 時誤觸發清理
- 版本 1.1.7 → 1.1.15

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:19:48 +08:00

721 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==UserScript==
// @name TWPKInfo 廣告清除器
// @namespace http://tampermonkey.net/
// @version 1.1.15
// @description 移除頂部導覽列、底部功能列與空白列,保留地圖工具
// @author AdBlocker
// @match https://twpkinfo.com/*
// @match https://*.twpkinfo.com/*
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
console.log('TWPKInfo 廣告清除器啟動');
let cleanupStarted = false;
let tooltipTimer = null;
// 判斷元素是否屬於 Leaflet 地圖/彈窗系統(清理時必須跳過)
function isLeafletDescendant(element) {
if (!element || !element.closest) {
return false;
}
return Boolean(
element.closest('#map') ||
element.closest('.leaflet-container') ||
element.closest('.leaflet-pane') ||
element.closest('.leaflet-popup') ||
element.closest('.leaflet-tooltip') ||
element.closest('.leaflet-control')
);
}
// 清理用 CSS
function addCleanupCSS() {
if (document.getElementById('twpk-ad-blocker-cleanup')) {
return;
}
const styleRoot = document.head || document.documentElement;
if (!styleRoot) {
setTimeout(addCleanupCSS, 0);
return;
}
const style = document.createElement('style');
style.id = 'twpk-ad-blocker-cleanup';
style.textContent = `
/* 移除 Google AdSense */
.adsbygoogle,
ins.adsbygoogle,
[data-ad-client],
[data-ad-slot] {
display: none !important;
}
html,
body {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
height: 100% !important;
min-height: 100vh !important;
overflow: hidden !important;
background: transparent !important;
}
/* 移除頂部標題列 */
body > div:first-child:not(#map):not([class*="leaflet"]) {
display: none !important;
visibility: hidden !important;
height: 0 !important;
min-height: 0 !important;
overflow: hidden !important;
}
body > div:has(> h1),
body > div:has(> h2),
body > div:has(> h3),
body > div:has(> ins.adsbygoogle) {
display: none !important;
visibility: hidden !important;
height: 0 !important;
min-height: 0 !important;
overflow: hidden !important;
}
/* 移除 igym.aspx 的頂部網站導覽列 */
#myheader,
#myheader > .navbar,
nav.navbar.main-menu,
nav.navbar-fixed-top,
body > nav.navbar,
body > .navbar,
body > [class*="navbar"] {
display: none !important;
visibility: hidden !important;
height: 0 !important;
min-height: 0 !important;
overflow: hidden !important;
}
/* 移除底部功能列 */
#div_close_cf,
body > div[style*="position"][style*="bottom"],
body > div[style*="z-index"][style*="bottom"]:not(.leaflet-control-attribution),
div[style*="position: fixed; bottom"],
div[style*="position: absolute; bottom"],
div[style*="bottom: 0"]:not(.leaflet-control-attribution):not([class*="leaflet"]) {
display: none !important;
}
/* 移除特定結構的底部列 */
body > div:last-child:not(#map):not([id*="map"]):not([class*="leaflet"]):not(.leaflet-control-attribution) {
display: none !important;
}
/* 強制隱藏常見的底部導航類名 */
.bottom-nav,
.bottom-bar,
.footer-nav,
.nav-bottom,
.bottom-menu,
.bottom-toolbar,
[class*="bottom"][class*="nav"],
[class*="bottom"][class*="bar"],
[class*="bottom"][class*="menu"]:not([class*="pokemon"]) {
display: none !important;
visibility: hidden !important;
height: 0 !important;
overflow: hidden !important;
}
/* 確保重要元素可見 */
#map,
.leaflet-container,
.leaflet-control-attribution,
[class*="pokemon"],
[class*="sidebar"]:not([class*="bottom"]),
[class*="panel"]:not([class*="bottom"]),
[class*="menu"]:not([class*="bottom"]) {
display: block !important;
visibility: visible !important;
}
/* 保護 Leaflet 彈出視窗popup所有內容避免被廣告清除規則誤殺 */
html body .leaflet-pane,
html body .leaflet-pane *,
html body .leaflet-popup,
html body .leaflet-popup * {
visibility: visible !important;
opacity: 1 !important;
}
/* 只強制 popup 內文rows的顯示避免被 display:none 規則蓋掉;
不再覆寫 .leaflet-popup / .leaflet-popup-content-wrapper / tip 容器尺寸,
讓 TWPKInfo 與 Leaflet 自身的排版(含底部 tip 三角)正常運作 */
html body .leaflet-popup-content > *,
html body .leaflet-popup-content > * > *,
html body .leaflet-popup-content > * > * > *,
html body .leaflet-popup-content > * > * > * > * {
display: revert !important;
height: auto !important;
min-height: 0 !important;
max-height: none !important;
overflow: visible !important;
}
/* 把 Leaflet popup 預設的白色三角 tip 拿掉,並把 wrapper 改成自己控制留白 */
html body .leaflet-popup-content-wrapper {
overflow: hidden !important;
border-radius: 12px !important;
padding: 14px 4px !important;
}
/* 內層 content margin 歸零,全部交給 wrapper 的 padding 負責 */
html body .leaflet-popup-content {
margin: 0 14px !important;
}
html body .leaflet-popup-tip-container,
html body .leaflet-popup-tip,
html body [class*="leaflet-popup-tip"] {
display: none !important;
}
#map,
.leaflet-container {
top: 0 !important;
width: 100vw !important;
height: 100vh !important;
min-height: 100vh !important;
}
[data-twpk-tooltip] {
position: relative !important;
}
[data-twpk-tooltip]::after {
content: attr(data-twpk-tooltip);
position: absolute;
top: 50%;
left: calc(100% + 10px);
transform: translateY(-50%);
z-index: 2147483647;
display: block;
max-width: 220px;
padding: 6px 9px;
border-radius: 4px;
background: rgba(20, 20, 20, 0.9);
color: #fff;
font-size: 13px;
font-weight: 500;
line-height: 1.35;
letter-spacing: 0;
text-align: left;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.24);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 120ms ease, visibility 120ms ease;
}
[data-twpk-tooltip]::before {
content: "";
position: absolute;
top: 50%;
left: calc(100% + 4px);
transform: translateY(-50%);
z-index: 2147483647;
border: 6px solid transparent;
border-right-color: rgba(20, 20, 20, 0.9);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 120ms ease, visibility 120ms ease;
}
[data-twpk-tooltip]:hover::after,
[data-twpk-tooltip]:hover::before,
[data-twpk-tooltip]:focus::after,
[data-twpk-tooltip]:focus::before,
[data-twpk-tooltip]:focus-within::after,
[data-twpk-tooltip]:focus-within::before {
opacity: 1;
visibility: visible;
}
[data-twpk-tooltip-side="left"]::after {
right: calc(100% + 10px);
left: auto;
}
[data-twpk-tooltip-side="left"]::before {
right: calc(100% + 4px);
left: auto;
border-right-color: transparent;
border-left-color: rgba(20, 20, 20, 0.9);
}
`;
styleRoot.appendChild(style);
console.log('清理 CSS 規則已應用');
}
// 讓移除底部列後的 Leaflet 地圖重新吃滿視窗
function resizeMapToViewport() {
const mapElement = document.getElementById('map');
if (mapElement) {
mapElement.style.width = '100vw';
mapElement.style.height = '100vh';
mapElement.style.minHeight = '100vh';
}
window.requestAnimationFrame(() => {
window.dispatchEvent(new Event('resize'));
if (window.map && typeof window.map.invalidateSize === 'function') {
window.map.invalidateSize(false);
}
});
}
function setToolbarTooltip(element, label) {
if (!element || element.closest('#map .leaflet-pane')) {
return;
}
const labeledParent = element.parentElement && element.parentElement.closest('[data-twpk-tooltip]');
if (!labeledParent || labeledParent === element) {
element.setAttribute('data-twpk-tooltip', label);
}
element.setAttribute('title', label);
element.setAttribute('aria-label', label);
updateTooltipSide(element);
}
function updateTooltipSide(element) {
const rect = element.getBoundingClientRect();
const label = element.getAttribute('data-twpk-tooltip') || element.getAttribute('title') || '';
const estimatedWidth = Math.min(Math.max(label.length * 14 + 18, 90), 220);
if (rect.right + estimatedWidth + 18 > window.innerWidth) {
element.setAttribute('data-twpk-tooltip-side', 'left');
} else {
element.removeAttribute('data-twpk-tooltip-side');
}
}
function applyToolbarTooltips() {
const directTooltips = [
['#update', '重新整理地圖資料'],
['#filter_btn', '開啟/關閉篩選面板'],
['#filter_bGYM', '切換道館/補給站顯示'],
['#gym', '切換到道館地圖'],
['#ipoke', '切換到寶可夢地圖'],
['#iquest', '切換到任務地圖'],
['#generation', '切換寶可夢世代顯示'],
['#a_iquest', '切換到任務地圖'],
['#quest_img', '切換到任務地圖'],
['.leaflet-control a[href*="iquest"]', '切換到任務地圖'],
['.leaflet-control a[href*="igym"]', '切換到道館地圖'],
['.leaflet-control a[href*="ipoke"]', '切換到寶可夢地圖'],
['.leaflet-control-locate a', '定位到我的位置'],
['.leaflet-control a[title*="Location"]', '定位到我的位置'],
['.leaflet-control a[title*="位置"]', '定位到我的位置'],
['.leaflet-control-zoom-in', '放大地圖'],
['.leaflet-control-zoom-out', '縮小地圖']
];
directTooltips.forEach(([selector, label]) => {
document.querySelectorAll(selector).forEach(element => {
setToolbarTooltip(element, label);
});
});
const imageTooltips = [
['img[src*="GYMgo"]', '切換到道館地圖'],
['img[src*="ipoke"]', '切換到寶可夢地圖'],
['img[src*="quest"]', '切換到任務地圖'],
['img[src*="Quest"]', '切換到任務地圖'],
['img[src*="iquest"]', '切換到任務地圖'],
['img[src*="target"]', '定位/移到目前位置'],
['img[src*="radar"]', '定位/移到目前位置'],
['img[src*="locat"]', '定位/移到目前位置'],
['img[src*="gps"]', '定位/移到目前位置'],
['img[src*="blue"]', '定位/移到目前位置'],
['img[src*="eye_"]', '切換道館/補給站顯示']
];
imageTooltips.forEach(([selector, label]) => {
document.querySelectorAll(selector).forEach(image => {
const target = image.closest('a, button, div') || image;
setToolbarTooltip(target, label);
image.setAttribute('alt', label);
});
});
}
function scheduleToolbarTooltips(delay = 100) {
window.clearTimeout(tooltipTimer);
tooltipTimer = window.setTimeout(applyToolbarTooltips, delay);
}
// 移除底部功能列
function cleanupBottomBar() {
let count = 0;
// 移除廣告
document.querySelectorAll('.adsbygoogle, ins.adsbygoogle, [data-ad-client], [data-ad-slot]').forEach(ad => {
ad.remove();
count++;
});
// 移除頂部標題
document.querySelectorAll('h1, h2, h3').forEach(title => {
if (title.textContent.includes('台灣寶可夢資訊站') || title.textContent.includes('TWPKInfo')) {
const container = title.closest('div');
if (container && container !== document.getElementById('map') && !container.classList.contains('leaflet-container')) {
container.remove();
count++;
}
}
});
// 清理底部列
const potentialBottomBars = [];
// 策略1通過位置識別
document.querySelectorAll('div').forEach(div => {
if (div.id === 'map' ||
div.classList.contains('leaflet-container') ||
div.classList.contains('leaflet-control-attribution') ||
isLeafletDescendant(div)) {
return;
}
const rect = div.getBoundingClientRect();
const style = window.getComputedStyle(div);
// 識別底部固定元素
if ((style.position === 'fixed' || style.position === 'absolute') &&
(style.bottom === '0px' || parseInt(style.bottom) <= 30) &&
rect.width > 200 && // 有一定寬度
rect.height > 10 && rect.height < 150) { // 適中高度
potentialBottomBars.push(div);
}
// 識別位於底部的大寬度元素
if (rect.bottom >= window.innerHeight - 80 &&
rect.width > window.innerWidth * 0.4 &&
rect.height < 100 &&
rect.left <= 100) { // 不是右側小元素
potentialBottomBars.push(div);
}
});
// 策略2通過內容識別
document.querySelectorAll('div').forEach(div => {
if (isLeafletDescendant(div)) {
return;
}
const classList = (typeof div.className === 'string' ? div.className : '').toLowerCase();
if (classList.includes('bottom') &&
(classList.includes('nav') || classList.includes('bar') || classList.includes('menu') || classList.includes('toolbar')) &&
!classList.includes('pokemon') &&
!classList.includes('leaflet')) {
potentialBottomBars.push(div);
}
});
// 策略3DOM 結構識別(最後一個子元素)
const bodyChildren = Array.from(document.body.children);
bodyChildren.reverse().slice(0, 3).forEach(child => { // 檢查最後3個子元素
if (child.id !== 'map' &&
!child.classList.contains('leaflet-container') &&
!child.classList.contains('leaflet-control-attribution')) {
const rect = child.getBoundingClientRect();
if (rect.width > 200 && rect.height < 120 && rect.height > 20) {
potentialBottomBars.push(child);
}
}
});
// 執行移除(去重)
const uniqueElements = [...new Set(potentialBottomBars)];
uniqueElements.forEach(element => {
// 最後確認:確保不是重要元素
if (!element.querySelector('[class*="pokemon"]') &&
!element.querySelector('img[src*="pokemon"]') &&
!element.textContent.includes('Leaflet') &&
!element.textContent.includes('MapTiler') &&
!element.textContent.includes('OpenStreetMap')) {
element.remove();
count++;
}
});
if (count > 0) {
console.log(`已清理 ${count} 個元素`);
}
resizeMapToViewport();
return count;
}
// 移除 igym.aspx 這類頁面的頂部網站導覽列
function cleanupTopNavigation() {
let count = 0;
const navTexts = ['首頁', '官方消息', '關於寶可夢', '寵物圖鑑', '寵物身高體重最大最小參考值', '對戰排行榜'];
const candidates = document.querySelectorAll('body > nav, body > header, body > div, body > ul');
candidates.forEach(element => {
if (element.id === 'map' ||
element.classList.contains('leaflet-container') ||
isLeafletDescendant(element)) {
return;
}
const rect = element.getBoundingClientRect();
const text = element.textContent.replace(/\s+/g, '');
const navTextMatches = navTexts.filter(item => text.includes(item)).length;
const className = element.className.toString().toLowerCase();
const looksLikeTopBar = rect.top <= 80 &&
rect.width > window.innerWidth * 0.5 &&
rect.height > 20 &&
rect.height < 120;
const looksLikeSiteNav = navTextMatches >= 2 ||
className.includes('navbar') ||
className.includes('nav-bar') ||
className.includes('navigation');
if (looksLikeTopBar && looksLikeSiteNav) {
element.remove();
count++;
}
});
if (count > 0) {
console.log(`已清理 ${count} 個頂部導覽列`);
}
resizeMapToViewport();
return count;
}
// 持續清理監控
function startCleanupMonitor() {
const observer = new MutationObserver((mutations) => {
let needsCleanup = false;
let hasElementChanges = false;
mutations.forEach((mutation) => {
// 略過 Leaflet popup / pane 內部的變動,避免誤判清理
if (mutation.target && mutation.target.nodeType === 1 &&
isLeafletDescendant(mutation.target)) {
return;
}
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // 元素節點
if (isLeafletDescendant(node)) {
return;
}
hasElementChanges = true;
const rect = node.getBoundingClientRect();
const style = window.getComputedStyle(node);
// 檢測新增的底部元素
if ((style.position === 'fixed' || style.position === 'absolute') &&
(style.bottom === '0px' || parseInt(style.bottom) <= 50) &&
rect.width > 100) {
needsCleanup = true;
}
if (rect.top <= 80 &&
rect.width > window.innerWidth * 0.5 &&
rect.height > 20 &&
rect.height < 120) {
const text = node.textContent.replace(/\s+/g, '');
if (text.includes('首頁') && text.includes('官方消息')) {
needsCleanup = true;
}
}
if (node.matches('div') &&
(node.querySelector('h1, h2, h3, .adsbygoogle, ins.adsbygoogle') ||
node.textContent.includes('台灣寶可夢資訊站') ||
node.textContent.includes('TWPKInfo'))) {
needsCleanup = true;
}
}
});
});
if (hasElementChanges) {
scheduleToolbarTooltips();
}
if (needsCleanup) {
console.log('偵測到新列元素,準備清理...');
setTimeout(cleanupTopNavigation, 200);
setTimeout(cleanupBottomBar, 200);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log('清理監控已啟動');
}
// 阻止廣告腳本
function blockAdScripts() {
if (!window.adsbygoogle) {
window.adsbygoogle = [];
}
window.adsbygoogle.push = () => console.log('AdSense 已阻止');
}
// 檢查清理效果
function checkCleanupEffect() {
setTimeout(() => {
const map = document.querySelector('#map, .leaflet-container');
const pokemonElements = document.querySelectorAll('[src*="pokemon"], [class*="pokemon"]');
const leafletAttribution = document.querySelector('.leaflet-control-attribution');
// 檢查是否還有底部列存在
const remainingBottomElements = [];
document.querySelectorAll('div').forEach(div => {
if (div.id === 'map' ||
div.classList.contains('leaflet-container') ||
isLeafletDescendant(div)) {
return;
}
const rect = div.getBoundingClientRect();
const style = window.getComputedStyle(div);
if (((style.position === 'fixed' || style.position === 'absolute') &&
(style.bottom === '0px' || parseInt(style.bottom) <= 30)) ||
(rect.bottom >= window.innerHeight - 50 && rect.width > 200)) {
if (!div.classList.contains('leaflet-control-attribution') &&
!div.closest('.leaflet-control-attribution')) {
remainingBottomElements.push(div);
}
}
});
console.log('清理效果檢查:');
console.log('- 地圖:', map ? '正常' : '缺失');
console.log('- 寶可夢元素:', pokemonElements.length > 0 ? `${pokemonElements.length}` : '缺失');
console.log('- Leaflet 版權:', leafletAttribution ? '保留' : '缺失');
console.log('- 剩餘底部元素:', remainingBottomElements.length, remainingBottomElements.length > 0 ? '仍有底部列' : '底部乾淨');
if (remainingBottomElements.length > 0) {
console.log('發現剩餘底部元素,再次清理...');
setTimeout(cleanupBottomBar, 1000);
} else {
console.log('清理任務完成');
}
}, 3000);
}
// 主要初始化
function init() {
blockAdScripts();
addCleanupCSS();
startCleanupWhenReady();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startCleanupWhenReady, { once: true });
} else {
startCleanupWhenReady();
}
}
function startCleanupWhenReady() {
if (cleanupStarted) {
return;
}
if (!document.body) {
setTimeout(startCleanupWhenReady, 50);
return;
}
cleanupStarted = true;
performCleanup();
}
// 執行清理
function performCleanup() {
console.log('開始清理任務');
resizeMapToViewport();
applyToolbarTooltips();
cleanupTopNavigation();
cleanupBottomBar();
// 多次清理提高穩定性
setTimeout(() => applyToolbarTooltips(), 300);
setTimeout(() => cleanupTopNavigation(), 500);
setTimeout(() => cleanupBottomBar(), 1000);
setTimeout(() => applyToolbarTooltips(), 1500);
setTimeout(() => cleanupTopNavigation(), 2000);
setTimeout(() => cleanupBottomBar(), 2500);
setTimeout(() => cleanupBottomBar(), 4000);
setTimeout(() => applyToolbarTooltips(), 4500);
// 啟動持續監控
startCleanupMonitor();
// 檢查清理效果
checkCleanupEffect();
// 定期檢查
setInterval(() => {
let hasBottomBars = false;
document.querySelectorAll('div').forEach(div => {
if (div.id === 'map' ||
div.classList.contains('leaflet-container') ||
isLeafletDescendant(div)) {
return;
}
const rect = div.getBoundingClientRect();
const style = window.getComputedStyle(div);
if (((style.position === 'fixed' || style.position === 'absolute') &&
(style.bottom === '0px' || parseInt(style.bottom) <= 30)) ||
(rect.bottom >= window.innerHeight - 50 && rect.width > 200)) {
if (!div.classList.contains('leaflet-control-attribution')) {
hasBottomBars = true;
}
}
});
if (hasBottomBars) {
console.log('定期檢查:發現底部元素');
cleanupBottomBar();
}
}, 8000);
}
console.log('初始化 TWPKInfo 廣告清除器');
init();
})();