superopenj @ ウィキ
スクリプト(2)
最終更新:
superopenj
-
view
管理者のみ編集可
続き
作者:七色の彩り
| + | Open2ch NG Fix |
コメント:
おーぷんがサーバー移転してから自動新着に対してNG(×ボタンで消したID)が効いていない不具合対策 対象者の新着音と+1件新着レスの通知を消すことはできませんでした。 新着音が鳴っても自動新着を非表示に出来ればいい、さとる氏が直すのをどうしても待てないという人向け // ==UserScript==
// @name Open2ch NG Fix
// @namespace https://greasyfork.org/ja/users/864059
// @version 2.0
// @description サーバー移転後自動新着でNGが効いていないのを修正(CSS強制上書き)
// @author 七色の彩り
// @match https://*.open2ch.net/test/read.cgi/*
// @icon https://open2ch.net/favicon.ico
// @run-at document-start
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const match = location.pathname.match(/\/([^\/]+)\/\d+/);
if (!match) return;
const storageKey = 'ignv4' + match[1];
// CSSを管理するためのstyle要素
let styleEl = null;
// NG IDリストを元に、非表示用のCSSを生成して適用する
const updateNGStyle = () => {
const ignoreListRaw = localStorage.getItem(storageKey);
if (!ignoreListRaw) return;
const ignoreList = ignoreListRaw.split('<D>').filter(id => id);
if (ignoreList.length === 0) {
if (styleEl) styleEl.textContent = '';
return;
}
// 各IDに対して「display: none !important」を強制するCSSを作成
// 例: dl[uid="ID"] { display: none !important; }
const cssRules = ignoreList.map(id => `dl[uid="${id}"] { display: none !important; }`).join('\n');
if (!styleEl) {
styleEl = document.createElement('style');
document.head ? document.head.appendChild(styleEl) : document.documentElement.appendChild(styleEl);
}
styleEl.textContent = cssRules;
};
// 初回実行
updateNGStyle();
// ユーザーがNG登録(赤の×ボタンクリック)した時にCSSを更新する
document.addEventListener('click', (e) => {
if (e.target.closest('.iok')) {
setTimeout(updateNGStyle, 100);
}
}, true);
// 念のため、新着が来たタイミングでもリストが更新されていないかチェック
// (別のタブでNG登録した場合などへの対策)
const observer = new MutationObserver(() => {
updateNGStyle();
});
window.addEventListener('DOMContentLoaded', () => {
const target = document.getElementById('res_field') || document.body;
observer.observe(target, { childList: true });
updateNGStyle();
});
})();
|
| + | Open2ch Find Link Modifier |
コメント:
おーぷん2chの検索結果で対象スレッドタイトルのリンク(l50)を任意の数に書き換え 更にレス番号のリンクからハイフンを削除して対象レスのみ開くように 検索結果の内容に安価があればそれも含めて開く ※内容がアク禁の場合対象者も開くので要注意 // ==UserScript==
// @name Open2ch Find Link Modifier
// @namespace https://greasyfork.org/ja/users/864059
// @version 0.6
// @description レス番号リンクのハイフン削除と、安価があればそれも含める修正。表示数(/lXX)を動的に変更。プリセットボタン付き。
// @author 七色の彩り
// @match https://find.open2ch.net/*
// @icon https://open2ch.net/favicon.ico
// @grant none
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @license MIT
// ==/UserScript==
(function() {
'use strict';
let targetLValue = localStorage.getItem('open2ch_find_l_value') || '50';
let observer = null;
function injectUI() {
if ($('#l-modifier-container').length) return;
const customStyle = `<style>
.l-preset-btn { margin-left: 5px; padding: 2px 6px; font-size: 11px; cursor: pointer; background: #eee; border: 1px solid #ccc; border-radius: 3px; display: inline-block; }
.l-preset-btn:hover { background: #ddd; }
#l-modifier-container { margin-top: 0px; display: flex; align-items: center; flex-wrap: wrap; color: #333; }
</style>`;
const uiHtml = `
<div id="l-modifier-container">
<span style="font-size: 12px; font-weight: bold; margin-right: 5px;">スレタイリンク</span>
<input type="text" id="l-value-input" class="form-control"
value="${targetLValue === 'all' ? '' : targetLValue}"
placeholder="${targetLValue === 'all' ? 'ALL' : ''}"
style="width: 60px; height: 28px; display: inline-block; text-align: center;">
<div id="l-presets">
<span class="l-preset-btn" data-val="10">l10</span>
<span class="l-preset-btn" data-val="30">l30</span>
<span class="l-preset-btn" data-val="50">l50</span>
<span class="l-preset-btn" data-val="100">l100</span>
<span class="l-preset-btn" data-val="all">ALL</span>
</div>
</div>`;
$('head').append(customStyle);
$('.input-group').after(uiHtml);
$('#l-value-input').on('input', function() {
updateLValue($(this).val() === '' ? 'all' : $(this).val());
});
$('.l-preset-btn').on('click', function() {
const val = $(this).data('val');
$('#l-value-input').val(val === 'all' ? '' : val).attr('placeholder', val === 'all' ? 'ALL' : '');
updateLValue(val);
});
}
function updateLValue(val) {
targetLValue = val;
localStorage.setItem('open2ch_find_l_value', val);
fixFindLinks();
}
function fixFindLinks() {
// 監視を一時停止して無限ループを防ぐ
if (observer) observer.disconnect();
// 1. スレタイおよびli要素の修正
$('.result .subject a, li.list-group-item[url]').each(function() {
let attrName = $(this).is('a') ? 'href' : 'url';
let url = $(this).attr(attrName);
if (!url || !url.includes('/test/read.cgi/')) return;
if (!$(this).data('original-url')) $(this).data('original-url', url);
let base = $(this).data('original-url').replace(/\/l\d+$/, '');
let newUrl = (targetLValue === 'all') ? base : `${base}/l${targetLValue}`;
if (url !== newUrl) $(this).attr(attrName, newUrl);
});
// 2. レス番号リンクの修正
$('.result .content .th').each(function() {
const $th = $(this);
const $link = $th.find('a').first();
let href = $link.attr('href');
if (!href) return;
if (!$link.data('original-href')) $link.data('original-href', href);
let newHref = $link.data('original-href').replace(/-$/, '');
$link.text($link.text().replace(/-$/, ''));
const thText = $th.text();
const matches = thText.match(/(?:>>|!aku|!kaijo|!cap|!set|!sub)(\d+)/g);
if (matches) {
const nums = matches.map(m => m.match(/\d+/)[0]);
const uniqueNums = [...new Set(nums)];
newHref += ',' + uniqueNums.join(',');
}
if (href !== newHref) $link.attr('href', newHref);
});
// 監視を再開
if (observer) observer.observe(document.body, { childList: true, subtree: true });
}
$(function() {
injectUI();
observer = new MutationObserver(function() {
// UIが消えていれば再注入
if (!$('#l-modifier-container').length) injectUI();
fixFindLinks();
});
fixFindLinks();
});
})();
|
| + | ... |
(Open2ch アラビア文字規制避け (拡張版))
コメント: おーぷん2chでうっかりアラビア文字等の禁止されている文字種を書き込みしようとして吸い込まれるのを阻止 うっかり書き込むとダイアログ無しで吸い込まれる文字種、ワードが入力されても投稿ボタンを無効化する。 最近増えたAIチャットの回答によく含まれる特殊スペース文字も吸い込み対象なので無効化対象に 文字種全般を指定して検知出来るものもあるが、2種類を組み合わせて構成される文字種は個別指定しないと検知できない。 昔ながらのAAに使用されるギリシャ文字やキリル文字以外は極力避けるのが無難です。
このスクリプトは、おんJ民氏による「Open2ch アラビア文字規制避け」
(https://greasyfork.org/ja/scripts/526462-open2ch-アラビア文字規制避け)をベースに拡張したものです。 元の作者であるおんJ民氏に感謝いたします。 // ==UserScript==
// @name Open2ch アラビア文字規制避け (拡張版)
// @namespace https://greasyfork.org/ja/users/864059
// @version 1.0.5
// @description アラビア文字やAI回答の特殊空白文字等、書き込むと吸い込まれてしまう文字種を入力したとき投稿ボタンを無効化します
// @author 七色の彩り
// @match https://*.open2ch.net/test/read.cgi/*
// @icon https://avatars.githubusercontent.com/u/88383494
// @grant none
// @license GNU Affero General Public License v3.0 or later
// ==/UserScript==
/*
注意:
このスクリプトは、おんJ民氏による「Open2ch アラビア文字規制避け」
(https://greasyfork.org/ja/scripts/526462-open2ch-アラビア文字規制避け)をベースに拡張したものです。
元の作者であるおんJ民氏に感謝いたします。
*/
(function() {
'use strict';
// 規制対象とする文字の正規表現
// --- アラビア文字 ---
// \u0600-\u06FF: 基本的なアラビア文字
// \u0750-\u077F: アラビア文字補助
// \u08A0-\u08FF: アラビア文字拡張A
// \uFB50-\uFDFF: アラビア文字表現形式A
// \uFE70-\uFEFF: アラビア文字表現形式B
// --- タイ文字 ---
// \u0E00-\u0E7F
// --- チベット文字 ---
// \u0F00-\u0FFF
// --- ヘブライ文字 ---
// \u0590-\u05FF
// --- デーヴァナーガリー (インド文字) ---
// \u0900-\u097F
// --- 結合文字 (Combining Diacritical Marks) ---
// \u0300-\u036F: 顔文字に用いられる記号など
// --- AI回答等に使われる特殊スペース文字 ---
// \u00A0: NO-BREAK SPACE (改行なし半角スペース)
// \u200B: ZERO WIDTH SPACE (ゼロ幅スペース) // リテラルに移動
// \u202F: NARROW NO-BREAK SPACE (狭い改行なしスペース) // リテラルに移動
// \uFEFF: ZERO WIDTH NO-BREAK SPACE / BOM (幅ゼロ改行なしスペース)
// --- 特殊記号を用いた顔文字に採用されている文字種 ---
// \u0590-\u05FF: ヘブライ文字ブロック
// \u1DC0-\u1DFF: 結合用ダイアクリティカルマーク拡張
// \u0D00-\u0D7F: マラヤーラム文字
// \u0C80-\u0CFF: カンナダ文字
// \u0250-\u02AF: 発音記号
// \u1D00-\u1D7F: 発音記号拡張
// \u0980-\u09FF: ベンガル文字
// --- 規制対象文字の定義 ---
const arabicRange = '\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF'; // アラビア
const asianRange = '\u0E00-\u0E7F\u0F00-\u0FFF\u0900-\u097F\u0980-\u09FF'; // タイ・チベット・インド
const specialRange = '\u0590-\u05FF\u0300-\u036F\u1DC0-\u1DFF\u0D00-\u0D7F\u0C80-\u0CFF\u0250-\u02AF\u1D00-\u1D7F'; // ヘブライ・結合・マラヤーラム等
const spaceRange = '\u00A0\uFEFF'; // 特殊スペース
const literals = '૮ܸ˶ෆ੭ͽ꧁꧂\u200B\u202F꩜\u206D'; // 個別リテラル
// 全てを統合して一つの正規表現にする
const ARABIC_CHAR_REGEX = new RegExp('[' + arabicRange + asianRange + specialRange + spaceRange + literals + ']');
// --- 日本語NGワード ---
const jpNgWords = [
'アフィ',
'大麻',
'唐沢恒心教',
'(^|[。\\s、!?\\u3000])(安倍晋三)([。\\s、!?\\u3000]|$)' // 単体マッチ用の正規表現
].join('|');
const JAPANESE_NG_WORD_REGEX = new RegExp(jpNgWords);
// 現在のテキストエリアの状態を監視するためのタイマーID
let inputCheckTimeout = null;
/**
* 全てのテキストエリアの状態をチェックし、投稿ボタンの有効/無効を決定する
*/
function updateSubmitButtonStatus() {
// 一つでもNG文字またはNGワードを含むテキストエリアがあるかチェック
const isAnyTextareaInvalid = [...document.querySelectorAll('textarea')]
.some(ta => ARABIC_CHAR_REGEX.test(ta.value) || JAPANESE_NG_WORD_REGEX.test(ta.value)); // 日本語NGワードチェックを追加
[...document.querySelectorAll('[type="submit"]')].forEach(button => {
// NG文字/NGワードがあれば無効化、なければ有効化
button.disabled = isAnyTextareaInvalid;
// 投稿ボタン無効化の理由をタイトル属性でユーザーに提示
if (isAnyTextareaInvalid) {
button.title = '規制対象の特殊文字またはNGワードが含まれているため、投稿できません。';
} else {
button.removeAttribute('title');
}
});
}
/**
* テキストエリアの内容をチェックし、アラビア文字が含まれていれば警告表示を行う
* @param {HTMLTextAreaElement} textareaElement - チェック対象のtextarea要素
*/
function checkTextareaForArabic(textareaElement) {
const textContent = textareaElement.value;
// 1. 特殊文字のチェック
const hasArabic = ARABIC_CHAR_REGEX.test(textContent);
// 2. 日本語NGワードのチェック (大文字小文字・全角半角を区別したい場合は調整が必要です)
// hasJapaneseNgWordは、hasArabicがfalseの場合でもチェックを実行します。
const hasJapaneseNgWord = JAPANESE_NG_WORD_REGEX.test(textContent);
// どちらか一方でもNGであればtrue
const isInvalid = hasArabic || hasJapaneseNgWord;
// 背景色の設定
textareaElement.style.backgroundColor = isInvalid ? 'pink' : '';
// 全体の投稿ボタンの状態を更新
updateSubmitButtonStatus();
}
// pasteイベントリスナー (既存の機能を維持)
window.document.addEventListener('paste', (e) => {
if (e.target.tagName !== 'TEXTAREA') return;
const textareaElement = e.target;
const pasteData = (e.clipboardData || window.clipboardData).getData('text');
// ペーストデータに特殊文字または日本語NGワードが含まれるかチェック
if (ARABIC_CHAR_REGEX.test(pasteData) || JAPANESE_NG_WORD_REGEX.test(pasteData)) {
// ペースト後のテキストエリアの背景色を即座に変更
// (inputイベントが発火するまでの間、即座に視覚的フィードバックを与えるため)
textareaElement.style.backgroundColor = 'pink';
}
// pasteの直後、または短い遅延後に全体チェックを強制的に実行
// (textarea.valueが更新されるのを待ってからチェックするため)
if (inputCheckTimeout) {
clearTimeout(inputCheckTimeout);
}
inputCheckTimeout = setTimeout(() => {
checkTextareaForArabic(textareaElement);
}, 10); // ごく短い遅延
});
// inputイベントリスナー (直接入力やテキストエリアの内容変更を監視)
window.document.addEventListener('input', (e) => {
if (e.target.tagName !== 'TEXTAREA') return;
// 遅延させてチェックすることで、連続した入力でのパフォーマンス負荷を軽減
if (inputCheckTimeout) {
clearTimeout(inputCheckTimeout);
}
inputCheckTimeout = setTimeout(() => {
checkTextareaForArabic(e.target);
}, 300); // 300msの遅延
});
// ページロード時に既存のテキストエリアをチェック
window.addEventListener('load', () => {
document.querySelectorAll('textarea').forEach(textarea => {
checkTextareaForArabic(textarea);
});
});
})();
|
| + | Open2ch ID Search Button |
コメント:
IDと時刻の間に虫眼鏡アイコンを追加し、クリックするとおーぷん全体で全文のID検索をする。 IDの右側だと無視ボタン誤爆の恐れがあるため左にした。 また、スレッド内で選択範囲の右上にも虫眼鏡アイコンを追加して同じように全文検索をする。
主にマルチポスト疑惑の書き込みに対して即座に検索出来るように作ったもの
// ==UserScript==
// @name Open2ch ID Search Button
// @namespace https://greasyfork.org/ja/users/864059
// @version 1.2.0
// @description おーぷん2chでIDの左側と本文を選択した右上に検索ボタン(虫眼鏡アイコン)を表示します。
// @author 七色の彩り
// @match https://*.open2ch.net/test/read.cgi/*
// @icon https://open2ch.net/favicon.ico
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// URLからbbs名を取得する関数
const getBbsName = () => {
const path = window.location.pathname;
const match = path.match(/\/read\.cgi\/([a-zA-Z0-9_-]+)\//);
return match ? match[1] : '';
};
const bbsName = getBbsName();
const $searchUrl = 'https://find.open2ch.net/?bbs=&t=f&q='; // ID: は searchQuery で付与する
/**
* 指定されたIDスパンに検索ボタンを追加する
* @param {HTMLElement} idSpan - <span class="_id"> 要素
* @param {string} searchUrlTemplate - 検索URLの雛形
*/
const processIdSpan = (idSpan, searchUrlTemplate) => {
// 1. ID情報の取得
const idText = idSpan.getAttribute('val');
if (!idText || idText === '???') return;
// 2. ID整形ロジック (余計なピリオド問題を解決したもの)
let fullId = '';
const idLinks = idSpan.querySelectorAll('.id');
if (idLinks.length > 0) {
fullId = Array.from(idLinks).map(link => link.textContent).join('.');
} else if (idText.length > 4) {
fullId = idText.slice(0, 2) + '.' + idText.slice(2, 4) + '.' + idText.slice(4);
} else {
fullId = idText;
}
const searchQuery = 'ID:' + fullId;
const encodedQuery = encodeURIComponent(searchQuery);
const finalUrl = searchUrlTemplate + encodedQuery + '&wh=&d=';
// 3. 重複チェックと既存ボタンの再利用
const existingButton = idSpan.previousElementSibling;
// 既に虫眼鏡アイコンを持つSPAN要素が存在する場合
if (existingButton && existingButton.tagName === 'SPAN' && existingButton.querySelector('.fas.fa-search')) {
// 既存のボタンに正しい機能を持つクリックイベントを追加/上書きする
existingButton.addEventListener('click', function() {
window.open(finalUrl, '_blank', 'noopener noreferrer');
});
return; // 既存要素を再利用したため、ここで終了
}
// 4. 新しいボタンの作成・挿入 (通常時の動作)
const searchButton = document.createElement('span');
searchButton.title = 'ID:' + fullId + 'を検索';
searchButton.style.cssText = 'margin-left: 5px; cursor: pointer;';
const icon = document.createElement('i');
icon.className = 'fas fa-search';
icon.style.cssText = 'color: #333;';
searchButton.appendChild(icon);
searchButton.addEventListener('click', function() {
window.open(finalUrl, '_blank', 'noopener noreferrer');
});
// idSpanの直前に挿入 (時刻とID:の間)
idSpan.insertAdjacentElement('beforebegin', searchButton);
};
// スタイルの追加(丸いボタンのデザイン)
const style = document.createElement('style');
style.textContent = `
#floating-search-btn {
position: absolute;
z-index: 10001;
background: #fff;
border: 1px solid #ccc;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
display: none;
transition: transform 0.1s;
}
#floating-search-btn:hover {
transform: scale(1.1);
background-color: #f0f0f0;
}
#floating-search-btn i {
color: #333;
font-size: 14px;
}
`;
document.head.appendChild(style);
// ボタン要素を作成
const floatBtn = document.createElement('div');
floatBtn.id = 'floating-search-btn';
floatBtn.title = 'おーぷん全体検索';
floatBtn.innerHTML = '<i class="fas fa-search"></i>';
document.body.appendChild(floatBtn);
// テキスト選択時のイベント
document.addEventListener('mouseup', (e) => {
// 少し遅らせて選択範囲を取得(クリック解除時のタイミング調整)
setTimeout(() => {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText) {
// ボタンの表示位置をマウス位置の少し右上に設定
floatBtn.style.left = `${e.pageX + 10}px`;
floatBtn.style.top = `${e.pageY - 40}px`;
floatBtn.style.display = 'flex';
// 検索実行イベント
floatBtn.onclick = () => {
const url = $searchUrl + encodeURIComponent(selectedText) + '&wh=&d=';
window.open(url, '_blank', 'noopener noreferrer');
floatBtn.style.display = 'none';
selection.removeAllRanges(); // 選択解除
};
} else {
// 何もないところをクリックしたらボタンを隠す
if (e.target.closest('#floating-search-btn')) return;
floatBtn.style.display = 'none';
}
}, 10);
});
// スクロール時にも隠す
document.addEventListener('scroll', () => {
floatBtn.style.display = 'none';
}, { passive: true });
// 初期実行: 既存の要素に対してボタンを追加
// Mutation Observerで動的に追加される要素を監視
// 監視対象を最も確実なコンテナである .thread に
const threadContainer = document.querySelector('.thread'); // <div class="thread"> または <dl class="thread"> の両方に対応
if (!threadContainer) {
// コンテナが見つからなければ、処理を終了する
//console.log('Open2ch ID Search Button: スレッドコンテナ (.thread) が見つかりません。終了します。');
return;
}
// スレッドコンテナ内のみを検索対象とする
const idSpansInitial = threadContainer.querySelectorAll('._id');
idSpansInitial.forEach(idSpan => {
processIdSpan(idSpan, $searchUrl);
});
// Mutation Observerで動的に追加される要素を監視
// threadContainer が取得できた前提で、そのまま observer を設定
setupObserver(threadContainer, $searchUrl);
function setupObserver(targetNode, searchUrlTemplate) {
const observer = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
// 要素ノードでない場合はスキップ
if (node.nodeType !== 1) return;
// 追加された要素(またはその子孫)から '_id' スパンを検索
// 新しい書き込み(<dl>)が追加された場合、その中の<dt>の._idを見つける
const idSpans = node.querySelectorAll('._id');
idSpans.forEach(idSpan => {
processIdSpan(idSpan, searchUrlTemplate);
});
// 追加されたノード自体が '_id' スパンの場合
if (node.classList && node.classList.contains('_id')) {
processIdSpan(node, searchUrlTemplate);
}
});
}
});
});
// スレッドコンテナとその子要素の変更を監視開始
observer.observe(targetNode, { childList: true, subtree: true });
}
})();
|
| + | Open2ch Kome URL自動補完 |
コメント:
おーぷん2chの「kome」機能で、URLに含まれる「=(イコール)」記号がなぜか消えてしまう不具合を修正します。
不具合の症状: 自分が書き込んだ直後は正常に見えますが、ページをリロードしたり、
他人から見たりするとイコールが欠けてしまい、リンクをクリックしてもTOP戻されてしまいます。(動画が存在しない扱い)
このスクリプトの役割: 特に出現頻度の高い YouTube のURLを自動でチェックし、欠けているイコールを補完して正しいリンクに修正します。
// ==UserScript==
// @name Open2ch Kome URL自動補完
// @namespace https://greasyfork.org/ja/users/864059
// @version 1.3.1
// @description komeでyoutubeのURLに含まれるはずの「=」が消えてしまうのを補完
// @author 七色の彩り
// @match https://*.open2ch.net/test/read.cgi/*
// @icon https://open2ch.net/favicon.ico
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const PROCESSED_ATTR = 'data-kome-fixed';
/**
* 動画IDと追加パラメータから正しいYouTube URLを生成します。
*/
function reconstructYoutubeUrl(videoId, extraParams) {
let finalUrl = `https://www.youtube.com/watch?v=${videoId}`;
if (extraParams) {
let extra = extraParams.replace(/&/g, '&');
// 2つ目の'?'が来た場合は'&'に置換して結合
if (extra.startsWith('?')) {
extra = '&' + extra.substring(1);
}
finalUrl += extra;
}
return finalUrl;
}
/**
* 既に<a>タグになっているが不正なURLを修正します。
*/
function fixYoutubeUrlsInNode(node) {
if (!(node instanceof Element)) return;
const links = node.querySelectorAll(`a[href]:not([${PROCESSED_ATTR}])`);
links.forEach(link => {
const href = link.href;
let videoId = null;
let extra = '';
const watchMatch = href.match(/watch\?v(?:=)?([a-zA-Z0-9_-]{11})([&?#].*)?/);
const shortMatch = href.match(/youtu\.be\/([a-zA-Z0-9_-]{11})([&?#].*)?/);
if (watchMatch) {
videoId = watchMatch[1];
extra = watchMatch[2] || '';
} else if (shortMatch) {
videoId = shortMatch[1];
extra = shortMatch[2] || '';
}
if (videoId) {
const finalUrl = reconstructYoutubeUrl(videoId, extra);
link.href = finalUrl;
// テキストがURL形式だった場合のみ書き換える(アンカーテキストが動画タイトルの場合は保持)
if (link.textContent.includes('youtube.com') || link.textContent.includes('youtu.be')) {
link.textContent = finalUrl;
}
link.setAttribute(PROCESSED_ATTR, 'true');
}
});
}
/**
* テキストノード内のURLを検索し、リンクに変換します。
*/
function parseAndReplaceUrl(node) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
// 途切れたURLをキャプチャ (watch?v 直後の = の有無を問わない)
const youtuBeRegex = /https:\/\/(?:youtu\.be\/|www\.youtube\.com\/watch\??v=?)([a-zA-Z0-9_-]{11})([&?#][^\s]*)?/g;
const matches = [...text.matchAll(youtuBeRegex)];
if (matches.length > 0) {
const fragment = document.createDocumentFragment();
let lastIndex = 0;
matches.forEach(match => {
if (match.index > lastIndex) {
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
}
const videoId = match[1];
const extra = match[2] || '';
const finalUrl = reconstructYoutubeUrl(videoId, extra);
const a = document.createElement('a');
a.href = finalUrl;
a.textContent = finalUrl;
a.style.color = '#3399ff';
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.setAttribute(PROCESSED_ATTR, 'true');
fragment.appendChild(a);
lastIndex = match.index + match[0].length;
});
if (lastIndex < text.length) {
fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
}
if (node.parentNode) {
node.parentNode.replaceChild(fragment, node);
}
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
// 処理済み属性がある場合や特定のタグ内はスキップ
if (node.hasAttribute(PROCESSED_ATTR)) return;
const skipTags = ['A', 'BUTTON', 'SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT'];
if (!skipTags.includes(node.tagName)) {
Array.from(node.childNodes).forEach(parseAndReplaceUrl);
}
}
}
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
fixYoutubeUrlsInNode(node);
parseAndReplaceUrl(node);
} else if (node.nodeType === Node.TEXT_NODE) {
parseAndReplaceUrl(node);
}
}
}
});
const config = { childList: true, subtree: true };
const klogView = document.getElementById('klog_view');
if (klogView) {
observer.observe(klogView, config);
} else {
observer.observe(document.body, config);
}
// 初期実行
fixYoutubeUrlsInNode(document.body);
parseAndReplaceUrl(document.body);
})();
|
| + | Open2ch Kome UID Display |
コメント:
【概要】 Open2chのkome(チャット機能)で内部的に保持されているUIDを、時刻の左側に表示します。 このUIDは忍法帖(Cookie)と連動しているため、スレッドで固定ID(高レベル)の状態であれば、komeでも実質的な固定IDとして機能します。 (※ブラウザのCookie削除やシークレットモードの使用でUIDは更新されます)
自演・荒らしの確認や通報の補助、人違いの回避に役立ちます。 一方で、匿名性が薄れるため「見なきゃよかった」となる可能性もあります。
スレッドでの固定IDと同様、IDを紐付けられても構わないという方向けのツールです。
【注意点】
ログの限界: komeの発言が50個を超えて過去ログ送りになった分や、古い日付の発言は、スレッドのリロード時にUIDが取得できなくなります。
自分のUID: 自分の書き込みにはサーバーからUIDが返されない仕様のため、本スクリプトでは確認できません。確認したい場合は別ブラウザやシークレットウィンドウから見てください。
自分の名前設定: 自分のUIDが表示されるはずの場所には、好きな名前を記入できます(全角3文字/半角5文字まで。文字数制限は表示崩れ防止のためです)。
【主な機能】 UID部分をクリックすると、以下のオプション設定が展開されます。
メモ機能 UIDにマウスを合わせると、入力したメモをツールチップとして表示します。 メモ内にUID自体を表示するかどうかを個別に設定可能です(デフォルト:オン)。
UIDの色指定 プリセット(150色)またはカラーピッカーから、UIDごとに背景色を指定できます。特定のユーザーを目立たせたい場合に便利です。
デフォルト設定(詳細設定)
ランダムカラー表示: オンにすると全UIDを自動で色分けします。オフにすると全部白文字になります。
カラー調整: 明度100%(白文字)にしたり、明度0%(黒文字)に調整することで、普段はUIDを背景に埋もれさせて隠し、マウスで選択した時だけ見えるようにする、といった使い方も可能です。
色相の範囲を極端に狭く(小さく)して、色相の開始位置を好みの色にすれば特定の色だけにする事も可能です。
YouTubeのURL補完 komeではイコール記号が消えてしまう謎仕様があるので、つべのURLを補完して正常にアクセス出来るように表示
内部的には https://greasyfork.org/ja/scripts/564076-Open2ch-Kome-URL自動補完 これと同一です。 長いので割愛。
https://greasyfork.org/ja/scripts/564221-open2ch-kome-uid-display/code
|
| + | Open2ch Option Expansion |
コメント:
書き込み欄の左下にある「▼オプション」=「投稿おぷしょん」から選べる項目を拡張します。 初期状態では全部オフなので何もしません。 必要に応じてチェックを入れて試してください。
※注意:サブドメインごとの設定保存について
おーぷん2chの仕様上、設定は各板(サブドメイン)ごとに個別に保存されます。 例:uni での設定は、open や hayabusa には自動で反映されません。お手数ですが、それぞれの板ごとに設定を行ってください。
1. 虹色無効
rainbow.cssの無効化 虹色タイトル等の再来を阻止 2. 次スレ 常に次スレ作成・次スレ検索の項目を表示します 3. 虫消し クリスマスに飛び交うアレを消します ※ソースにbugと書かれているのでそれに準じています 4. 花消し 花びらが飛び交うのを消します 5. Error軽 komeエラーダイアログが重なり続けてどんどん暗く重くなるのを軽減します ※多重ダイアログ自体は阻止できません 6. お知らせ 板TOPにあるお知らせをスレTOPのスレ内検索欄右側に表示します 7. スレタイ消 スレタイにある!max数字や【文字列】をブラウザのタイトルから消します 8. X無効化 X(旧Twitter)のiframe展開を阻止します 9. 画像海苔 画像URL「のみ」に海苔を使用した場合にクリック出来ないのを解消します 10. カード無効 URLが自動的にカード形式(長方形)で表示されるのを阻止します 11. 花火無効 スレッド終了時(1000レス到達時など)に打ち上がる花火の演出を無効化します 12. つべ無効 youtube"等"の動画URLが書き込まれてもミニプレイヤーの展開を阻止します ニコニコ動画は独自の処理がされているようで書き込みがURLのみだと空発言のようになります 13. 雪消し スレッドに雪が舞うのを消します
追加情報
このスクリプトは、おーぷん2chの標準機能に干渉しすぎず、見た目の「ちょっと不便・ちょっと煩わしい」を解消することを目的としています。 // ==UserScript==
// @name Open2ch Option Expansion
// @namespace https://greasyfork.org/ja/users/864059
// @version 1.6.4
// @description 投稿おぷしょんに項目を追加し、表示に関するCSS等を切り替えます
// @author 七色の彩り
// @match https://*.open2ch.net/test/read.cgi/*
// @icon https://open2ch.net/favicon.ico
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// X(Twitter)の埋め込みスクリプトを無効化(スイッチON時のみ)
if (localStorage.getItem('ext_no_x_embed') === 'true') {
// 1. twttrオブジェクトを完全にロック
Object.defineProperty(window, 'twttr', {
value: { ready: function() {}, widgets: { load: function() {} } },
writable: false, configurable: false
});
// 2. スタイルで展開用の枠や非表示設定を無効化
const xStyle = document.createElement('style');
xStyle.textContent = `
/* 展開後の外枠を消す */
.twitter-tweet-rendered { display: none !important; }
/* iframeそのものを消す */
iframe[id^="twitter-widget-"] { display: none !important; }
/* 元のテキスト(blockquote)が必要なら表示する設定(今回はリンクのみにするなら不要ですが念のため) */
blockquote.twitter-tweet { display: none !important; }
`;
document.documentElement.appendChild(xStyle);
// 3. スクリプトタグと展開用要素の削除
const xObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
// Twitterスクリプトの阻止
if (node.tagName === 'SCRIPT' && node.src && node.src.indexOf('platform.twitter.com') !== -1) {
node.removeAttribute('src');
node.textContent = '';
node.remove();
}
// おーぷん側が作った「空の枠」を即座に消す
if (node.classList && node.classList.contains('twitter-tweet-rendered')) {
node.remove();
}
}
}
});
xObserver.observe(document.documentElement, { childList: true, subtree: true });
}
// カード化の無効化(スイッチON時のみ)
if (localStorage.getItem('ext_no_card') === 'true') {
// 1. カード生成関数を空にする
window.open2ch_url_card = function() {};
// 2. スクリプトの読み込みを阻止するObserver
const cardObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.tagName === 'SCRIPT' && node.src && node.src.includes('url_card')) {
node.remove();
}
}
}
});
cardObserver.observe(document.documentElement, { childList: true, subtree: true });
}
// 花火の無効化(スイッチON時のみ)
if (localStorage.getItem('ext_no_hanabi') === 'true') {
const hanabiObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.tagName === 'SCRIPT' && node.src && node.src.includes('hanabi.js')) {
node.remove();
}
}
}
});
hanabiObserver.observe(document.documentElement, { childList: true, subtree: true });
}
// YouTube展開の無効化(スイッチON時のみ)
if (localStorage.getItem('ext_no_yt') === 'true') {
const ytObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
// 1. スクリプトタグの阻止
if (node.tagName === 'SCRIPT' && node.src) {
if (node.src.includes('youtube.com/player_api') ||
node.src.includes('jquery.lazy.youtube')) {
node.remove();
}
}
// 2. 独自プレイヤー枠 <v> が作られた瞬間に消す
if (node.tagName === 'V' || node.querySelector('v')) {
const vTag = node.tagName === 'V' ? node : node.querySelector('v');
if (vTag) vTag.remove();
}
}
}
});
ytObserver.observe(document.documentElement, { childList: true, subtree: true });
}
// 雪の無効化(スイッチON時のみ)
if (localStorage.getItem('ext_no_snow') === 'true') {
const snowObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.tagName === 'SCRIPT' && node.src && node.src.includes('snow.js')) {
node.remove();
}
}
}
});
snowObserver.observe(document.documentElement, { childList: true, subtree: true });
}
// ==========================================
// 設定リスト:ここを増やすだけで、自動で段が増えます
// ==========================================
const customSettings = [
{
id: 'ext_no_rainbow',
label: '虹色無効',
title: '虹色アニメーションを停止して文字色を固定します',
css: `
/* .rainbow_cssと.rainbow-textの両方を無効化 */
body.ext_no_rainbow_enabled .rainbow_css,
body.ext_no_rainbow_enabled .rainbow-text {
animation: none !important;
-webkit-background-clip: initial !important;
-webkit-text-fill-color: initial !important;
color: inherit !important;
background: none !important;
}
/* キーフレームそのものを上書きして停止 */
body.ext_no_rainbow_enabled @keyframes rainbowFlow {
0% { background-position: 0% 0%; }
100% { background-position: 0% 0%; }
}
/* 新UI特有のアニメーションももしあればここで封じる */
`
},
{
id: 'ext_next_thread',
label: '次スレ',
title: '次スレ作成・検索ボタンを常時表示します',
css: `body.ext_next_thread_enabled div.next_thread_div { display: initial !important; }`
},
{
id: 'ext_no_bug',
label: '虫消し',
title: 'クリスマスの虫を消します',
css: `
body.ext_no_bug_enabled .bug { display: none !important; }
body.ext_no_bug_enabled body.valus { background-image: none !important; }
`
},
{
id: 'ext_no_hana',
label: '花消し',
title: '花びらを消します',
css: `
body.ext_no_hana_enabled .hana { display: none !important; }
`
},
{
id: 'ext_error_light',
label: 'Error軽',
title: 'komeエラーダイアログの描画負荷を軽減します',
css: `
body.ext_error_light_enabled .jconfirm,
body.ext_error_light_enabled .jconfirm *,
body.ext_error_light_enabled .jconfirm-bg,
body.ext_error_light_enabled .jconfirm-animated,
body.ext_error_light_enabled .jconfirm-box {
transition: none !important;
animation: none !important;
transform: none !important;
transition-duration: 0s !important;
animation-duration: 0s !important;
}
body.ext_error_light_enabled .jconfirm-bg {
opacity: 0 !important;
background: #000 !important;
}
body.ext_error_light_enabled .jconfirm-hilight-shake {
animation: none !important;
}
`
},
{
id: 'ext_notice',
label: 'お知らせ',
title: '板TOPのお知らせを検索窓の右に表示します',
css: `
/* オフの時は非表示 */
body:not(.ext_notice_enabled) #ext_board_notice { display: none !important; }
/* お知らせの基本スタイル */
#ext_board_notice {
margin-left: 10px;
font-size: 11px;
line-height: 1.3;
display: inline-block;
vertical-align: middle;
color: inherit;
}
/* 中のfontタグなどが持っている行間設定をリセット */
#ext_board_notice font { line-height: inherit !important; }
`
},
{
id: 'ext_clean_title',
label: 'スレタイ消',
title: 'タブに表示されるスレタイから!maxや【】内を削除します',
css: ``
},
{
id: 'ext_no_x_embed',
label: 'X無効化',
title: 'Twitter(X)の埋め込み展開を阻止します(要再読み込み)',
css: ``
},
{
id: 'ext_image_nori',
label: '画像海苔',
title: '画像URLへの直海苔が剥がせなくなる問題を修正します',
css: `
/* 海苔(<n>)の中の画像やリンクをクリック可能にし、海苔を剥がせるようにします */
body.ext_image_nori_enabled n {
pointer-events: auto !important;
cursor: pointer !important;
}
body.ext_image_nori_enabled n.open {
background: none !important;
color: inherit !important;
}
`
},
{
id: 'ext_no_card',
label: 'カード無効',
title: 'URLが書き込まれた際のカード化を阻止します(要再読み込み)',
css: ``
},
{
id: 'ext_no_hanabi',
label: '花火無効',
title: 'スレッド終了時の花火演出を無効化します(要再読み込み)',
css: `
/* 万が一JSが動いても表示されないよう念のためCSSでもガード */
body.ext_no_hanabi_enabled .hanabi_canvas { display: none !important; }
`
},
{
id: 'ext_no_yt',
label: 'つべ無効',
title: 'YouTube等の動画プレイヤー展開を阻止します',
css: `
/* 独自タグ <v> 単位でプレイヤーを完全に消す */
body.ext_no_yt_enabled v,
body.ext_no_yt_enabled .youtube_div,
body.ext_no_yt_enabled .video_div,
body.ext_no_yt_enabled iframe.iyoutube {
display: none !important;
}
`
},
{
id: 'ext_no_snow',
label: '雪消し',
title: '画面内に雪が降る演出を無効化します',
css: `
/* JSが動く前に雪のキャンバス等を非表示にする */
body.ext_no_snow_enabled #snow-canvas,
body.ext_no_snow_enabled .snow {
display: none !important;
}
`
},
];
function updateState(id, isEnabled) {
const className = id + '_enabled';
if (isEnabled) {
document.body.classList.add(className);
} else {
document.body.classList.remove(className);
}
if (id === 'ext_clean_title') handleTitleClean();
// お知らせがオンになった瞬間に取得を開始する
if (id === 'ext_notice' && isEnabled) {
fetchBoardNotice();
}
// 虹色無効がオンなら、新UI側の設定(rainbowTitleEnabled)をfalseにする
if (id === 'ext_no_rainbow') {
try {
// 無効化したいので、isEnabledがtrue(チェックあり)の時に'false'を書き込む
localStorage.setItem('rainbowTitleEnabled', isEnabled ? 'false' : 'true');
} catch(e) {}
}
// 雪消しがオンなら公式側の設定をfalse、オフならtrueにする
if (id === 'ext_no_snow') {
try {
localStorage.setItem('snowEnabled', isEnabled ? 'false' : 'true');
} catch(e) {}
}
// 画像海苔機能が有効な場合のみ、クリックで剥がれるようにイベントを補助
if (id === 'ext_image_nori') {
const noriHandler = function(e) {
// 設定がオフ、またはすでに剥がれている場合は何もしない
if (localStorage.getItem('ext_image_nori') !== 'true') return;
const nTag = e.target.closest('n');
if (nTag && !nTag.classList.contains('open')) {
nTag.classList.add('open');
}
};
// 重複登録を防ぐため削除してから登録
document.removeEventListener('click', noriHandler);
if (isEnabled) {
document.addEventListener('click', noriHandler);
}
}
}
// スレタイ修正用関数
function handleTitleClean() {
// チェックボックスがオフなら何もしない
if (localStorage.getItem('ext_clean_title') !== 'true') return;
// 削除パターン: !max(数字) や 【(文字列)】
const unkPattern = /(?:!max\d+)?\s*【[^】]+】\s*|!max\d+\s*/g;
const currentTitle = document.title;
const fixedTitle = currentTitle.replace(unkPattern, "");
// 現在のタイトルと修正後が違う場合のみ書き換える
if (currentTitle !== fixedTitle) {
document.title = fixedTitle;
}
}
function init() {
const parentContainer = document.querySelector('.options')?.parentNode;
if (!parentContainer) return;
let currentTargetRow = null;
customSettings.forEach((set, index) => {
// 4項目ごとに新しい行(div.options)を作成
if (index % 4 === 0) {
const newRow = document.createElement('div');
newRow.className = 'options';
const allRows = parentContainer.querySelectorAll('.options');
allRows[allRows.length - 1].after(newRow);
currentTargetRow = newRow;
}
const savedValue = localStorage.getItem(set.id) === 'true';
const label = document.createElement('label');
label.title = set.title;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = set.id;
checkbox.checked = savedValue;
label.appendChild(checkbox);
label.appendChild(document.createTextNode(set.label));
currentTargetRow.appendChild(label);
currentTargetRow.appendChild(document.createTextNode(' '));
const styleEl = document.createElement('style');
styleEl.id = 'style-' + set.id;
styleEl.textContent = set.css;
document.head.appendChild(styleEl);
updateState(set.id, savedValue);
checkbox.addEventListener('change', (e) => {
localStorage.setItem(set.id, e.target.checked);
updateState(set.id, e.target.checked);
});
});
}
// 二重実行を防ぐためのフラグ
let isInitDone = false;
let isNoticeDone = false;
const timer = setInterval(() => {
// 1. オプションパネルの初期化
if (!isInitDone && document.querySelector('.options')) {
isInitDone = true;
init();
}
// 2. お知らせの初期表示
const searchInput = document.querySelector('input[name="q"]');
const isNoticeEnabled = localStorage.getItem('ext_notice') === 'true';
if (!isNoticeDone && searchInput) {
if (isNoticeEnabled) {
fetchBoardNotice();
}
isNoticeDone = true;
}
if (isInitDone && isNoticeDone) {
clearInterval(timer);
}
}, 500);
// タイトルの変化を監視するMutationObserver
const titleObserver = new MutationObserver(() => {
handleTitleClean();
});
// <title>タグそのものを監視対象にする
const titleElement = document.querySelector('title');
if (titleElement) {
titleObserver.observe(titleElement, { childList: true });
}
// 初回実行
handleTitleClean();
// 二重取得防止用のフラグ
let isFetchingNotice = false;
// --- お知らせ取得関数 ---
async function fetchBoardNotice() {
const searchInput = document.querySelector('input[name="q"]');
if (!searchInput) return;
const searchDiv = searchInput.closest('div');
// 取得中、または既に要素が存在するなら即終了
if (isFetchingNotice || document.getElementById('ext_board_notice')) return;
isFetchingNotice = true; // 取得開始フラグを立てる
try {
// URLから板名を取得
const boardMatch = window.location.href.match(/\.net\/test\/read\.cgi\/([^\/]+)/);
if (!boardMatch) return;
const boardName = boardMatch[1];
const boardUrl = `${window.location.origin}/${boardName}/`;
// 板のTOPページを取得
const response = await fetch(boardUrl);
if (!response.ok) return;
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
// 背景色付きのテーブルを抽出
const tables = doc.querySelectorAll('table[bgcolor]');
let noticeHTML = "";
// 2番目のテーブルがお知らせ枠である可能性が高い(なんJ等)
if (tables.length >= 2) {
const secondTable = tables[1];
// 中身がスレ一覧(リンク集)でないことを確認して採用
if (!secondTable.querySelector('a[href^="/test/read.cgi/"]')) {
noticeHTML = secondTable.innerHTML;
}
}
// 挿入直前にもう一度だけ要素チェック(念のためのガード)
if (noticeHTML && !document.getElementById('ext_board_notice')) {
const noticeSpan = document.createElement('span');
noticeSpan.id = 'ext_board_notice';
const cleanHTML = noticeHTML
.replace(/<tr[^>]*>|<td[^>]*>|<\/tr>|<\/td>|<table[^>]*>|<\/table>/gi, '')
.trim();
noticeSpan.innerHTML = cleanHTML;
searchDiv.after(noticeSpan);
}
} catch (e) {
console.error("お知らせ取得エラー:", e);
} finally {
// 成功・失敗に関わらずフラグを下ろすが、要素が既にあれば何もしない
isFetchingNotice = false;
}
}
})();
|
作者:WaiON
| + | おんJ スレ主IDチェッカー |
使う時はCHECK_LIMITと取得時間調節するとおすすめ
https://greasyfork.org/ja/scripts/563392-%E3%81%8A%E3%82%93j-%E3%82%B9%E3%83%AC%E4%B8%BBid%E3%83%81%E3%82%A7%E3%83%83%E3%82%AB%E3%83%BC/code // ==UserScript==
// @name おんJ スレ主IDチェッカー
// @namespace http://tampermonkey.net/
// @version 3.7
// @author WaiON
// @description おんJでスレ主のIDをチェックし、NGIDが立てたスレは警告/非表示する。
// @match https://hayabusa.open2ch.net/livejupiter/*
// @match https://hayabusa.open2ch.net/test/read.cgi/livejupiter/*
// @license MIT
// @grant none
// ==/UserScript==
(function() {
'use strict';
//iOS26.1(Safari)で検証済み
// ==================== 設定エリア ====================
const CONFIG = {
CHECK_LIMIT: 10, // 自動取得する上位スレッド数
WAIT_MIN: 800, // 取得間隔 下限 (ms)
WAIT_MAX: 1600, // 取得間隔 上限 (ms)
CACHE_EXPIRY_HOURS: 4, // キャッシュ有効期限 (時間)
ID_LIST_KEY: 'ignv4livejupiter',
CACHE_KEY: 'ignv_cache',
HIDE_NG: false, // 警告/NGのスレッドを消す場合は true
// 各表示ページでの動作設定 (true で有効 / false で無効)
TARGET_VIEWS: {
'': true, // デフォルト(リンクの最後に#がつかない場合)
'#ikioi': true, // 勢い順
'#created': true, // 新スレ
'#ninzu': false, // 人数順
'#updated': false, // 新レス順
'#live': false, // 配信
'#history': false // 履歴
}
};
// ====================================================
const COLORS = {
LOADING: '#FFEB3B',
WARNING: '#F44336',
SAFE: '#4CAF50',
ERROR: '#000000'
};
const CACHE_EXPIRY_MS = CONFIG.CACHE_EXPIRY_HOURS * 60 * 60 * 1000;
let fetchedCount = 0;
let isProcessing = false;
let queue = [];
let observer = null;
// 現在のハッシュ値が設定で有効になっているか確認
const isTargetPage = () => {
const hash = window.location.hash;
return !!CONFIG.TARGET_VIEWS[hash];
};
const isThreadReadPage = () => window.location.pathname.includes('/test/read.cgi/livejupiter/');
const getBlackList = () => {
const raw = localStorage.getItem(CONFIG.ID_LIST_KEY) || "";
return raw.split('<D>').map(id => id.trim()).filter(id => id.length > 0);
};
const getCache = () => JSON.parse(localStorage.getItem(CONFIG.CACHE_KEY) || "{}");
const setCache = (url, ownerId) => {
const cache = getCache();
cache[url] = { ownerId, date: Date.now() };
localStorage.setItem(CONFIG.CACHE_KEY, JSON.stringify(cache));
};
const cleanOldCache = () => {
const cache = getCache();
const now = Date.now();
let changed = false;
for (const url in cache) {
if (now - cache[url].date > CACHE_EXPIRY_MS) {
delete cache[url];
changed = true;
}
}
if (changed) localStorage.setItem(CONFIG.CACHE_KEY, JSON.stringify(cache));
};
const normalizeUrl = (url) => {
const match = url.match(/\/test\/read\.cgi\/livejupiter\/\d+/);
return match ? match[0] : url;
};
const applyJudgment = (threadElem, lineElem, isNG, isError = false) => {
if (!threadElem || !lineElem) return;
if (isError) {
lineElem.style.backgroundColor = COLORS.ERROR;
return;
}
if (isNG) {
if (CONFIG.HIDE_NG) {
threadElem.style.display = 'none';
} else {
lineElem.style.backgroundColor = COLORS.WARNING;
}
} else {
lineElem.style.backgroundColor = COLORS.SAFE;
threadElem.style.display = '';
}
};
async function processQueue() {
if (isProcessing || queue.length === 0) return;
isProcessing = true;
try {
const blackList = getBlackList();
while (queue.length > 0) {
// 処理中にページが対象外(無効なハッシュ)に変わったら中止
if (!isTargetPage() || document.visibilityState === 'hidden') break;
const task = queue.shift();
const normalized = normalizeUrl(task.threadUrl);
const cache = getCache();
if (cache[normalized]) {
applyJudgment(task.threadElem, task.lineElem, blackList.includes(cache[normalized].ownerId));
continue;
}
if (fetchedCount >= CONFIG.CHECK_LIMIT) {
task.lineElem.remove();
continue;
}
try {
fetchedCount++;
const res = await fetch(normalized + "/-1");
const html = await res.text();
const idMatch = html.match(/class="_id"[^>]+val="([^"]+)"/) || html.match(/val="([^"]+)"[^>]+class="_id"/);
const ownerId = idMatch ? idMatch[1] : null;
if (ownerId) {
setCache(normalized, ownerId);
applyJudgment(task.threadElem, task.lineElem, blackList.includes(ownerId));
}
} catch (e) {
applyJudgment(task.threadElem, task.lineElem, false, true);
}
if (queue.length > 0) {
const waitTime = Math.floor(Math.random() * (CONFIG.WAIT_MAX - CONFIG.WAIT_MIN + 1)) + CONFIG.WAIT_MIN;
await new Promise(r => setTimeout(r, waitTime));
}
}
} finally {
isProcessing = false;
}
}
function initUI() {
if (!isTargetPage()) {
document.querySelectorAll('.id-checker-line').forEach(el => el.remove());
queue = [];
return;
}
const threads = Array.from(document.querySelectorAll('.thread'));
const cache = getCache();
const blackList = getBlackList();
let addedToQueue = false;
threads.forEach((thread, index) => {
if (thread.querySelector('.id-checker-line')) return;
const link = thread.querySelector('a');
if (!link) return;
const url = link.href;
const normalized = normalizeUrl(url);
const cachedData = cache[normalized];
thread.style.position = 'relative';
const line = document.createElement('div');
line.className = 'id-checker-line';
line.style.cssText = `position: absolute; top: 2px; bottom: 2px; right: 0; width: 6px; border-radius: 2px; z-index: 10; pointer-events: none;`;
thread.appendChild(line);
if (cachedData) {
applyJudgment(thread, line, blackList.includes(cachedData.ownerId));
} else if (index < CONFIG.CHECK_LIMIT) {
line.style.backgroundColor = COLORS.LOADING;
queue.push({ threadUrl: url, lineElem: line, threadElem: thread });
addedToQueue = true;
}
});
if (addedToQueue && !isProcessing) processQueue();
}
const resetAndStart = () => {
isProcessing = false;
queue = [];
fetchedCount = 0;
document.querySelectorAll('.id-checker-line').forEach(el => el.remove());
if (isThreadReadPage()) {
const idSpan = document.querySelector('li[val="1"] ._id, dl[val="1"] ._id');
if (idSpan) setCache(normalizeUrl(window.location.href), idSpan.getAttribute('val'));
return;
}
if (!isTargetPage()) return;
cleanOldCache();
if (observer) observer.disconnect();
const container = document.querySelector('#current_thread_list') || document.querySelector('#thread_list') || document.body;
observer = new MutationObserver(() => initUI());
observer.observe(container, { childList: true, subtree: true });
initUI();
};
window.addEventListener('hashchange', resetAndStart);
window.addEventListener('popstate', resetAndStart);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') processQueue();
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', resetAndStart);
} else {
resetAndStart();
}
})();
|
| + | ... |
コメント:
|
| + | ... |
コメント:
|