atwiki-logo
  • 新規作成
    • 新規ページ作成
    • 新規ページ作成(その他)
      • このページをコピーして新規ページ作成
      • このウィキ内の別ページをコピーして新規ページ作成
      • このページの子ページを作成
    • 新規ウィキ作成
  • 編集
    • ページ編集
    • ページ編集(簡易版)
    • ページ名変更
    • メニュー非表示でページ編集
    • ページの閲覧/編集権限変更
    • ページの編集モード変更
    • このページにファイルをアップロード
    • メニューを編集
    • 右メニューを編集
  • バージョン管理
    • 最新版変更点(差分)
    • 編集履歴(バックアップ)
    • アップロードファイル履歴
    • ページ操作履歴
  • ページ一覧
    • ページ一覧
    • このウィキのタグ一覧
    • このウィキのタグ(更新順)
    • このページの全コメント一覧
    • このウィキの全コメント一覧
    • おまかせページ移動
  • RSS
    • このウィキの更新情報RSS
    • このウィキ新着ページRSS
  • ヘルプ
    • ご利用ガイド
    • Wiki初心者向けガイド(基本操作)
    • このウィキの管理者に連絡
    • 運営会社に連絡(不具合、障害など)
ページ検索 メニュー
Jazap!
  • ウィキ募集バナー
  • 目安箱バナー
  • 操作ガイド
  • 新規作成
  • 編集する
  • 全ページ一覧
  • 登録/ログイン
ページ一覧
Jazap!
  • ウィキ募集バナー
  • 目安箱バナー
  • 操作ガイド
  • 新規作成
  • 編集する
  • 全ページ一覧
  • 登録/ログイン
ページ一覧
Jazap!
ページ検索 メニュー
  • 新規作成
  • 編集する
  • 登録/ログイン
  • 管理メニュー
管理メニュー
  • 新規作成
    • 新規ページ作成
    • 新規ページ作成(その他)
      • このページをコピーして新規ページ作成
      • このウィキ内の別ページをコピーして新規ページ作成
      • このページの子ページを作成
    • 新規ウィキ作成
  • 編集
    • ページ編集
    • ページ編集(簡易版)
    • ページ名変更
    • メニュー非表示でページ編集
    • ページの閲覧/編集権限変更
    • ページの編集モード変更
    • このページにファイルをアップロード
    • メニューを編集
    • 右メニューを編集
  • バージョン管理
    • 最新版変更点(差分)
    • 編集履歴(バックアップ)
    • アップロードファイル履歴
    • ページ操作履歴
  • ページ一覧
    • このウィキの全ページ一覧
    • このウィキのタグ一覧
    • このウィキのタグ一覧(更新順)
    • このページの全コメント一覧
    • このウィキの全コメント一覧
    • おまかせページ移動
  • RSS
    • このwikiの更新情報RSS
    • このwikiの新着ページRSS
  • ヘルプ
    • ご利用ガイド
    • Wiki初心者向けガイド(基本操作)
    • このウィキの管理者に連絡
    • 運営会社に連絡する(不具合、障害など)
  • atwiki
  • Jazap!
  • スクリプト保管庫

Jazap!

スクリプト保管庫

最終更新:2025年09月26日 15:35

opensecretj

- view
だれでも歓迎! 編集
荒らしに使われないような安全なスクリプトを掲示します
自由に書いていってどうぞ
書き方は真似た方が良いかも
(導入は自己責任でお願いします)
https://hayabusa.open2ch.net/test/read.cgi/livejupiter/1757942123/
↑タンパーモンキー部?

Tampermonkey用

+ ID横に検索ボタンをすべてのIDに追加
// ==UserScript==
// @name Open2ch ID横に検索ボタンをすべてのIDに追加
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 全てのIDに検索ボタンを追加
// @author jazap
// @match https://*.open2ch.net/*
// @grant none
// @require https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==

(function() {
'use strict';

const bbs = 'livejupiter';

function addSearchButtons() {
$('span._id').each(function() {
const $idSpan = $(this);

if ($idSpan.find('.id-search-btn').length > 0) return;

const idVal = $idSpan.attr('val');
if (!idVal) return;

const $btn = $('<button>')
.text('検索')
.addClass('id-search-btn')
.css({
marginLeft: '6px',
fontSize: '10px',
cursor: 'pointer',
padding: '1px 5px',
borderRadius: '3px',
border: '1px solid #888',
background: '#eee',
verticalAlign: 'middle'
})
.attr('type', 'button')
.on('click', () => {
const url = `https://find.open2ch.net/?bbs=${bbs}&t=f&q=${encodeURIComponent(idVal)}`;
window.open(url, '_blank');
});

$idSpan.append($btn);
});
}

$(function() {
addSearchButtons();
setInterval(addSearchButtons, 500);
});

)();
+ おん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>
&nbsp;<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="#">&gt;&gt;${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="#">&gt;&gt;${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);

})();

タグ:

+ タグ編集
  • タグ:
タグの更新に失敗しました
エラーが発生しました。ページを更新してください。
ページを更新
「スクリプト保管庫」をウィキ内検索
LINE
シェア
Tweet
Jazap!
記事メニュー
快適なおんJライフを!!!
Jazap!
おんJのレスやスレタイを検索できるサイト
nopic貫通&アイコン貫通(新)
nopic貫通(旧)
アク禁抜け
AA連投
NGワード抜け(新)
NGワード抜け(旧)
ステルスコマンド(新)
ステルスコマンド(旧)
無限に書き込めるスレを作る方法
自由に書ける所
過去の裏技
コメント
スクリプト保管庫
記事メニュー2
快適なおんJライフを!!!
人気記事ランキング
  1. おんJのレスやスレタイを検索できるサイト
  2. 無限に書き込めるスレを作る方法
  3. スクリプト保管庫
  4. アク禁抜け
  5. AA連投
  6. ステルスコマンド(新)
  7. ステルスコマンド(旧)
  8. nopic貫通&アイコン貫通(新)
  9. コメント/コメント
  10. NGワード抜け(新)
もっと見る
最近更新されたページ
  • 1日前

    ステルスコマンド(新)
  • 3日前

    AA連投
  • 3日前

    コメント/コメント
  • 4日前

    スクリプト保管庫
  • 4日前

    無限に書き込めるスレを作る方法
  • 4日前

    Jazap!
  • 4日前

    メニュー
  • 9日前

    おんJのレスやスレタイを検索できるサイト
  • 18日前

    nopic貫通&アイコン貫通(新)
  • 18日前

    nopic貫通(旧)
もっと見る
人気記事ランキング
  1. おんJのレスやスレタイを検索できるサイト
  2. 無限に書き込めるスレを作る方法
  3. スクリプト保管庫
  4. アク禁抜け
  5. AA連投
  6. ステルスコマンド(新)
  7. ステルスコマンド(旧)
  8. nopic貫通&アイコン貫通(新)
  9. コメント/コメント
  10. NGワード抜け(新)
もっと見る
最近更新されたページ
  • 1日前

    ステルスコマンド(新)
  • 3日前

    AA連投
  • 3日前

    コメント/コメント
  • 4日前

    スクリプト保管庫
  • 4日前

    無限に書き込めるスレを作る方法
  • 4日前

    Jazap!
  • 4日前

    メニュー
  • 9日前

    おんJのレスやスレタイを検索できるサイト
  • 18日前

    nopic貫通&アイコン貫通(新)
  • 18日前

    nopic貫通(旧)
もっと見る
ウィキ募集バナー
急上昇Wikiランキング

急上昇中のWikiランキングです。今注目を集めている話題をチェックしてみよう!

  1. 遊戯王2ch本スレWiki
  2. PC版Webサカ@ ウィキ
  3. 千鳥の鬼レンチャン サビだけカラオケデータベース
  4. 鬼レンチャンWiki
  5. 錬金術2021 パズル 攻略wiki
  6. asagaolabo @ ポップン百科大事典
  7. "The Planet Crafter" 非公式日本語Wiki
  8. 番組スポンサー 詳しく! @ ウィキ
  9. ヌカづけ◎日本語コンシューマ版「Fallout」wiki
  10. メダロット辞典@wiki
もっと見る
人気Wikiランキング

atwikiでよく見られているWikiのランキングです。新しい情報を発見してみよう!

  1. アニヲタWiki(仮)
  2. ストグラ まとめ @ウィキ
  3. ゲームカタログ@Wiki ~名作からクソゲーまで~
  4. 初音ミク Wiki
  5. 発車メロディーwiki
  6. モンスター烈伝オレカバトル2@wiki
  7. 検索してはいけない言葉 @ ウィキ
  8. Grand Theft Auto V(グランドセフトオート5)GTA5 & GTAオンライン 情報・攻略wiki
  9. オレカバトル アプリ版 @ ウィキ
  10. 機動戦士ガンダム EXTREME VS.2 INFINITEBOOST wiki
もっと見る
新規Wikiランキング

最近作成されたWikiのアクセスランキングです。見るだけでなく加筆してみよう!

  1. MadTown GTA (Beta) まとめウィキ
  2. シュガードール情報まとめウィキ
  3. まどドラ攻略wiki
  4. 戦国ダイナスティ攻略@ウィキ
  5. ちいぽけ攻略
  6. SurrounDead 攻略 (非公式wiki)
  7. シミュグラ2Wiki(Simulation Of Grand2)GTARP
  8. 20XX @ ウィキ
  9. Dark War Survival攻略
  10. 魔法少女ノ魔女裁判 攻略・考察Wiki
もっと見る
全体ページランキング

最近アクセスの多かったページランキングです。話題のページを見に行こう!

  1. 参加者一覧 - ストグラ まとめ @ウィキ
  2. 鬼レンチャン(レベル順) - 鬼レンチャンWiki
  3. 信じていた仲間達にダンジョン奥地で殺されかけたがギフト『無限ガチャ』でレベル9999の仲間達を手に入れて元パーティーメンバーと世界に復讐&『ざまぁ!』します! - アニヲタWiki(仮)
  4. 魔獣トゲイラ - バトルロイヤルR+α ファンフィクション(二次創作など)総合wiki
  5. サーヴァント/一覧/クラス別 - Fate/Grand Order @wiki 【FGO】
  6. コメント/雑談・質問 - マージマンション@wiki
  7. 櫛田桔梗 - アニヲタWiki(仮)
  8. 機動戦士ガンダム サンダーボルト - アニヲタWiki(仮)
  9. パナマ運河 - アニヲタWiki(仮)
  10. ミッション攻略 - 地球防衛軍6 @ ウィキ
もっと見る

  • このWikiのTOPへ
  • 全ページ一覧
  • アットウィキTOP
  • 利用規約
  • プライバシーポリシー

2019 AtWiki, Inc.