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