荒らしに使われないような安全なスクリプトを掲示します
自由に書いていってどうぞ
書き方は真似た方が良いかも
(導入は自己責任でお願いします)
https://hayabusa.open2ch.net/test/read.cgi/livejupiter/1757942123/
↑タンパーモンキー部?
自由に書いていってどうぞ
書き方は真似た方が良いかも
(導入は自己責任でお願いします)
https://hayabusa.open2ch.net/test/read.cgi/livejupiter/1757942123/
↑タンパーモンキー部?
jazap作者が作ったものは現在「https://greasyfork.org/ja」に移行中です
| + | 対応機種[greasyfork.orgから引用] |
Chrome: Tampermonkey デスクトップ
Firefox: Greasemonkey, Tampermonkey, or Violentmonkey デスクトップ・アンドロイド Safari: Tampermonkey or Userscripts デスクトップ・iOS Microsoft Edge: Tampermonkey デスクトップ・アンドロイド Opera: Tampermonkey or Violentmonkey デスクトップ Maxthon: Violentmonkey デスクトップ・アンドロイド AdGuard: (no additional software required) デスクトップ Gear: (アプリの追加不要) iOS Dolphin: Tampermonkey アンドロイド UC: Tampermonkey アンドロイド XBrowser アンドロイド |
Tampermonkey用
| + | Open2ch ID横に検索ボタンをすべてのIDに追加 |
| + | おんJの新着レスの番号が赤色になるのを無効化 |
// ==UserScript==
// @name Open2ch レス番号赤色表示無効化 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 最新レスの番号の赤色表示を無効にする // @author jazap // @match https://*.open2ch.net/* // @grant none // ==/UserScript== (function () { 'use strict'; const style = document.createElement('style'); style.textContent = ` nn { color: black !important; font-weight: normal !important; `; document.head.appendChild(style);
})();
|
| + | スレ内で初出のIDを赤く表示 |
// ==UserScript==
// @name Open2ch ID初出赤表示(l10/l50除外、) // @namespace http://tampermonkey.net/ // @version 1.0 // @description スレ内で初めて登場したIDを赤く表示(l10/l50 URLでは除外) // @author jazap // @match https://*.open2ch.net/* // @grant none // @require https://code.jquery.com/jquery-3.6.0.min.js // ==/UserScript== (function () { 'use strict'; const isLimitedView = location.href.includes('l10') || location.href.includes('l50') || location.href.includes('?id=') || location.href.includes('?q='); const seenIds = new Set(); const processed = new WeakSet(); function markFirstAppearance() { $('span._id').each(function () { if (processed.has(this)) return; const $span = $(this); const idVal = $span.attr('val'); if (!idVal) return; const $idLink = $span.find('a.id'); if (!seenIds.has(idVal)) { seenIds.add(idVal); if (!isLimitedView) { $idLink.css({ color: 'red', fontWeight: 'bold' }); } } processed.add(this); }); } $(document).ready(() => { markFirstAppearance(); }); const observer = new MutationObserver(() => { markFirstAppearance(); }); observer.observe(document.body, { childList: true, subtree: true });
)();
|
| + | imgurをアップロードするやつ※open2chのポップアップブロックをOFFにしないと作動しません |
// ==UserScript==
// @name 画像うp偽ボタン(最終レイアウト対策・エラー対応・ペースト乗っ取り・プレビュー機能強化版) // @namespace http://tampermonkey.net/ // @version 7.3 // @description Ctrl+Vまたはボタンクリックでの画像選択時に、サイトのレイアウトを維持した高速アップロードUIを生成します。!important指定でレイアウト崩れに強力に対応。 // @match https://*.open2ch.net/* // @match https://imgur.com/* // @grant none // ==/UserScript== (function() { 'use strict'; const hostname = window.location.hostname; //================================================================================ // Imgur.comのページで動作する部分(変更なし) //================================================================================ if (hostname.includes('imgur.com')) { console.log('[Imgur Receiver] Imgurページとして動作開始'); if (window.opener) window.opener.postMessage({ ready: true }, '*'); const errorObserver = new MutationObserver((mutations, obs) => { for (const mutation of mutations) { if (mutation.addedNodes.length) { const errorDialog = document.querySelector('.UploadLargeDialog-text'); if (errorDialog && errorDialog.textContent.includes('This image is too large to be uploaded anonymously')) { console.log('[Imgur Receiver] サイズ超過エラーを検知'); if (window.opener) window.opener.postMessage({ uploadError: '匿名でアップロードするには画像サイズが大きすぎます。' }, '*'); obs.disconnect(); setTimeout(() => window.close(), 100); return; } } } }); errorObserver.observe(document.body, { childList: true, subtree: true }); window.addEventListener('message', async (event) => { if (!event.data.file) return; try { const dataTransfer = new DataTransfer(); const blob = await (await fetch(event.data.file)).blob(); const file = new File([blob], 'upload.png', { type: blob.type }); dataTransfer.items.add(file); const input = document.querySelector('input[type="file"]'); if (input) { input.files = dataTransfer.files; input.dispatchEvent(new Event('change', { bubbles: true })); monitorForFinalUrl(event.source); } } catch (err) { console.error('[Imgur Receiver] エラー発生:', err); } }); function monitorForFinalUrl(source) { const observerTargetSelector = '.PostContent-imageWrapper-rounded img'; let observer; const timeout = setTimeout(() => { if (observer) { observer.disconnect(); errorObserver.disconnect(); }}, 15000); const findAndObserve = () => { const imgElement = document.querySelector(observerTargetSelector); if (imgElement) { observer = new MutationObserver(() => { const newSrc = imgElement.src; if (newSrc && !newSrc.startsWith('blob:')) { if (source) source.postMessage({ uploadedLink: newSrc }, '*'); clearTimeout(timeout); observer.disconnect(); errorObserver.disconnect(); setTimeout(() => window.close(), 100); } }); observer.observe(imgElement, { attributes: true, attributeFilter: ['src'] }); } else { setTimeout(findAndObserve, 200); } }; findAndObserve(); } return; } //================================================================================ // 元のページで動作する部分(★★★ 改良箇所 ★★★) //================================================================================ console.log('[Uploader Sender] 親ページとして動作'); let imgurTab = null; function startUpload(dataUrl) { if (imgurTab && !imgurTab.closed) { imgurTab.focus(); return; } imgurTab = window.open('https://imgur.com/upload', '_blank'); const readyListener = (event) => { if (event.source === imgurTab && event.data.ready) { if (imgurTab) imgurTab.postMessage({ file: dataUrl }, '*'); window.removeEventListener('message', readyListener); } }; window.addEventListener('message', readyListener); } // --- ペースト時専用のプレビューUIを生成し表示する関数 --- function showPastePreviewUI(dataUrl) { const existingUi = document.querySelector('.custom-paste-ui'); if (existingUi) existingUi.remove(); const uiWrapper = document.createElement('div'); uiWrapper.className = 'image-paste custom-paste-ui'; uiWrapper.style.cssText = ` background: rgb(221, 221, 221) !important; border-radius: 0px 0px 10px !important; padding: 5px !important; display: flex !important; align-items: center !important; margin-bottom: 5px !important; `; uiWrapper.innerHTML = ` <div style="border:2px dotted black; padding:2px; flex-shrink: 0;"> <a data-lightbox="d" href="${dataUrl}" target="_blank"> <img style="max-width:100px; max-height:100px; object-fit:cover; display: block;" src="${dataUrl}"> </a> </div> <div style="padding-left: 10px;"> <b>簡単画像コピペうp機能</b><br><br> ←この画像をuploadするっぺか? <div style="margin-top: 5px;"> <input class="paste-ok" type="button" value="画像うp"> <input class="paste-no" type="button" value="やめる"> </div> </div> `; let insertionSuccess = false; const textarea = document.querySelector('textarea#MESSAGE'); if (textarea && textarea.parentNode) { textarea.parentNode.insertBefore(uiWrapper, textarea.nextSibling); insertionSuccess = true; } if (!insertionSuccess) { const targetForm = document.querySelector('form#form1') || document.querySelector('form'); if (targetForm) { targetForm.insertBefore(uiWrapper, targetForm.firstChild); } else { document.body.insertBefore(uiWrapper, document.body.firstChild); } } const fakePasteBtn = uiWrapper.querySelector('.paste-ok'); const cancelBtn = uiWrapper.querySelector('.paste-no'); fakePasteBtn.addEventListener('click', () => { startUpload(dataUrl); uiWrapper.remove(); }); cancelBtn.addEventListener('click', () => uiWrapper.remove()); } // --- 1. 元から存在する「画像うp」ボタン (.gazo_up) の処理 --- const origBtnDiv = document.querySelector('.gazo_up'); if (origBtnDiv) { origBtnDiv.style.display = 'none'; const fakeDiv = document.createElement('div'); fakeDiv.title = '画像うp機能(高速版)'; fakeDiv.className = 'my_gazo_up'; fakeDiv.style.cssText = origBtnDiv.style.cssText; fakeDiv.style.display = 'inline-block'; const innerDiv = document.createElement('div'); innerDiv.style.cssText = origBtnDiv.firstElementChild.style.cssText; innerDiv.style.display = 'inline-block'; const btn = document.createElement('input'); btn.type = 'button'; btn.value = ''; btn.className = 'far noselect btgr my_image_bt'; btn.style.cssText = origBtnDiv.querySelector('input[type=button]').style.cssText; innerDiv.appendChild(btn); fakeDiv.appendChild(innerDiv); origBtnDiv.parentNode.insertBefore(fakeDiv, origBtnDiv.nextSibling); btn.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.style.display = 'none'; document.body.appendChild(input); input.click(); input.onchange = () => { const file = input.files[0]; if (!file) { input.remove(); return; } const reader = new FileReader(); // ★★★ 改良点:プレビューを表示せず、即座にアップロードを開始 ★★★ reader.onload = (readerEvent) => startUpload(readerEvent.target.result); reader.readAsDataURL(file); input.remove(); }; }); } // --- 2. Ctrl+Vイベントを乗っ取り、独自UIを生成する処理 --- window.addEventListener('paste', (event) => { const items = (event.clipboardData || event.originalEvent.clipboardData).items; let imageFile = null; for (const item of items) { if (item.type.indexOf('image') !== -1) { imageFile = item.getAsFile(); break; } } if (imageFile) { event.preventDefault(); event.stopPropagation(); console.log('[Uploader Sender] 画像ペーストを検知。カスタムUIを生成します。'); const reader = new FileReader(); reader.onload = (e) => { const dataUrl = e.target.result; showPastePreviewUI(dataUrl); }; reader.readAsDataURL(imageFile); } }, true); // --- 3. Imgurタブからのコールバック処理 --- window.addEventListener('message', (event) => { if (!event.data.uploadError && !event.data.uploadedLink) return; const insertionPoint = document.querySelector('.fav_aa'); if (!insertionPoint || !insertionPoint.parentNode) { console.error('[Uploader Sender] アップロード後のプレビュー挿入位置(.fav_aa)が見つかりません。'); return; } if (event.data.uploadError) { const errorContainer = document.createElement('div'); errorContainer.innerHTML = `<div class="upload_error" style="display: block; background: rgb(255, 221, 221); color: red; padding: 3px; margin: 0 0 5px 0; border: 1px solid red;">${event.data.uploadError}</div>`; insertionPoint.parentNode.insertBefore(errorContainer, insertionPoint.nextSibling); setTimeout(() => errorContainer.remove(), 5000); } if (event.data.uploadedLink) { const uploadedLink = event.data.uploadedLink; const previewContainer = document.createElement('div'); previewContainer.innerHTML = ` <div class="upload_picinfo" style="display: block; background: rgb(221, 221, 221); padding: 3px; margin: 0 0 5px 0;"> <div class="uppic dlink hide" style="display: block;"> <a target="_blank" href="${uploadedLink}"><img class="upld_imgur" style="width: 80px; height: 80px; object-fit: cover;" src="${uploadedLink}"></a> <font size="2"><a class="cancel_uppic" href="#" url="${uploadedLink}">取消</a></font> </div> </div>`; insertionPoint.parentNode.insertBefore(previewContainer, insertionPoint.nextSibling); let targetTextarea = document.querySelector('textarea#MESSAGE'); if (targetTextarea) { const urlToInsert = (targetTextarea.value.trim() ? '\n' : '') + uploadedLink + '\n'; targetTextarea.focus(); targetTextarea.value += urlToInsert; targetTextarea.scrollTop = targetTextarea.scrollHeight; } const cancelBtn = previewContainer.querySelector('.cancel_uppic'); cancelBtn.addEventListener('click', (e) => { e.preventDefault(); previewContainer.remove(); if (targetTextarea) { const urlToRemove = uploadedLink + '\n'; targetTextarea.value = targetTextarea.value.replace('\n' + urlToRemove, '\n').replace(urlToRemove, ''); } }); } if (imgurTab && !imgurTab.closed) setTimeout(() => { imgurTab.close(); }, 150); imgurTab = null; });
)();
|
| + | 色々な画像投稿サイトをプレビュー表示するやつ |
// ==UserScript==
// @name 完成 (修正版) - HTML構造の精密な再現 + キャッシュ機能 + ツールチップ無効化 + レス番号取得修正 + Imgur GIF互換性改善 v6 // @namespace yournamespace // @version 5.0.0.0 // @description 各種画像サイトのリンクをLightboxで表示。Imgur GIFを自動再生し、右側にサムネイル表示ボタン付きで展開。MP4にミニプレイヤーボタン追加。連続する画像を横並び表示。 // @match https://*.open2ch.net/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // ==/UserScript== (function () { 'use strict'; let imageDatabase = new Map(); let imageCounter = 0; let globalEscHandler = null; // GIFの共通サイズ設定 - サムネイルも同じサイズに統一 const GIF_DISPLAY_SIZE = '300px'; // デバッグログ関数 function debugLog(message, data = null) { console.log(`[Imgur GIF Debug] ${message}`, data || ''); // キャッシュ管理クラス
class ThumbnailCache {
constructor() {
this.CACHE_DURATION = 10 * 24 * 60 * 60 * 1000; // 10日間(ミリ秒)
this.CACHE_PREFIX = 'thumb_cache_';
this.cleanupOldEntries();
}
cleanupOldEntries() {
try {
const keys = GM_listValues();
const now = Date.now();
keys.forEach(key => {
if (key.startsWith(this.CACHE_PREFIX)) {
try {
const data = JSON.parse(GM_getValue(key, '{}'));
if (!data.timestamp || (now - data.timestamp) > this.CACHE_DURATION) {
GM_deleteValue(key);
}
} catch (e) {
GM_deleteValue(key);
}
}
});
} catch (e) {
console.warn('Cache cleanup failed:', e);
}
}
generateKey(url) {
let hash = 0;
for (let i = 0; i < url.length; i++) {
const char = url.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return this.CACHE_PREFIX + Math.abs(hash).toString(36);
}
get(url) {
try {
const key = this.generateKey(url);
const cached = GM_getValue(key, null);
if (!cached) return null;
const data = JSON.parse(cached);
if (!data.timestamp || (Date.now() - data.timestamp) > this.CACHE_DURATION) {
GM_deleteValue(key);
return null;
}
return data.imageUrl;
} catch (e) {
console.warn('Cache get failed:', e);
return null;
}
}
set(url, imageUrl) {
try {
if (/imgur/i.test(url)) return;
const key = this.generateKey(url);
const data = { imageUrl: imageUrl, timestamp: Date.now(), originalUrl: url };
GM_setValue(key, JSON.stringify(data));
} catch (e) {
console.warn('Cache set failed:', e);
}
}
shouldCache(url) {
return /tadaup\.jp|ul\.h3z\.jp|ibb\.co|postimg\.cc|freeimage\.host|iili\.io|funakamome\.com/i.test(url) && !/imgur/i.test(url);
}
}
const thumbnailCache = new ThumbnailCache(); function closeLightbox(e) {
if (e) e.preventDefault();
const existingLightbox = document.getElementById('lightbox');
if (existingLightbox) existingLightbox.remove();
const existingOverlay = document.getElementById('lightboxOverlay');
if (existingOverlay) existingOverlay.remove();
if (globalEscHandler) {
document.removeEventListener('keydown', globalEscHandler);
globalEscHandler = null;
}
}
function generatePostLink(postNumber) {
const currentUrl = window.location.href;
const baseUrl = currentUrl.split('#')[0];
return `${baseUrl}#${postNumber}`;
}
function addCustomCSS() {
if (document.getElementById('custom-lb-styles')) return;
const style = document.createElement('style');
style.id = 'custom-lb-styles';
style.textContent = `
.lightboxOverlay { position: absolute; top: 0; left: 0; z-index: 99990; background-color: black; opacity: 0.85; display: none; }
#lightbox { position: absolute; left: 0; width: 100%; z-index: 99991; text-align: center; line-height: 0; font-family: "Lucida Grande", sans-serif; }
.gm-no-tooltip { position: relative; }
.gm-no-tooltip::before, .gm-no-tooltip::after { display: none !important; }
.gm-imgur-gif-container { display: inline-block; position: relative; }
.gm-imgur-gif-wrapper { position: relative; display: inline-block; }
.gm-thumbnail-button {
position: absolute;
bottom: 5px;
right: 5px;
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: opacity 0.2s;
padding: 0;
line-height: 1;
z-index: 10;
}
.gm-thumbnail-button:hover {
opacity: 0.7;
background: rgba(0, 0, 0, 0.9);
}
.gm-thumbnail-button:active {
opacity: 0.5;
}
.gm-media-wrapper {
display: inline-flex;
align-items: flex-start;
gap: 10px;
vertical-align: top;
}
.gm-thumbnail-container {
display: inline-block;
margin-left: 10px;
vertical-align: middle;
}
.gm-thumbnail-img {
object-fit: contain;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
/* 連続画像の横並び表示用 */
.gm-image-row {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: flex-start;
margin: 10px 0;
}
.gm-image-row .gm-media-wrapper,
.gm-image-row .gm-media-embed-container {
display: inline-flex;
vertical-align: top;
}
`;
document.head.appendChild(style);
}
function showLightbox(imageIndex) {
const imageData = imageDatabase.get(imageIndex);
if (!imageData) return;
const { imageUrl, postNumber, originalUrl } = imageData;
closeLightbox(); const isImgur = /imgur/i.test(originalUrl);
let serviceName = 'image';
if (isImgur) serviceName = 'imgur';
else if (originalUrl.includes('ibb.co')) serviceName = 'img.bb';
else if (originalUrl.includes('tadaup.jp')) serviceName = 'tadaup';
else if (originalUrl.includes('ul.h3z.jp')) serviceName = 'h3z.jp';
else if (originalUrl.includes('postimg.cc')) serviceName = 'postimg.cc';
else if (originalUrl.includes('freeimage.host') || originalUrl.includes('iili.io')) serviceName = 'freeimage.host';
else if (originalUrl.includes('funakamome.com')) serviceName = 'funakamome.com';
const overlay = document.createElement('div');
overlay.id = 'lightboxOverlay';
overlay.className = 'lightboxOverlay';
overlay.style.width = '100%';
overlay.style.height = document.documentElement.scrollHeight + 'px';
overlay.style.display = 'block';
document.body.appendChild(overlay);
const lightbox = document.createElement('div');
lightbox.id = 'lightbox';
lightbox.className = 'lightbox';
lightbox.style.display = 'none';
let detailsHTML;
if (isImgur) {
let board = 'unknown', threadId = '0';
const urlMatch = window.location.href.match(/test\/read\.cgi\/([^\/]+)\/(\d+)/);
if (urlMatch) { board = urlMatch[1]; threadId = urlMatch[2]; }
const pid = `${board}-${threadId}-${postNumber}`;
const imgurPageUrl = imageUrl.replace(/\.(jpe?g|png|gif)$/i, '');
const twitterPostUrl = `https://${window.location.hostname}/test/read.cgi/${board}/${threadId}/${postNumber}-`;
detailsHTML = `
<div class="lb-details" style="min-width:300px">
<span class="lb-caption" style="display: inline; cursor: pointer;">
<u class="lb-ank" resnum="${postNumber}" href="#">>>${postNumber}</u>
</span>
<span class="lb-number" style="">全${imageDatabase.size}件中、${imageIndex}件目</span>
<span style="clear: left;display: block;" class="lb-save">
<div>
<a n="${postNumber}" pid="${pid}" class="lb-korabo-link gm-no-tooltip" href="${imageUrl}"><font size="2" color="white">コラボ</font></a>
<a class="lb-icon gm-no-tooltip" href="${imageUrl}"><font size="2" color="white">アイコン</font></a>
<a class="lb-search gm-no-tooltip" href="https://lens.google.com/uploadbyurl?hl=ja&url=${encodeURIComponent(imageUrl)}"><font size="2" color="white">画像検索</font></a>
<a class="lb-open-link gm-no-tooltip" href="${imageUrl}"><font size="2" color="white">直URL</font></a>
<a class="lb-open-link gm-no-tooltip" href="${imgurPageUrl}"><font size="2" color="white">imgur</font></a>
</div>
<div style="margin-top:5px">
<a class="lb-twiter gm-no-tooltip" url="${twitterPostUrl}" href="#"><font size="2" color="white">Twitterに貼る</font></a>
</div>
</span>
<span class="lb-korabo"></span>
</div>
`;
} else {
detailsHTML = `
<div class="lb-details" style="min-width:300px">
<span class="lb-caption" style="display: inline; cursor: pointer;">
<u class="lb-ank" resnum="${postNumber}" href="#">>>${postNumber}</u>
</span>
<span style="clear: left;display: block;" class="lb-save">
<div>
<a class="lb-search gm-no-tooltip" href="https://lens.google.com/uploadbyurl?hl=ja&url=${encodeURIComponent(imageUrl)}"><font size="2" color="white">画像検索</font></a>
<a class="lb-open-link gm-no-tooltip" href="${imageUrl}"><font size="2" color="white">直URL</font></a>
<a class="lb-service-link gm-no-tooltip" href="${originalUrl}"><font size="2" color="white">${serviceName}</font></a>
</div>
</span>
<span class="lb-korabo"></span>
</div>
`;
}
lightbox.innerHTML = `
<div class="lb-outerContainer">
<div class="lb-container">
<img class="lb-image" src="">
<div class="lb-nav">
<a class="lb-prev" href=""></a>
<a class="lb-next" href=""></a>
</div>
<div class="lb-loader" style="display: none;"><a class="lb-cancel"></a></div>
</div>
</div>
<div class="lb-dataContainer">
<div class="lb-data">
${detailsHTML}
<div class="lb-closeContainer"><a class="lb-close"></a></div>
</div>
</div>
`;
document.body.appendChild(lightbox);
const outerContainer = lightbox.querySelector('.lb-outerContainer');
const dataContainer = lightbox.querySelector('.lb-dataContainer');
const imageEl = lightbox.querySelector('.lb-image');
const prevLink = lightbox.querySelector('.lb-prev');
const nextLink = lightbox.querySelector('.lb-next');
const closeButton = lightbox.querySelector('.lb-close');
const loader = lightbox.querySelector('.lb-loader');
const ankLink = lightbox.querySelector('.lb-ank');
globalEscHandler = (e) => {
if (e.key === 'Escape') { closeLightbox(e); }
};
document.addEventListener('keydown', globalEscHandler);
overlay.addEventListener('click', closeLightbox);
closeButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
closeLightbox();
});
ankLink.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
closeLightbox();
window.location.href = generatePostLink(postNumber);
});
lightbox.addEventListener('click', e => {
if (e.target.id === 'lightbox') {
closeLightbox();
} else {
e.stopPropagation();
}
});
lightbox.querySelectorAll('.lb-save a').forEach(a => {
if (a.href && a.getAttribute('href') !== '#') {
a.target = '_blank'; a.rel = 'noopener noreferrer';
}
a.removeAttribute('title'); a.title = '';
a.addEventListener('mouseenter', (e) => {
e.target.removeAttribute('title'); e.target.title = '';
});
});
if (imageIndex > 1) {
prevLink.style.display = 'block';
prevLink.onclick = (e) => { e.preventDefault(); e.stopPropagation(); showLightbox(imageIndex - 1); };
} else {
prevLink.style.display = 'none';
}
if (imageIndex < imageDatabase.size) {
nextLink.style.display = 'block';
nextLink.onclick = (e) => { e.preventDefault(); e.stopPropagation(); showLightbox(imageIndex + 1); };
} else {
nextLink.style.display = 'none';
}
loader.style.display = 'block'; const tempImg = new Image();
tempImg.onload = function() {
const maxWidth = document.documentElement.clientWidth * 0.9;
const maxHeight = document.documentElement.clientHeight * 0.9 - 80;
let imgWidth = this.naturalWidth; let imgHeight = this.naturalHeight;
const ratio = Math.min(maxWidth / imgWidth, maxHeight / imgHeight, 1);
imgWidth = Math.round(imgWidth * ratio);
imgHeight = Math.round(imgHeight * ratio);
const framePadding = 8;
outerContainer.style.width = `${imgWidth + framePadding}px`;
outerContainer.style.height = `${imgHeight + framePadding}px`;
dataContainer.style.width = `${imgWidth + framePadding}px`;
imageEl.style.width = `${imgWidth}px`;
imageEl.style.height = `${imgHeight}px`;
imageEl.src = imageUrl;
imageEl.style.display = 'block';
lightbox.querySelector('.lb-nav').style.display = 'block';
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const clientHeight = document.documentElement.clientHeight;
let top = scrollTop + (clientHeight - (imgHeight + framePadding + dataContainer.offsetHeight)) / 2;
if (top < scrollTop + 10) top = scrollTop + 10;
lightbox.style.top = `${top}px`;
lightbox.style.left = `0px`;
loader.style.display = 'none';
lightbox.style.display = 'block';
};
tempImg.onerror = function() {
loader.textContent = '画像の読み込みに失敗しました。';
loader.style.color = '#ff8a8a';
loader.style.display = 'block';
};
tempImg.src = imageUrl;
}
function getPostNumber(element) {
const postContainerSelectors = [
'article[id]', 'div.post[id]', 'div[data-res-id]', 'dl[val]',
'div.thread-post', '.post-container', '.message',
];
const postBlock = element.closest(postContainerSelectors.join(', '));
if (postBlock) {
const dataAttributes = ['data-res-id', 'data-res', 'data-num', 'data-id'];
for (const attr of dataAttributes) {
if (postBlock.hasAttribute(attr)) return postBlock.getAttribute(attr);
}
if (postBlock.id) {
const match = postBlock.id.match(/\d+/);
if (match) return match[0];
}
if (postBlock.hasAttribute('val')) return postBlock.getAttribute('val');
const numElementSelectors = ['.post-number', '.res-number', '.post-id', 'a.num', '.num b'];
const numElement = postBlock.querySelector(numElementSelectors.join(', '));
if (numElement) {
const match = numElement.textContent.match(/\d+/);
if (match) return match[0];
}
}
const ddElement = element.closest('dd[rnum]');
if (ddElement && ddElement.hasAttribute('rnum')) return ddElement.getAttribute('rnum');
const dtElement = element.closest('dl')?.querySelector('dt[res]');
if (dtElement && dtElement.hasAttribute('res')) return dtElement.getAttribute('res');
const hash = window.location.hash.match(/\d+/);
return hash ? hash[0] : 'N/A';
}
function bindLightboxOnClick(imageElement, imageUrl, originalUrl) {
if (imageElement.dataset.gmlbProcessed) return;
imageCounter++;
const currentImageIndex = imageCounter;
const postNumber = getPostNumber(imageElement) || currentImageIndex;
imageDatabase.set(currentImageIndex, { imageUrl, postNumber, originalUrl });
imageElement.addEventListener('click', (e) => {
e.stopPropagation(); e.preventDefault();
showLightbox(currentImageIndex);
}, true);
imageElement.dataset.gmlbProcessed = '1';
}
function insertThumbnail(linkElement, imageUrl, originalUrl, isFromCache = false) {
linkElement.classList.add('gm-media-embed-container');
const img = document.createElement('img');
img.src = imageUrl;
img.style.cssText = `max-width:${GIF_DISPLAY_SIZE}; max-height:${GIF_DISPLAY_SIZE}; object-fit:contain; cursor:pointer; border:1px solid #ddd; border-radius:4px;`;
img.alt = "thumbnail";
img.onerror = () => { img.alt = 'サムネイル読み込み失敗'; };
const container = document.createElement('div');
container.style.display = 'inline-block';
container.appendChild(img);
linkElement.innerHTML = '';
linkElement.appendChild(container);
linkElement.style.display = 'inline-block';
linkElement.onclick = e => e.preventDefault();
linkElement.removeAttribute('title');
linkElement.onmouseover = (e) => {
e.stopPropagation(); e.target.removeAttribute('title');
return false;
};
bindLightboxOnClick(img, imageUrl, originalUrl);
}
function expandGalleryLink(a) {
const servicePageUrl = a.href;
const cachedImage = thumbnailCache.get(servicePageUrl);
if (cachedImage) {
insertThumbnail(a, cachedImage, servicePageUrl, true);
return;
}
GM_xmlhttpRequest({
method: "GET", url: servicePageUrl,
onload: (res) => {
if (res.status === 200) {
const match = res.responseText.match(/<meta property="og:image" content="([^"]+)"/);
if (match) {
const imgUrl = match[1];
thumbnailCache.set(servicePageUrl, imgUrl);
insertThumbnail(a, imgUrl, servicePageUrl);
}
}
}
});
}
function expandDirectLink(a) {
const imgUrl = a.href;
const cachedImage = thumbnailCache.get(imgUrl);
if (cachedImage) {
insertThumbnail(a, cachedImage, imgUrl, true);
return;
}
thumbnailCache.set(imgUrl, imgUrl);
insertThumbnail(a, imgUrl, imgUrl);
}
function expandTadaupLink(a) {
const originalUrl = a.href;
const existingImg = a.querySelector('img');
if (existingImg && /tadaup\.jp/i.test(existingImg.src) && /\.(jpg|jpeg|png|gif)$/i.test(existingImg.src)) {
bindLightboxOnClick(existingImg, existingImg.src, originalUrl);
a.onclick = e => e.preventDefault();
return;
}
if (/\.(jpg|jpeg|png|gif)$/i.test(originalUrl)) {
expandDirectLink(a);
}
}
function embedImgurMp4(element) {
if (element.dataset.mp4Processed) return;
element.dataset.mp4Processed = '1';
const directUrl = element.href;
const imgurPageUrl = directUrl.replace(/\.mp4$/i, '');
const container = document.createElement('div');
container.className = 'gm-media-embed-container';
container.style.cssText = 'display: inline-flex; align-items: center; gap: 10px; vertical-align: middle;';
const video = document.createElement('video');
video.src = directUrl;
video.style.cssText = 'max-width:350px; max-height:350px; object-fit:contain; border:1px solid #ddd; border-radius:4px;';
video.autoplay = true;
video.loop = true;
video.muted = true;
video.playsInline = true;
video.controls = true;
video.onerror = () => { console.error('Failed to load video:', directUrl); };
const linkContainer = document.createElement('div');
linkContainer.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start; font-size: 12px;';
const directLink = document.createElement('a');
directLink.href = directUrl;
directLink.textContent = '直URL';
directLink.target = '_blank';
directLink.rel = 'noopener noreferrer';
directLink.addEventListener('click', (e) => e.stopPropagation());
const imgurLink = document.createElement('a');
imgurLink.href = imgurPageUrl;
imgurLink.textContent = 'imgur';
imgurLink.target = '_blank';
imgurLink.rel = 'noopener noreferrer';
imgurLink.addEventListener('click', (e) => e.stopPropagation());
linkContainer.appendChild(directLink);
linkContainer.appendChild(imgurLink);
container.appendChild(video);
container.appendChild(linkContainer);
if (element.parentNode) {
element.parentNode.insertBefore(container, element);
element.remove();
}
}
function replaceImgurGifWithNativeCompatibility(a) {
if (a.dataset.gifProcessed) return;
a.dataset.gifProcessed = '1';
const originalHref = a.href; // 全体を囲むコンテナ (Flexbox用)
const flexContainer = document.createElement('div');
flexContainer.className = 'gm-media-wrapper';
flexContainer.style.cssText = 'display: inline-flex; align-items: flex-start; vertical-align: top;';
// 左側の自動再生GIFとボタンのラッパー
const wrapper = document.createElement('div');
wrapper.className = 'gm-imgur-gif-wrapper';
wrapper.style.cssText = 'display: inline-block; position: relative;';
// 自動再生用のGIF画像
const autoplayImg = document.createElement('img');
autoplayImg.src = originalHref;
autoplayImg.style.cssText = `max-width:${GIF_DISPLAY_SIZE}; max-height:${GIF_DISPLAY_SIZE}; object-fit:contain; border:1px solid #ddd; border-radius:4px; display: block;`;
autoplayImg.alt = "Imgur GIF (Auto-playing)";
// サムネイル表示ボタン(📦の見た目)- 画像の右下に配置
const thumbnailButton = document.createElement('button');
thumbnailButton.className = 'gm-thumbnail-button';
thumbnailButton.textContent = '📦';
thumbnailButton.type = 'button';
thumbnailButton.title = 'サムネイル表示';
// サムネイル表示用のコンテナ(最初は非表示)- 画像の右に配置
const thumbnailContainer = document.createElement('div');
thumbnailContainer.className = 'gm-thumbnail-container';
thumbnailContainer.style.display = 'none';
let thumbnailLoaded = false; // サムネイル表示ボタンクリック時の処理
thumbnailButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (thumbnailContainer.style.display === 'none') {
if (!thumbnailLoaded) {
const thumbnailUrl = originalHref.replace(/\.gif$/i, 'm.gif');
const thumbLink = document.createElement('a');
thumbLink.href = originalHref;
thumbLink.addEventListener('click', (clickEvent) => {
clickEvent.preventDefault();
clickEvent.stopPropagation();
}, true);
const thumbnailImg = document.createElement('img');
thumbnailImg.className = 'gm-thumbnail-img';
thumbnailImg.src = thumbnailUrl;
thumbnailImg.alt = 'サムネイル';
// サムネイルも自動再生GIFと同じサイズに統一
thumbnailImg.style.cssText = `max-width:${GIF_DISPLAY_SIZE}; max-height:${GIF_DISPLAY_SIZE}; object-fit:contain; border:1px solid #ddd; border-radius:4px; cursor:pointer;`;
thumbnailImg.onerror = () => {
console.error('Failed to load thumbnail:', thumbnailUrl);
thumbnailImg.alt = 'サムネイル読み込み失敗';
};
// Lightbox機能を付与
bindLightboxOnClick(thumbnailImg, originalHref, originalHref);
thumbLink.appendChild(thumbnailImg);
thumbnailContainer.appendChild(thumbLink);
thumbnailLoaded = true;
}
thumbnailContainer.style.display = 'inline-block';
thumbnailButton.textContent = '📁';
thumbnailButton.title = 'サムネイル非表示';
} else {
thumbnailContainer.style.display = 'none';
thumbnailButton.textContent = '📦';
thumbnailButton.title = 'サムネイル表示';
}
});
// 要素を組み立て
wrapper.appendChild(autoplayImg);
wrapper.appendChild(thumbnailButton);
flexContainer.appendChild(wrapper);
flexContainer.appendChild(thumbnailContainer);
// 元の要素と置き換え
if (a.parentNode) {
a.parentNode.insertBefore(flexContainer, a);
a.remove();
} else {
return;
}
// 自動再生画像にもLightbox機能を付与(UserScriptの独自Lightbox用)
bindLightboxOnClick(autoplayImg, originalHref, originalHref);
}
// 連続する画像URLを検出して横並び表示する機能
function groupConsecutiveImages() {
const links = document.querySelectorAll('a[href]');
const imagePattern = /\.(jpe?g|png|gif|webp)$/i;
const servicePattern = /imgur|tadaup\.jp|ul\.h3z\.jp|ibb\.co|postimg\.cc|freeimage\.host|iili\.io|funakamome\.com/i;
let consecutiveGroups = [];
let currentGroup = [];
// 画像リンクを順次チェックして連続するものをグループ化
for (let i = 0; i < links.length; i++) {
const link = links[i];
const href = link.href;
// 既に処理済み、またはLightbox関連要素はスキップ
if (link.closest('#lightbox, #lightboxOverlay, .lb-dataContainer, .gm-media-embed-container, .gm-imgur-gif-wrapper, .gm-image-row')
|| link.dataset.gmlbProcessed) {
if (currentGroup.length > 1) {
consecutiveGroups.push([...currentGroup]);
}
currentGroup = [];
continue;
}
// 画像URLまたはサポートするサービスのリンクかチェック
const isImageUrl = imagePattern.test(href) || servicePattern.test(href);
if (isImageUrl) {
if (currentGroup.length === 0) {
currentGroup.push(link);
} else {
const lastLink = currentGroup[currentGroup.length - 1];
// より柔軟な連続性チェック
if (isConsecutiveImage(lastLink, link)) {
currentGroup.push(link);
} else {
// 距離が離れている場合は新しいグループを開始
if (currentGroup.length > 1) {
consecutiveGroups.push([...currentGroup]);
}
currentGroup = [link];
}
}
} else {
// 画像以外のリンクに遭遇したら現在のグループを終了
if (currentGroup.length > 1) {
consecutiveGroups.push([...currentGroup]);
}
currentGroup = [];
}
}
// 最後のグループも追加
if (currentGroup.length > 1) {
consecutiveGroups.push(currentGroup);
}
// 各グループを横並び表示に変換
consecutiveGroups.forEach(group => {
if (group.length > 1) {
createImageRow(group);
}
});
}
// 2つの画像リンクが連続しているかどうかを判定
function isConsecutiveImage(elem1, elem2) {
// DOM上で隣接しているかチェック
if (areAdjacentInDOM(elem1, elem2)) {
return true;
}
// 物理的な距離もチェック(改行があっても近い場合)
const distance = getElementDistance(elem1, elem2);
if (distance < 100) { // 100px以内なら連続とみなす(より厳密に)
return true;
}
// 同じ段落内にある場合もチェック
if (areInSameParagraph(elem1, elem2)) {
return true;
}
return false; } // DOM上で隣接している(間にテキストノードや<br>のみ)かチェック
function areAdjacentInDOM(elem1, elem2) {
let current = elem1.nextSibling;
let textOnlyBetween = true;
while (current && current !== elem2) {
// テキストノード、改行、空白のみの場合は連続とみなす
if (current.nodeType === Node.TEXT_NODE) {
// 空白や改行のみなら継続
if (current.textContent.trim() !== '') {
textOnlyBetween = false;
break;
}
} else if (current.nodeType === Node.ELEMENT_NODE) {
// <br>タグや空白のみの要素なら継続
if (current.tagName === 'BR' ||
(current.textContent && current.textContent.trim() === '')) {
// 継続
} else {
textOnlyBetween = false;
break;
}
}
current = current.nextSibling;
}
return current === elem2 && textOnlyBetween; } // 同じ段落内にあるかチェック
function areInSameParagraph(elem1, elem2) {
const para1 = elem1.closest('p, dd, div.message, .post-content');
const para2 = elem2.closest('p, dd, div.message, .post-content');
return para1 && para2 && para1 === para2; } // 要素間の距離を計算(縦方向の距離を重視)
function getElementDistance(elem1, elem2) {
const rect1 = elem1.getBoundingClientRect();
const rect2 = elem2.getBoundingClientRect();
// 縦方向の距離を計算
const verticalDistance = Math.abs(rect2.top - rect1.bottom);
return verticalDistance; } // 連続する画像を横並び表示するコンテナを作成
function createImageRow(imageLinks) {
if (imageLinks.length < 2) return;
const firstLink = imageLinks[0];
const rowContainer = document.createElement('div');
rowContainer.className = 'gm-image-row';
// 最初の画像の前にコンテナを挿入
firstLink.parentNode.insertBefore(rowContainer, firstLink);
// 各画像をコンテナに移動
imageLinks.forEach(link => {
const wrapper = document.createElement('div');
wrapper.style.cssText = 'display: inline-block; vertical-align: top;';
wrapper.appendChild(link);
rowContainer.appendChild(wrapper);
});
}
function processLinks() {
const links = document.querySelectorAll('a[href]');
const baseDomainsToIgnore = ['ul.h3z.jp', 'tadaup.jp', 'ibb.co', 'i.ibb.co', 'i.postimg.cc', 'postimg.cc', 'freeimage.host', 'iili.io', 'funakamome.com'];
links.forEach(a => {
if (a.closest('#lightbox, #lightboxOverlay, .lb-dataContainer, .gm-media-embed-container, .gm-imgur-gif-wrapper, .gm-thumbnail-button, .gm-image-row') || a.dataset.gmlbProcessed) return;
const innerImg = a.querySelector('img');
if (innerImg && innerImg.dataset.gmlbProcessed) return;
const href = a.href;
try {
const url = new URL(href);
if (baseDomainsToIgnore.includes(url.hostname) && (url.pathname === '/' || url.pathname === '')) {
a.dataset.gmlbProcessed = '1'; return;
}
} catch (e) { return; }
const existingImg = a.querySelector('img');
// Imgur GIF処理
if ((existingImg && /imgur/i.test(existingImg.className) && /\.gif/i.test(href)) || /i\.imgur\.com\/[0-9A-Za-z]+\.gif/i.test(href)) {
replaceImgurGifWithNativeCompatibility(a);
return;
}
if (!existingImg && /i\.imgur\.com\/[0-9A-Za-z]+\.mp4/i.test(href)) {
embedImgurMp4(a); return;
}
if (existingImg && /i\.imgur\.com\//.test(href) && /\.(jpe?g|png)$/i.test(href)) {
bindLightboxOnClick(existingImg, href, href);
a.onclick = e => e.preventDefault(); a.dataset.gmlbProcessed = '1'; return;
}
const isTadaup = /tadaup\.jp/.test(href) || (existingImg && /tadaup\.jp/.test(existingImg.src));
const isH3z = /ul\.h3z\.jp/.test(href);
const isIbbDirect = /^https?:\/\/i\.ibb\.co\//.test(href);
const isPostimgDirect = /i\.postimg\.cc/.test(href);
const isIilioDirect = /iili\.io/.test(href);
const isFunakamome = /funakamome\.com/.test(href);
const isIbbGallery = /^https?:\/\/ibb\.co\/[0-9A-Za-z]+/.test(href);
const isPostimgGallery = /^https?:\/\/postimg\.cc\/[0-9A-Za-z]+/.test(href);
const isFreeimageGallery = /^https?:\/\/freeimage\.host\/i\/[0-9A-Za-z]+/.test(href);
if (isIbbDirect || isPostimgDirect || isH3z || isIilioDirect || isFunakamome) {
expandDirectLink(a);
} else if (isIbbGallery || isPostimgGallery || isFreeimageGallery) {
expandGalleryLink(a);
} else if (isTadaup) {
expandTadaupLink(a);
}
a.dataset.gmlbProcessed = '1';
});
// 連続する画像の横並び表示処理
setTimeout(() => {
groupConsecutiveImages();
}, 500);
}
function disableTooltips() {
document.addEventListener('mouseover', function(e) {
if (e.target.matches('.lb-save a, .gm-no-tooltip, .gm-media-embed-container a, .gm-thumbnail-button')) {
e.target.removeAttribute('title'); e.target.title = '';
}
}, true);
setTimeout(() => {
document.querySelectorAll('.lb-save a, .gm-no-tooltip, .gm-thumbnail-button').forEach(el => {
el.removeAttribute('title'); el.title = '';
});
}, 100);
}
// サイト固有のlightbox検出と互換性向上
function enhanceSiteCompatibility() {
// DOM変更を監視して、動的に追加されるリンクも処理
const observer = new MutationObserver((mutations) => {
let needsProcessing = false;
mutations.forEach(mutation => {
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('a[href]') || node.querySelector('a[href]')) {
needsProcessing = true;
}
}
});
}
});
if (needsProcessing) {
// 処理が重複しないように少し遅延させる
setTimeout(processLinks, 200);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// 初期化処理
function init() {
addCustomCSS();
processLinks();
disableTooltips();
enhanceSiteCompatibility();
}
// ページの読み込み状態に応じて実行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
|
| + | !akuコマンドが一定以上出現してたら警告 |
// ==UserScript==//
// @name open2ch !aku Warning (作:Copilot&GPT-5) // @namespace https://example.local/ // @version 0.0 // @description スレ内の!akuコマンド出現回数が閾値を超えたら1レス目下に小さく警告を表示 // @match *://*.open2ch.net/test/read.cgi/*/* // @match *://hayabusa.open2ch.net/test/read.cgi/*/* // @grant none // ==/UserScript== (function () { 'use strict'; const APU_CMD = '!aku'; const THRESHOLD = 1; //ここの数値以上のアク禁回数なら検出 const CHECK_DELAY = 300; const STYLE_ID = 'tamper-aku-warning-style'; function addStyles() { if (document.getElementById(STYLE_ID)) return; const css = ` .aku-warning { font-size: 11px; color: #8a2b2b; background: rgba(255,235,235,0.95); border: 1px solid rgba(138,43,43,0.25); padding: 4px 6px; border-radius: 4px; display: inline-block; margin-top: 6px; line-height: 1.2; .aku-warning small { color: #5b1b1b; font-size: 10px; }
`;
const s = document.createElement('style');
s.id = STYLE_ID;
s.textContent = css;
document.head.appendChild(s);
}
// 各レスの dd 要素一覧を得る(要素そのものを返す)
function collectPostElements() {
let ddNodes = Array.from(document.querySelectorAll('dd[class*="mesg body"], dd[class*="mesg"]'));
if (ddNodes.length === 0) {
ddNodes = Array.from(document.querySelectorAll('dl > dd'));
}
return ddNodes;
}
// 各レス要素から aku をカウント(警告要素は除外)
function countAkuInElements(elems) {
if (!elems || elems.length === 0) return 0;
const esc = APU_CMD.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(esc, 'ig');
let total = 0;
for (const el of elems) {
// 警告を含めずにテキストを取得するために要素を浅くクローンして警告要素を取り除く
const clone = el.cloneNode(true);
const warnings = clone.querySelectorAll('.aku-warning');
warnings.forEach(n => n.remove());
const txt = clone.textContent || '';
const matches = txt.match(re);
total += (matches ? matches.length : 0);
}
return total;
}
function getFirstPostBodyElement() {
const firstDt = document.querySelector('dl dt, dl > dt');
if (!firstDt) return null;
let el = firstDt.nextElementSibling;
while (el && el.tagName.toLowerCase() !== 'dd') el = el.nextElementSibling;
return el;
}
function showWarning(firstBodyEl, count) {
if (!firstBodyEl) return;
const existing = firstBodyEl.querySelector('.aku-warning');
if (existing) existing.remove();
const wrapper = document.createElement('div');
wrapper.className = 'aku-warning';
wrapper.innerHTML = `
<strong>注意</strong>:このスレでは <code>${APU_CMD}</code> が <strong>${count}</strong> 回使われています。(読み込まれている部分のみ)
<small>(閾値 ${THRESHOLD})</small>
`;
// 🔍ボタンを追加
const btn = document.createElement('button');
btn.textContent = '🔍';
btn.style.marginLeft = '6px';
btn.style.cursor = 'pointer';
btn.style.border = 'none';
btn.style.background = 'transparent';
btn.title = '!aku を検索';
btn.addEventListener('click', () => {
const url = window.location.href.split('?')[0]; // 元URLをベースにする
window.location.href = url + '?q=%21aku';
});
wrapper.appendChild(btn); firstBodyEl.appendChild(wrapper);
}
function checkAndWarn() {
addStyles();
const elems = collectPostElements();
if (!elems || elems.length === 0) return;
const count = countAkuInElements(elems); const firstBodyEl = getFirstPostBodyElement(); if (count >= THRESHOLD) {
showWarning(firstBodyEl, count);
} else if (firstBodyEl) {
const existing = firstBodyEl.querySelector('.aku-warning');
if (existing) existing.remove();
}
}
function observeThread() {
const container = document.querySelector('ol, div.thread, div.MAIN_WRAP, body');
if (!container) return;
const mo = new MutationObserver(() => {
if (window.__aku_check_timeout) clearTimeout(window.__aku_check_timeout);
window.__aku_check_timeout = setTimeout(() => {
checkAndWarn();
}, 200);
});
mo.observe(container, { childList: true, subtree: true });
}
setTimeout(() => {
try {
checkAndWarn();
observeThread();
} catch (e) {
console.error('aku-warning error', e);
}
}, CHECK_DELAY);
})();
|