Greasy Fork 支持简体中文。

erogamescape-compare-sale-tables

「ErogameScape -エロゲー批評空間-」のセール表同士を比較して差分を出す

// ==UserScript==
// @name         erogamescape-compare-sale-tables
// @namespace    http://erogamescape.dyndns.org/~ap2/ero/toukei_kaiseki/
// @version      1.0.1
// @description  「ErogameScape -エロゲー批評空間-」のセール表同士を比較して差分を出す
// @author       ame-chan
// @match        http://erogamescape.dyndns.org/~ap2/ero/toukei_kaiseki/*
// @match        https://erogamescape.dyndns.org/~ap2/ero/toukei_kaiseki/*
// @match        http://erogamescape.org/~ap2/ero/toukei_kaiseki/*
// @match        https://erogamescape.org/~ap2/ero/toukei_kaiseki/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=erogamescape.dyndns.org
// @require      https://greasyfork.org/scripts/473024-tablesort-library/code/tablesort-library.js?version=1234866
// @require      https://greasyfork.org/scripts/473022-erogamescape-table-sorter/code/erogamescape-table-sorter.user.js?version=1.0.1
// @grant        unsafeWindow
// @license      MIT
// @run-at       document-idle
// @connect      erogamescape.dyndns.org
// ==/UserScript==
(async () => {
  'use strict';
  const addStyle = `<style id="compareSaleTables-userjs-style">
  body.is-loading::before {
    position: absolute;
    top: 0;
    left: 0;
    content: "";
    width: 100vw;
    height: 100vh;
    background-color: rgba(255, 255, 255, 0.5);
    z-index: 1000;
  }
  body.is-loading::after {
    position: absolute;
    top: calc(50vh - 125px);
    left: calc(50vw - 125px);
    content: "";
    display: block;
    width: 250px;
    height: 250px;
    background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='margin: auto; background: none; display: block; shape-rendering: auto; animation-play-state: running; animation-delay: 0s;' width='250px' height='250px' viewBox='0 0 100 100' preserveAspectRatio='xMidYMid'%3E%3Cg transform='rotate(0 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.9166666666666666s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(30 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.8333333333333334s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(60 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.75s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(90 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.6666666666666666s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(120 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.5833333333333334s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(150 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.5s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(180 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.4166666666666667s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(210 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.3333333333333333s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(240 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.25s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(270 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.16666666666666666s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(300 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.08333333333333333s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(330 50 50)' style='animation-play-state: running; animation-delay: 0s;'%3E%3Crect x='47.5' y='27' rx='2.5' ry='2.9' width='5' height='10' fill='%23202020' style='animation-play-state: running; animation-delay: 0s;'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='0s' repeatCount='indefinite' style='animation-play-state: running; animation-delay: 0s;'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3C/svg%3E");
    background-position: center;
    background-repeat: no-repeat;
    background-size: cover;
    z-index: 1001;
  }
  #extension-wrapper {
    position: fixed;
    top: 8px;
    right: 8px;
    padding: 8px;
    width: 300px;
    height: auto;
    text-align: left;
    background-color: #fff;
    border: 1px solid #202020;
    border-radius: 6px;
    z-index: 99999;
  }
  #extension-diffNotes {
    position: relative;
    display: flex;
    align-items: center;
    margin: 0;
    padding: 0;
    user-select: none;
    cursor: pointer;
  }
  #extension-diffNotes::-webkit-details-marker {
    display: none;
  }
  #extension-diffNotes > span {
    padding-left: 8px;
    width: calc(100% - 24px);
  }
  #extension-diffNotes::before {
    display: block;
    content: "";
    width: 24px;
    height: 24px;
    background-image: url("data:image/svg+xml,%3Csvg width='800px' height='800px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.1018 16.9814C5.02785 16.9814 4.45387 15.7165 5.16108 14.9083L10.6829 8.59762C11.3801 7.80079 12.6197 7.80079 13.3169 8.59762L18.8388 14.9083C19.5459 15.7165 18.972 16.9814 17.898 16.9814H6.1018Z' fill='%23212121'/%3E%3C/svg%3E");
    background-size: contain;
  }
  #extension-wrapper[open] #extension-diffNotes::before {
    transform: rotate(180deg);
  }
  #extension-diffLists {
    overflow: hidden;
    position: relative;
    display: flex;
    margin: 4px 0 0;
    padding: 0;
    flex-wrap: wrap;
    gap: 4px;
  }
  #extension-diffLists.is-loading::before {
    position: absolute;
    top: 0;
    left: 0;
    content: "";
    width: 100%;
    height: 100%;
    background-color: rgba(255, 255, 255, 0.75);
  }
  #extension-diffLists.is-loading label,
  #extension-diffLists.is-loading label input {
    pointer-events: none;
  }
  #extension-diffLists label {
    display: flex;
    margin: 0;
    padding: 4px 8px;
    color: #202020;
    background: #bff;
    list-style-type: none;
    text-decoration: none;
    border: 1px solid #202020;
    border-radius: 4px;
    cursor: pointer;
  }
  #extension-diffLists label.is-hidden {
    display: none;
  }
  #extension-diffLists label:focus {
    outline: 0;
  }
  #extension-diffLists label:hover {
    background: #ffb;
  }
  #extension-diffLists label input {
    margin-right: 8px;
  }
  #extension-diffLists label input:focus {
    outline: 0;
  }
  .toukei_table.is-changed td {
    position: relative;
  }
  .toukei_table.is-changed div.tooltip {
    top: 0;
    left: -162px;
    align-items: center;
    justify-content: center;
    background-image: none;
    box-shadow: none;
    border: 1px solid #202020;
  }
  </style>`;
  const replacer = (str) => str.replace(/[\n\t\s]+/g, '');
  const getFileName = (path) => (path.split('/').pop() || '').replace('.php', '');
  const selector = '.toukei_table a[href*="game.php"]';
  const tableWrapperElm = document.querySelector('.recently_game_list');
  const toukeiTableElm = document.querySelector('.toukei_table');
  const targetElms = document.querySelectorAll(selector);
  const hasSaleInfo = (() => {
    const text = document.querySelector('.toukei_table td[style*="white-space"]')?.textContent ?? '';
    return /OFF|[0-9]+本で/.test(text);
  })();
  const fullPath = '/~ap2/ero/toukei_kaiseki/';
  if (toukeiTableElm === null || tableWrapperElm === null || !targetElms.length || !hasSaleInfo) {
    return;
  }
  if (document.querySelector('#compareSaleTables-userjs-style') === null) {
    document.head.insertAdjacentHTML('beforeend', addStyle);
  }
  const GLOBAL = {
    /** 比較済みならtrue */ isChangedTable: false,
    /** ローディング監視 */ isLoading: false,
  };
  const viewPageData = (() => {
    const result = {
      hasFanza: false,
      hasDlsite: false,
      fileName: '',
    };
    const titleElm = document.querySelector('#main_data_title');
    if (titleElm === null) return result;
    const title = replacer(titleElm.textContent || '');
    if (title === '') return result;
    result.hasFanza = (title.match(/FANZA/i) || []).length > 0;
    result.hasDlsite = (title.match(/DLSite/i) || []).length > 0;
    result.fileName = getFileName(location.pathname);
    return result;
  })();
  const fetchHTML = async (url) => {
    let topPageHtml = '';
    try {
      const res = await fetch(url);
      if (res.ok) {
        topPageHtml = await res.text();
      } else {
        throw new Error(res.statusText);
      }
    } catch (e) {
      throw new Error(String(e));
    }
    if (topPageHtml !== '') {
      const parser = new DOMParser();
      return parser.parseFromString(topPageHtml, 'text/html');
    }
    return false;
  };
  const getCampaignList = async () => {
    const topPageHTML = await fetchHTML(fullPath);
    if (topPageHTML === false) return {};
    const campaignLinks = topPageHTML.querySelectorAll('#campaignlist a[href*="half_price"]');
    const campaignList = {};
    for (const campaignLinkElm of campaignLinks) {
      const campaignName = replacer(campaignLinkElm.closest('dd')?.previousElementSibling?.textContent ?? '');
      if (campaignName === '') continue;
      const hasOriginalUrl = campaignLinkElm.href.includes(`${viewPageData.fileName}.php`);
      // 一方のセール表見てたらもう一方のセール表は除外したい
      const isDLSite = viewPageData.hasDlsite && /FANZA/i.test(campaignName);
      const isFanza = viewPageData.hasFanza && /DLSite/i.test(campaignName);
      // 今見てるセール表は除外する
      const isOriginalUrl = viewPageData.fileName.length > 0 && hasOriginalUrl;
      if (isDLSite || isFanza || isOriginalUrl) continue;
      campaignList[campaignName] = getFileName(campaignLinkElm.href);
    }
    return campaignList;
  };
  // セール表の先頭に他のセール表へのリンクと比較用ラジオボタンをセットする
  const createCampaignDiffList = async () => {
    const campaignList = await getCampaignList();
    const campaignListKeys = Object.keys(campaignList);
    if (!campaignListKeys.length) {
      console.error('キャンペーン取得失敗');
      return false;
    }
    let campaignListHTML = `<details id="extension-wrapper">
    <summary id="extension-diffNotes">
      <span>現在表示しているセール表と比較し、以下のセール表のみにあるセールを抜き出します</span>
    </summary>
    <div id="extension-diffLists">`;
    for (const key of campaignListKeys) {
      const fileName = campaignList[key];
      campaignListHTML += `<label for="${fileName}">
        <input type="radio" id="${fileName}" name="extension-diffListName" value="${fileName}">
        <p>
          ${key}(<a href="${fileName}.php">リンク</a>)
        </p>
      </label>`;
    }
    campaignListHTML += `<label for="${viewPageData.fileName}" class="is-hidden">
      <input type="radio" id="${viewPageData.fileName}" name="extension-diffListName" value="${viewPageData.fileName}">
      <p>現在の表を元に戻す</p>
    </label>`;
    campaignListHTML += '</div></details>';
    document.body.insertAdjacentHTML('afterbegin', campaignListHTML);
    return true;
  };
  const getTableData = (tableAnchorElms) => {
    const table = {};
    for (const tableAnchorElm of tableAnchorElms) {
      const url = new URL(tableAnchorElm.href);
      const param = new URLSearchParams(url.search);
      const key = param.get('game');
      const value = tableAnchorElm.closest('tr')?.outerHTML || null;
      if (key && value) {
        table[key] = value;
      }
    }
    return table;
  };
  const observeLoading = () => {
    const diffListElm = document.querySelector('#extension-diffLists');
    if (diffListElm === null) return;
    let isLoading = GLOBAL.isLoading;
    Object.defineProperty(GLOBAL, 'isLoading', {
      get: () => isLoading,
      set: (newValue) => {
        isLoading = newValue;
        if (isLoading) {
          document.body.classList.add('is-loading');
          diffListElm.classList.add('is-loading');
        } else {
          document.body.classList.remove('is-loading');
          diffListElm.classList.remove('is-loading');
        }
      },
    });
  };
  const observeTable = () => {
    const viewPageLabelElm = document.querySelector(`label[for="${viewPageData.fileName}"]`);
    if (viewPageLabelElm === null) return;
    let isChangedTable = GLOBAL.isChangedTable;
    Object.defineProperty(GLOBAL, 'isChangedTable', {
      get: () => isChangedTable,
      set: (newValue) => {
        isChangedTable = newValue;
        if (isChangedTable) {
          viewPageLabelElm.classList.remove('is-hidden');
        } else {
          viewPageLabelElm.classList.add('is-hidden');
        }
      },
    });
  };
  const restoreTooltipEvent = () => {
    const tooltipElms = document.querySelectorAll('a.tooltip');
    const hoverEventHandler = (e, isEnter) => {
      const self = e.currentTarget;
      const tooltipElm = self.querySelector('div.tooltip');
      if (tooltipElm !== null) {
        tooltipElm.style.display = isEnter ? 'flex' : 'none';
      }
    };
    toukeiTableElm.classList.add('is-changed');
    for (const tooltipElm of tooltipElms) {
      const parentTd = tooltipElm.closest('td');
      if (parentTd !== null) {
        parentTd.addEventListener('mouseover', (e) => hoverEventHandler(e, true));
        parentTd.addEventListener('mouseleave', (e) => hoverEventHandler(e, false));
      }
    }
  };
  const setDiffEvent = (originalTable) => {
    const radioElms = document.querySelectorAll('[name="extension-diffListName"]');
    const changeEventHandler = async (e) => {
      GLOBAL.isLoading = true;
      const toukeiTableElm = document.querySelector('.toukei_table tbody');
      const self = e.currentTarget;
      const isOriginalTable = self.value === viewPageData.fileName;
      const firstTrHTML =
        '<tr data-sort-method="none"><th>ゲーム名</th><th class="th_brand">ブランド名</th><th class="th_price">価格</th><th class="th_value">中央値</th><th class="th_value">標準偏差</th><th class="th_value">データ数</th></tr>';
      const createDiffTable = async () => {
        const campaignHTML = await fetchHTML(`${fullPath}${self.value}.php`);
        if (campaignHTML === false) return '';
        const targetElms = campaignHTML.querySelectorAll(selector);
        if (!targetElms.length) return '';
        const diffTable = getTableData(targetElms);
        const originalKeys = Object.keys(originalTable);
        const diffKeys = Object.keys(diffTable);
        const mergeTable = {
          ...originalTable,
          ...diffTable,
        };
        // 現在のセール表と比較し、対象のセール表のみにあるセールを抜き出す
        const diff = diffKeys.filter((key) => !originalKeys.includes(key));
        let html = '';
        for (const key of diff) {
          const trHTML = mergeTable[key];
          if (typeof trHTML !== 'undefined') {
            html += trHTML;
          }
        }
        return html;
      };
      const restoreOriginalTable = () => {
        const keys = Object.keys(originalTable);
        let html = '';
        for (const key of keys) {
          const trHTML = originalTable[key];
          html += trHTML;
        }
        return html;
      };
      if (toukeiTableElm !== null) {
        let newHTML = firstTrHTML;
        if (isOriginalTable) {
          newHTML += restoreOriginalTable();
        } else {
          newHTML += await createDiffTable();
          GLOBAL.isChangedTable = true;
        }
        toukeiTableElm.innerHTML = newHTML;
        if (typeof unsafeWindow.setTableSort === 'function') {
          unsafeWindow.setTableSort();
        }
        restoreTooltipEvent();
        const medianValue = document.querySelector('.toukei_table th.th_value[role="columnheader"]');
        // 中央値を降順にする
        if (medianValue !== null) {
          medianValue.click();
          medianValue.click();
        }
      }
      GLOBAL.isLoading = false;
    };
    if (!radioElms.length) return;
    observeLoading();
    observeTable();
    for (const radioElm of radioElms) {
      radioElm.addEventListener('change', changeEventHandler);
    }
  };
  const originalTable = getTableData(targetElms);
  const isCreateDiffList = await createCampaignDiffList();
  if (isCreateDiffList) {
    setDiffEvent(originalTable);
    setTimeout(() => {
      document.querySelector('#extension-diffNotes')?.click();
    }, 500);
  }
})();