- // ==UserScript==
- // @name buyNow!
- // @namespace http://2chan.net/
- // @version 0.8.14
- // @description ふたばちゃんねるのスレッド上で貼られた特定のECサイトのURLからタイトルとあれば価格と画像を取得する
- // @author ame-chan
- // @match http://*.2chan.net/b/res/*
- // @match https://*.2chan.net/b/res/*
- // @match https://kako.futakuro.com/futa/*
- // @match https://tsumanne.net/si/data/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=2chan.net
- // @grant GM_xmlhttpRequest
- // @connect amazon.co.jp
- // @connect www.amazon.co.jp
- // @connect amzn.to
- // @connect amzn.asia
- // @connect media-amazon.com
- // @connect m.media-amazon.com
- // @connect dlsite.com
- // @connect img.dlsite.jp
- // @connect bookwalker.jp
- // @connect c.bookwalker.jp
- // @connect store.steampowered.com
- // @connect cdn.akamai.steamstatic.com
- // @connect cdn.cloudflare.steamstatic.com
- // @connect store.cloudflare.steamstatic.com
- // @connect youtube.com
- // @connect youtu.be
- // @connect nintendo.com
- // @connect store-jp.nintendo.com
- // @connect dmm.co.jp
- // @connect www.dmm.co.jp
- // @connect dlsoft.dmm.co.jp
- // @connect pics.dmm.co.jp
- // @connect doujin-assets.dmm.co.jp
- // @license MIT
- // ==/UserScript==
- (function () {
- 'use strict';
- const WHITE_LIST_DOMAINS = [
- 'amazon.co.jp',
- 'amzn.to',
- 'amzn.asia',
- 'dlsite.com',
- 'bookwalker.jp',
- 'store.steampowered.com',
- 'youtube.com',
- 'youtu.be',
- 'store-jp.nintendo.com',
- 'dlsoft.dmm.co.jp',
- 'www.dmm.co.jp',
- ];
- const WHITE_LIST_SELECTORS = (() => WHITE_LIST_DOMAINS.map((domain) => `a[href*="${domain}"]`).join(','))();
- const convertHostname = (path) => new URL(path).hostname;
- const isAmazon = (path) => /^(www\.)?amazon.co.jp|amzn\.to|amzn\.asia$/.test(convertHostname(path));
- const isDLsite = (path) => /^(www\.)?dlsite\.com$/.test(convertHostname(path));
- const isBookwalker = (path) => /^(www\.)?bookwalker.jp$/.test(convertHostname(path));
- const isSteam = (path) => /^store\.steampowered\.com$/.test(convertHostname(path));
- const isYouTube = (path) => /^youtu\.be|(www\.)?youtube.com$/.test(convertHostname(path));
- const isNintendoStore = (path) => /^store-jp\.nintendo\.com$/.test(convertHostname(path));
- const isFanzaDoujin = (path) => /^(www\.)?dmm\.co\.jp$/.test(convertHostname(path));
- const isFanzaDlsoft = (path) => /^dlsoft\.dmm\.co\.jp$/.test(convertHostname(path));
- const isProductPage = (url) =>
- /^https?:\/\/(?:www\.)?amazon(.+?)\/(?:exec\/obidos\/ASIN|gp\/product|gp\/aw\/d|o\/ASIN|(?:.+?\/)?dp|d)\/[A-Z0-9]{10}/.test(
- url,
- ) ||
- /^https?:\/\/amzn.(asia|to)\//.test(url) ||
- /^https?:\/\/(www\.)?dlsite\.com\/.+?\/[A-Z0-9]{8,}(\.html)?/.test(url) ||
- /^https?:\/\/(www\.)?bookwalker\.jp\/[a-z0-9]{10}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}/.test(url) ||
- /^https?:\/\/(www\.)?bookwalker\.jp\/(series|tag)\/[0-9]+\//.test(url) ||
- /^https?:\/\/store.steampowered.com\/(agecheck\/)?app\/\d+/.test(url) ||
- /^https?:\/\/(youtu\.be\/|((www|m)\.)?youtube.com\/(watch\?v=|live\/))\w+/.test(url) ||
- /^https?:\/\/store-jp\.nintendo\.com\/(list\/software\/(D)?[0-9]+.html|item\/software\/(D)?[0-9]+)/.test(url) ||
- /^https?:\/\/dlsoft\.dmm\.co\.jp\/(list|detail)\/.+?/.test(url) ||
- /^https?:\/\/(www\.)?dmm\.co\.jp\/dc\/doujin\/.+?/.test(url);
- const getBrandName = (url) => {
- if (isAmazon(url)) {
- return 'amazon';
- } else if (isDLsite(url)) {
- return 'dlsite';
- } else if (isBookwalker(url)) {
- return 'bookwalker';
- } else if (isSteam(url)) {
- return 'steam';
- } else if (isYouTube(url)) {
- return 'youtube';
- } else if (isNintendoStore(url)) {
- return 'nintendo';
- } else if (isFanzaDlsoft(url)) {
- return 'fanzaDlsoft';
- } else if (isFanzaDoujin(url)) {
- return 'fanzaDoujin';
- }
- return '';
- };
- const getOgpImage = (targetDocument) => targetDocument.querySelector('meta[property="og:image"]')?.content || '';
- const getSelectorConditions = {
- amazon: {
- price: (targetDocument) => {
- const priceRange = () => {
- const rangeElm = targetDocument.querySelector('.a-price-range');
- if (!rangeElm) return 0;
- rangeElm.querySelectorAll('.a-offscreen').forEach((el) => el.remove());
- return rangeElm.textContent?.replace(/[\s]+/g, '');
- };
- try {
- const price =
- targetDocument.querySelector('#twister-plus-price-data-price')?.value ||
- targetDocument.querySelector('#kindle-price')?.textContent?.replace(/[\s¥,]+/g, '') ||
- targetDocument.querySelector('[name="displayedPrice"]')?.value ||
- targetDocument.querySelector('.a-price-whole')?.textContent?.replace(/[\s¥,]+/g, '');
- return Math.round(Number(price)) || priceRange() || 0;
- } catch (e) {
- return 0;
- }
- },
- image: (targetDocument) =>
- targetDocument.querySelector('#landingImage')?.src ||
- targetDocument.querySelector('.unrolledScrollBox li:first-child img')?.src ||
- targetDocument.querySelector('[data-a-image-name]')?.src ||
- targetDocument.querySelector('#imgBlkFront')?.src,
- title: (targetDocument) =>
- targetDocument.querySelector('#productTitle')?.textContent ||
- targetDocument.querySelector('#title')?.textContent ||
- targetDocument.querySelector('#landingImage')?.alt ||
- targetDocument.querySelector('.unrolledScrollBox li:first-child img')?.alt ||
- targetDocument.querySelector('[data-a-image-name]')?.alt ||
- targetDocument.querySelector('#imgBlkFront')?.alt,
- },
- dlsite: {
- price: (targetDocument) => {
- try {
- const url = targetDocument.querySelector('meta[property="og:url"]')?.content;
- const productId = url.split('/').pop()?.replace('.html', '');
- const priceElm = targetDocument.querySelector(`[data-product_id="${productId}"][data-price]`);
- return parseInt(priceElm?.getAttribute('data-price') || '0', 10);
- } catch (e) {
- return 0;
- }
- },
- image: getOgpImage,
- },
- bookwalker: {
- price: (targetDocument) => {
- try {
- const price =
- Number(targetDocument.querySelector('.t-c-sales-price__value')?.textContent?.replace(/[^0-9]/g, '')) ||
- Number(
- targetDocument
- .querySelector('.m-tile-list .m-tile .m-book-item__price-num')
- ?.textContent?.replace(/,/g, ''),
- ) ||
- Number(targetDocument.querySelector('#jsprice')?.textContent?.replace(/[円,]/g, ''));
- return Number.isInteger(price) && price > 0 ? price : 0;
- } catch (e) {
- return 0;
- }
- },
- image: (targetDocument) =>
- targetDocument.querySelector('.m-tile-list .m-tile img')?.getAttribute('data-original') ||
- getOgpImage(targetDocument),
- },
- steam: {
- price: (targetDocument) => {
- try {
- const elm =
- targetDocument.querySelector('.game_area_purchase_game_wrapper .game_purchase_price.price') ||
- targetDocument.querySelector('.game_area_purchase_game .game_purchase_price.price') ||
- targetDocument.querySelector('.game_area_purchase_game_wrapper .discount_final_price');
- const price = elm?.firstChild?.textContent?.replace(/[¥,\s\t\r\n]+/g, '');
- const isComingSoon = targetDocument.querySelector('.game_area_comingsoon');
- const isAgeCheck = targetDocument.querySelector('#app_agegate');
- const num = Number(price);
- if (isAgeCheck) {
- return 'ログインか年齢確認が必要です';
- } else if (isComingSoon) {
- return '近日登場';
- } else if (Number.isInteger(num) && num > 0) {
- return num;
- } else if (typeof price === 'string') {
- return price;
- }
- return 0;
- } catch (e) {
- return 0;
- }
- },
- image: getOgpImage,
- },
- nintendo: {
- price: (targetDocument) => {
- try {
- const mobifyData = targetDocument.querySelector('#mobify-data');
- if (mobifyData instanceof HTMLScriptElement && mobifyData.textContent) {
- const data = JSON.parse(mobifyData.textContent.trim());
- const queries = data?.__PRELOADED_STATE__?.__reactQuery?.queries ?? [];
- for (const query of queries) {
- const price = query?.state?.data?.price ?? 0;
- if (price > 0) {
- return price;
- }
- }
- }
- const priceElm = targetDocument.querySelector('.js-productMainRenderedPrice > span:first-of-type');
- const priceText = priceElm?.textContent?.replace(/,/g, '');
- const price = Number(priceText);
- if (Number.isInteger(price) && price > 0) {
- return price;
- } else if (typeof priceText === 'string') {
- return priceText;
- }
- return 0;
- } catch (e) {
- return 0;
- }
- },
- image: getOgpImage,
- },
- fanzaDlsoft: {
- price: (targetDocument) => {
- try {
- const priceElm =
- targetDocument.querySelector('.tx-bskt-price') ||
- targetDocument.querySelector('.sellingPrice__discountedPrice');
- const priceText = priceElm?.textContent?.replace(/[,円]/g, '');
- const price = Number(priceText);
- if (Number.isInteger(price) && price > 0) {
- return price;
- } else if (typeof priceText === 'string') {
- return priceText;
- }
- return 0;
- } catch (e) {
- return 0;
- }
- },
- image: (targetDocument) => {
- return getOgpImage(targetDocument) || targetDocument.querySelector('.d-item #list .img img')?.src || '';
- },
- },
- fanzaDoujin: {
- price: (targetDocument) => {
- try {
- const priceText =
- targetDocument.querySelector('p.priceList__main')?.textContent?.replace(/[,円]/g, '') ||
- targetDocument.querySelector('.purchase__btn')?.getAttribute('data-price');
- const price = Number(priceText);
- if (Number.isInteger(price) && price > 0) {
- return price;
- } else if (typeof priceText === 'string') {
- return priceText;
- }
- return 0;
- } catch (e) {
- return 0;
- }
- },
- image: (targetDocument) => {
- return (
- getOgpImage(targetDocument) || targetDocument.querySelector('.productList .tileListImg__tmb img')?.src || ''
- );
- },
- },
- // 画像のみ取得
- youtube: {
- price: () => 0,
- image: getOgpImage,
- },
- };
- const addedStyle = `<style id="userjs-buyNow-style">
- .userjs-title {
- display: flex;
- flex-direction: row;
- margin: 8px 0 16px;
- gap: 16px;
- padding: 16px;
- line-height: 1.6 !important;
- color: #ff3860 !important;
- background-color: #fff;
- border-radius: 4px;
- }
- .userjs-title-inner {
- display: flex;
- flex-direction: column;
- gap: 8px;
- line-height: 1.6 !important;
- color: #ff3860 !important;
- }
- .userjs-link {
- padding-right: 24px;
- background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2038%2038%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20stroke%3D%22%23000%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20transform%3D%22translate(1%201)%22%20stroke-width%3D%222%22%3E%3Ccircle%20stroke-opacity%3D%22.5%22%20cx%3D%2218%22%20cy%3D%2218%22%20r%3D%2218%22%2F%3E%3Cpath%20d%3D%22M36%2018c0-9.94-8.06-18-18-18%22%3E%20%3CanimateTransform%20attributeName%3D%22transform%22%20type%3D%22rotate%22%20from%3D%220%2018%2018%22%20to%3D%22360%2018%2018%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%2F%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E');
- background-repeat: no-repeat;
- background-position: right center;
- }
- .userjs-imageWrap {
- width: 150px;
- }
- .userjs-imageWrap.-center {
- text-align: center;
- }
- .userjs-imageWrap.-large {
- width: 600px;
- }
- .userjs-image {
- max-width: none !important;
- max-height: none !important;
- transition: all 0.2s ease-in-out;
- border-radius: 4px;
- }
- .userjs-price {
- display: block;
- color: #228b22 !important;
- font-weight: 700;
- }
- </style>`;
- if (!document.querySelector('#userjs-buyNow-style')) {
- document.head.insertAdjacentHTML('beforeend', addedStyle);
- }
- class FileReaderEx extends FileReader {
- constructor() {
- super();
- }
- #readAs(blob, ctx) {
- return new Promise((res, rej) => {
- super.addEventListener('load', ({ target }) => target?.result && res(target.result));
- super.addEventListener('error', ({ target }) => target?.error && rej(target.error));
- super[ctx](blob);
- });
- }
- readAsArrayBuffer(blob) {
- return this.#readAs(blob, 'readAsArrayBuffer');
- }
- readAsDataURL(blob) {
- return this.#readAs(blob, 'readAsDataURL');
- }
- }
- const fetchData = (url, responseType) =>
- new Promise((resolve) => {
- let options = {
- method: 'GET',
- url,
- timeout: 10000,
- onload: (result) => {
- if (result.status === 200 || result.status === 404) {
- return resolve(result.response);
- }
- return resolve(false);
- },
- onerror: () => resolve(false),
- ontimeout: () => resolve(false),
- };
- if (typeof responseType === 'string') {
- options = {
- ...options,
- responseType,
- };
- }
- GM_xmlhttpRequest(options);
- });
- const setFailedText = (linkElm) => {
- if (linkElm && linkElm instanceof HTMLAnchorElement) {
- linkElm.insertAdjacentHTML('afterend', '<span class="userjs-title">データ取得失敗</span>');
- }
- };
- const getPriceText = (price) => {
- let priceText = price;
- if (!price) return '';
- if (typeof price === 'number' && Number.isInteger(price) && price > 0) {
- priceText = new Intl.NumberFormat('ja-JP', {
- style: 'currency',
- currency: 'JPY',
- }).format(price);
- }
- return `<span class="userjs-price">${priceText}</span>`;
- };
- const setTitleText = ({ targetDocument, selectorCondition, linkElm, brandName }) => {
- let titleElm = targetDocument.querySelector('title');
- let title = titleElm?.textContent ?? '';
- // Amazonはtitleタグが無い場合がある
- if (title === '' && brandName === 'amazon') {
- title = selectorCondition.title(targetDocument);
- }
- if (!title) {
- setFailedText(linkElm);
- return false;
- }
- const price = selectorCondition.price(targetDocument);
- const priceText = getPriceText(price);
- const nextSibling = linkElm.nextElementSibling;
- if (nextSibling && nextSibling instanceof HTMLElement && nextSibling.tagName.toLowerCase() === 'br') {
- nextSibling.style.display = 'none';
- }
- if (title === 'サイトエラー') {
- const errorText = targetDocument.querySelector('#error_box')?.textContent;
- if (errorText) {
- title = errorText;
- }
- }
- if (linkElm && linkElm instanceof HTMLAnchorElement) {
- linkElm.insertAdjacentHTML(
- 'afterend',
- `<div class="userjs-title">
- <span class="userjs-title-inner">${title}${priceText}</span>
- </div>`,
- );
- return true;
- }
- return false;
- };
- const setImageElm = async ({ imagePath, titleTextElm }) => {
- const imageMinSize = 150;
- const imageMaxSize = 600;
- const imageEventHandler = (e) => {
- const self = e.currentTarget;
- const div = self?.parentElement;
- if (!(self instanceof HTMLImageElement) || !div) return;
- if (self.width === imageMinSize) {
- div.classList.remove('-center');
- div.classList.add('-large');
- self.width = imageMaxSize;
- } else {
- div.classList.remove('-large');
- if (self.naturalWidth > imageMinSize) {
- self.width = imageMinSize;
- } else {
- div.classList.add('-center');
- self.width = self.naturalWidth;
- }
- }
- };
- const blob = await fetchData(imagePath, 'blob');
- const titleInnerElm = titleTextElm.querySelector('.userjs-title-inner');
- if (!blob || !titleInnerElm) return false;
- const dataUrl = await new FileReaderEx().readAsDataURL(blob);
- const div = document.createElement('div');
- div.classList.add('userjs-imageWrap');
- const img = document.createElement('img');
- img.addEventListener('load', () => {
- if (img.naturalWidth < imageMinSize) {
- img.width = img.naturalWidth;
- }
- });
- img.src = dataUrl;
- img.width = imageMinSize;
- img.classList.add('userjs-image');
- div.appendChild(img);
- img.addEventListener('click', imageEventHandler);
- titleInnerElm.insertAdjacentElement('beforebegin', div);
- return img;
- };
- const setLoading = (linkElm) => {
- const parentElm = linkElm.parentElement;
- if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
- return;
- }
- linkElm.classList.add('userjs-link');
- };
- const removeLoading = (targetElm) => targetElm.classList.remove('userjs-link');
- // AgeCheck
- const isAgeCheck = (targetDocument, selector) => targetDocument.querySelector(selector) !== null;
- const getAgeCheckConfirmAdultPageHref = ({ targetDocument, selector, domain = '' }) => {
- const yesBtnLinkElm = targetDocument.querySelector(selector);
- if (yesBtnLinkElm instanceof HTMLAnchorElement) {
- return `${domain}${yesBtnLinkElm.getAttribute('href')}`;
- }
- return false;
- };
- const getAgeCheckPassedAdultDocument = async ({ targetDocument, linkElm, parser, selector, domain = '' }) => {
- const newHref = getAgeCheckConfirmAdultPageHref({
- targetDocument,
- selector,
- domain,
- });
- const htmlData = newHref && (await fetchData(newHref));
- if (!htmlData) {
- setFailedText(linkElm);
- removeLoading(linkElm);
- return false;
- }
- return parser.parseFromString(htmlData, 'text/html');
- };
- const getNewDocument = async ({ targetDocument, linkElm, parser, brandName }) => {
- const domain = brandName === 'amazon' ? 'https://www.amazon.co.jp' : '';
- const selector = (() => {
- switch (brandName) {
- case 'amazon':
- return '#black-curtain-yes-button a';
- case 'fanzaDlsoft':
- case 'fanzaDoujin':
- return '.ageCheck__link.ageCheck__link--r18';
- default:
- return false;
- }
- })();
- if (selector) {
- const newDocument = await getAgeCheckPassedAdultDocument({
- targetDocument,
- linkElm,
- parser,
- selector,
- domain,
- });
- if (newDocument) {
- return newDocument;
- }
- }
- return false;
- };
- // ふたクロで「新着レスに自動スクロール」にチェックが入っている場合画像差し込み後に下までスクロールさせる
- const scrollIfAutoScrollIsEnabled = () => {
- const checkboxElm = document.querySelector('#autolive_scroll');
- const readmoreElm = document.querySelector('#res_menu');
- if (checkboxElm === null || readmoreElm === null || !checkboxElm?.checked) {
- return;
- }
- const elementHeight = readmoreElm.offsetHeight;
- const viewportHeight = window.innerHeight;
- const offsetTop = readmoreElm.offsetTop;
- window.scrollTo({
- top: offsetTop - viewportHeight + elementHeight,
- behavior: 'smooth',
- });
- };
- // AmazonURLの正規化(amzn.toやamzn.asiaなど)
- const canonicalizeAmazonURL = (targetDocument, linkElm) => {
- const scriptElms = targetDocument.querySelectorAll('script');
- let asin = '';
- for (const scriptElm of scriptElms) {
- const text = scriptElm.textContent;
- if (text && text.includes('var opts')) {
- [, asin] = text.match(/asin:\s?\"(.+?)\"/) || [];
- break;
- }
- }
- if (asin && asin.length) {
- linkElm.href = `https://www.amazon.co.jp/dp/${asin}`;
- linkElm.textContent = `https://www.amazon.co.jp/dp/${asin}`;
- }
- };
- const insertURLData = async (linkElm) => {
- const parentElm = linkElm.parentElement;
- if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
- removeLoading(linkElm);
- return;
- }
- const brandName = getBrandName(linkElm.href);
- if (brandName === '') {
- setFailedText(linkElm);
- removeLoading(linkElm);
- return;
- }
- const htmlData = await fetchData(linkElm.href);
- if (!htmlData) {
- setFailedText(linkElm);
- removeLoading(linkElm);
- return;
- }
- const adultPageLists = ['amazon', 'fanzaDlsoft', 'fanzaDoujin'];
- const parser = new DOMParser();
- let targetDocument = parser.parseFromString(htmlData, 'text/html');
- // AmazonやFANZAのアダルトページ確認画面スキップ
- if (adultPageLists.includes(brandName)) {
- const is18xAmazon = isAgeCheck(targetDocument, '#black-curtain-warning');
- const is18xFanza = isAgeCheck(targetDocument, '.ageCheck');
- if (is18xAmazon || is18xFanza) {
- const newDocument = await getNewDocument({
- targetDocument,
- linkElm,
- parser,
- brandName,
- });
- if (newDocument) {
- targetDocument = newDocument;
- }
- }
- }
- const selectorCondition = getSelectorConditions[brandName];
- const isSuccessSetTitleText = setTitleText({
- targetDocument,
- selectorCondition,
- linkElm,
- brandName,
- });
- const titleTextElm = linkElm.nextElementSibling;
- const imagePath = selectorCondition.image(targetDocument);
- if (imagePath && titleTextElm) {
- const imageElm = await setImageElm({
- imagePath,
- titleTextElm,
- });
- if (imageElm instanceof HTMLImageElement) {
- imageElm.onload = () => scrollIfAutoScrollIsEnabled();
- }
- } else if (!imagePath && !isSuccessSetTitleText) {
- const failedElm = linkElm.nextElementSibling;
- const hasFailedElm = failedElm instanceof HTMLElement && failedElm.classList.contains('userjs-title');
- if (!hasFailedElm) {
- setFailedText(linkElm);
- }
- }
- if (brandName === 'amazon') {
- canonicalizeAmazonURL(targetDocument, linkElm);
- }
- removeLoading(linkElm);
- };
- const replaceDefaultURL = (targetElm) => {
- const linkElms = targetElm.querySelectorAll('a[href]');
- const replaceUrl = (url) => {
- const regex = /http:\/\/www\.dlsite\.com\/(.+?)\/dlaf\/=\/link\/work\/aid\/[a-zA-Z]+\/id\/(RJ[0-9]+)\.html/;
- const newUrlFormat = 'https://www.dlsite.com/$1/work/=/product_id/$2.html';
- return url.replace(regex, newUrlFormat);
- };
- const decodeHtmlEntities = (text) => {
- return text.replace(/&#(\d+);/g, (_, dec) => {
- return String.fromCharCode(dec);
- });
- };
- for (const linkElm of linkElms) {
- const brandName = getBrandName(linkElm.href);
- const href = linkElm.getAttribute('href');
- if (brandName === 'dlsite') {
- linkElm.href = decodeHtmlEntities(decodeURIComponent(replaceUrl(href.replace('/bin/jump.php?', ''))));
- } else {
- linkElm.href = decodeHtmlEntities(decodeURIComponent(href.replace('/bin/jump.php?', '')));
- }
- }
- };
- const processingQueue = [];
- let activeRequests = 0;
- const MAX_CONCURRENT_REQUESTS = 3;
- const processQueue = async () => {
- while (activeRequests < MAX_CONCURRENT_REQUESTS && processingQueue.length > 0) {
- const linkElm = processingQueue.shift();
- if (linkElm) {
- activeRequests++;
- insertURLData(linkElm).finally(() => {
- activeRequests--;
- processQueue();
- });
- }
- }
- };
- const observeLinkElements = (linkElms) => {
- const winH = window.innerHeight;
- const observerOptions = {
- root: null,
- // ビューポートの上下にビューポートの高さ分のマージンを持たせる
- rootMargin: `${winH}px 0px`,
- threshold: 0,
- };
- const observer = new IntersectionObserver(async (entries, observer) => {
- for (const entry of entries) {
- if (entry.isIntersecting) {
- const linkElm = entry.target;
- observer.unobserve(linkElm);
- // 見えるようになったリンクを処理キューに追加
- processingQueue.push(linkElm);
- processQueue();
- }
- }
- }, observerOptions);
- linkElms.forEach((linkElm) => observer.observe(linkElm));
- };
- const searchLinkElements = async (targetElm) => {
- const linkElms = targetElm.querySelectorAll(WHITE_LIST_SELECTORS);
- if (!linkElms.length) return;
- for (const linkElm of linkElms) {
- setLoading(linkElm);
- }
- observeLinkElements(Array.from(linkElms));
- };
- const mutationLinkElements = async (mutations) => {
- for (const mutation of mutations) {
- for (const addedNode of mutation.addedNodes) {
- if (!(addedNode instanceof HTMLElement)) continue;
- replaceDefaultURL(addedNode);
- searchLinkElements(addedNode);
- }
- }
- };
- const threadElm = document.querySelector('.thre');
- if (threadElm instanceof HTMLElement) {
- replaceDefaultURL(threadElm);
- searchLinkElements(threadElm);
- const observer = new MutationObserver(mutationLinkElements);
- observer.observe(threadElm, {
- childList: true,
- });
- }
- })();