巴哈姆特勇者福利社++

改進巴哈姆特的勇者福利社,動態載入全部商品、加入過濾隱藏功能、標示競標目前出價等。

// ==UserScript==
// @name         巴哈姆特勇者福利社++
// @namespace    https://github.com/DonkeyBear
// @version      0.8.3
// @description  改進巴哈姆特的勇者福利社,動態載入全部商品、加入過濾隱藏功能、標示競標目前出價等。
// @author       DonkeyBear
// @match        https://fuli.gamer.com.tw/shop.php*
// @icon         https://fuli.gamer.com.tw/favicon.ico
// @grant        none
// ==/UserScript==

const $ = jQuery;
const isDarkMode = $(document.documentElement).data('theme') === 'dark';

const stylesheet = /* css */`
  #BH-wrapper {
    padding-top: 0;
  }

  .items-card {
    ${isDarkMode ? '' : 'z-index: -1;'}
  }

  #tabs-btn-group {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  .tabs-btn-box {
    margin-top: 0;
  }

  .fuli-enhance-btn-box {
    overflow: visible;
  }

  .btn-distance.fuli-enhance {
    width: auto;
  }

  .btn-distance.fuli-enhance > [type=checkbox] {
    margin-right: .35rem;
  }

  .filter-by-title,
  .filter-by-type,
  .filter-by-state,
  #BH-pagebtn {
    display: none;
  }

  .digital.unaffordable {
    color: #DF4747;
  }

  .fuli-enhance.btn-search {
    padding: 0 .65rem;
  }

  .fuli-enhance.btn-filter {
    cursor: pointer;
    padding-left: 1.5rem;
    padding-right: 1.2rem;
    position: relative;
    border-right: 1px solid;
    ${isDarkMode ? 'border-color: #444444;' : ''};
  }

  .fuli-enhance.btn-filter:hover {
    ${isDarkMode ? '' : 'opacity: 1;'}
  }

  .search-bar {
    display: flex;
    align-items: center;
    border: 1px solid;
    border-radius: 4px;
    background-color: white;
    padding: .2rem .3rem;
  }

  .search-bar > [type=text] {
    width: 9rem;
    border: 0;
  }

  .search-bar > [type=text]:focus-visible {
    outline: 0;
  }

  .icon {
    width: 1rem;
    height: 1rem;
  }

  .icon-search {
    border-left: 1px solid;
    padding-left: .35rem;
    margin-left: .2rem;
  }

  .icon-close {
    transform: translate(2px, 2px);
    cursor: pointer;
  }

  .icon-filter {
    margin-left: .3rem;
  }

  .btn-filter:hover > #filter-popup {
    display: flex;
    flex-direction: column;
    align-items: baseline;
    width: max-content;
  }

  #filter-popup {
    padding: .5rem;
    position: absolute;
    transform: translateX(-50%);
    left: 50%;
    top: 100%;
    z-index: 1;
    display: none;
    cursor: default;
    overflow: visible;
    background-color: ${isDarkMode ? '#272728' : '#FBFBFB'};
    ${isDarkMode ? 'color: #C7C6CB;' : ''}
  }

  #filter-popup::before {
    content: '';
    width: 0;
    height: 0;
    border: .4rem solid transparent;
    border-bottom: .35rem solid currentColor !important;
    position: absolute;
    transform: translateX(-50%);
    left: 50%;
    bottom: 100%;
  }

  #filter-popup label {
    cursor: pointer;
  }

  #exchange-item-counter,
  #bid-item-counter,
  #lottery-item-counter {
    color: #11AAC1;
  }

  #exchange-item-counter,
  #bid-item-counter {
    margin-right: .75rem;
  }

  .digital.current-bid {
    margin-left: 8px;
  }
`;
$('<style>').text(stylesheet).appendTo('head');

// 依照 URL Param 判斷是否執行後續程式
const newUrl = new URL(window.location);
const getParam = (param) => newUrl.searchParams.get(param);
const isOnHistoryPage = getParam('history') !== null && getParam('history') !== '0';
const isOnFirstPage = getParam('page') === null || getParam('page') === '1';
if (isOnHistoryPage) { return }
if (!isOnFirstPage) { window.location.replace('https://fuli.gamer.com.tw/shop.php') }

// 持有的巴幣存款
const DEPOSIT = +$('.brave-assets').text().replaceAll(/\D/g, '');

// .items-card .type-tag 的內文
const TYPE_TAG = {
  exchange: '直購',
  bid: '競標',
  lottery: '抽抽樂'
};

const itemCounter = new Proxy({
  exchange: 0,
  bid: 0,
  lottery: 0
}, {
  set(target, prop, value) {
    target[prop] = +value;
    $(`#${prop}-item-counter`).text(target[prop]);
    return true;
  }
})

class ItemCard {
  /** @param {HTMLElement|jQuery|string} itemCard */
  constructor (itemCard) {
    this.itemCard = itemCard;
    this.$itemCard = $(itemCard);
    this.type = $(itemCard).find('.type-tag').text().trim();
  }

  fetchCurrentBid () {
    if (this.type !== TYPE_TAG.bid) { return this } // 若非競標品則結束函式
    const $priceElement = this.$itemCard.find('.price');
    $priceElement.text('正在讀取目前出價');
    fetch(this.$itemCard.attr('href'), { method: 'GET' })
      .then(res => res.text())
      .then(data => {
        const parser = new DOMParser();
        const virtualDoc = parser.parseFromString(data, 'text/html');


        let currentBid;
        $(virtualDoc).find('.pbox-content').each((idnex, item) => {
          if (!$(item).text().includes('目前出價')) { return }
          if (!Number.isNaN(+currentBid)) { return }
          currentBid = $(item).find('.pbox-content-r').text().match(/[\d|,]+/)[0];
        });
        const newTextHTML = /* html */`目前出價<p class="digital current-bid">${currentBid}</p>巴幣`;
        $priceElement.html(newTextHTML);
        this.colorPriceTag();
      })
      .catch((error) => {
        $priceElement.text('讀取出價失敗!');
        console.error(error);
      });
  }

  colorPriceTag () {
    const $priceNumberElement = this.$itemCard.find('.digital');
    const price = +$priceNumberElement.text().replaceAll(/\D/g, '');
    if (DEPOSIT < price) { $priceNumberElement.addClass('unaffordable') }
  }

  registerCard () {
    // 依照商品卡種類,增加計數和取得目前出價
    switch (this.type) {
      case TYPE_TAG.exchange:
        itemCounter.exchange++;
        break;
      case TYPE_TAG.bid:
        itemCounter.bid++;
        break;
      case TYPE_TAG.lottery:
        itemCounter.lottery++;
        break;
    }
  }
}

class ItemCardList {
  /** @param {HTMLElement|jQuery|string} [itemCardList=$('.item-list-box a.items-card')] */
  constructor (itemCardList = $('.item-list-box a.items-card')) {
    this.$itemCardList = $(itemCardList);
  }

  filterByTitle (filterTitle) {
    this.$itemCardList.each((i, itemCard) => {
      const title = $(itemCard).find('.items-title').text();
      const filterClassName = 'filter-by-title';
      title.includes(filterTitle) ? $(itemCard).removeClass(filterClassName) : $(itemCard).addClass(filterClassName);
    });
  }

  filterByType (filterType) {
    this.$itemCardList.each((i, itemCard) => {
      const type = new ItemCard(itemCard).type;
      if (!type.includes(filterType)) { return }
      const filterClassName = 'filter-by-type';
      $(itemCard).toggleClass(filterClassName);
    });
  }
}

// 放置商品類型計數區塊
$('#forum-lastBoard').after(/* html */`
  <div class="m-hidden">
    <h5>現有商品數量</h5>
    <div class="BH-rbox flex-center">
      <span>直購:</span>
      <span id="exchange-item-counter">0</span>
      <span>競標:</span>
      <span id="bid-item-counter">0</span>
      <span>抽抽樂:</span>
      <span id="lottery-item-counter">0</span>
    </div>
  </div>
`);

// 放置功能按鈕
const $firstTabsBtn = $('.tabs-btn-box');
$firstTabsBtn.wrap(/* html */`<div id="tabs-btn-group"></div>`);
const $tabsBtnGroup = $('#tabs-btn-group');
const $newTabsBtn = $('<div>', { class: 'tabs-btn-box fuli-enhance-btn-box' });
$newTabsBtn.html(/* html */`
  <a class="flex-center btn-distance fuli-enhance btn-filter">
    <span>篩選器</span>
    <svg class="icon icon-filter" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 18">
      <path d="M18.85 1.1A1.99 1.99 0 0 0 17.063 0H2.937a2 2 0 0 0-1.566 3.242L6.99 9.868 7 14a1 1 0 0 0 .4.8l4 3A1 1 0 0 0 13 17l.01-7.134 5.66-6.676a1.99 1.99 0 0 0 .18-2.09Z"/>
    </svg>
    <div id="filter-popup" class="tabs-btn-box">
      <div class="flex-center btn-distance fuli-enhance">
        <input id="hide-exchange-items" type="checkbox" data-keyword="${TYPE_TAG.exchange}">
        <label for="hide-exchange-items">隱藏直購</label>
      </div>
      <div class="flex-center btn-distance fuli-enhance">
        <input id="hide-bid-items" type="checkbox" data-keyword="${TYPE_TAG.bid}">
        <label for="hide-bid-items">隱藏競標</label>
      </div>
      <div class="flex-center btn-distance fuli-enhance">
        <input id="hide-lottery-items" type="checkbox" data-keyword="${TYPE_TAG.lottery}">
        <label for="hide-lottery-items">隱藏抽抽樂</label>
      </div>
    </div>
  </a>
  <a class="flex-center btn-distance fuli-enhance btn-search">
    <div class="search-bar">
      <input type="text">
      <svg class="icon icon-close" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
        <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.9" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
      </svg>
      <svg class="icon icon-search" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
        <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.9" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"></path>
      </svg>
    </div>
  </a>
`);
$newTabsBtn.on('change', '[type=checkbox]', function() {
  const typeTag = $(this).data('keyword');
  const itemCardList = new ItemCardList();
  itemCardList.filterByType(typeTag);
});
const iconClose = $newTabsBtn.find('.icon-close');
iconClose.on('click', () => {
  searchBar.value = '';
  const itemCardList = new ItemCardList();
  itemCardList.filterByTitle('');
});
const searchBar = $newTabsBtn.find('.search-bar > [type=text]');
searchBar.on('input', function() {
  const searchText = $(this).val();
  const itemCardList = new ItemCardList();
  itemCardList.filterByTitle(searchText);
});
$tabsBtnGroup.append($newTabsBtn);

$('a.items-card').each((i, card) => {
  // 依照商品卡種類,增加計數和取得目前出價
  const itemCard = new ItemCard($(card));
  itemCard.registerCard();
  if (itemCard.type === TYPE_TAG.bid) { itemCard.fetchCurrentBid() }
});

// 動態載入全部商品
const $itemListBox = $('.item-list-box');
const maxPage = $('.BH-pagebtnA a:last-child').text();
if (maxPage === '1') { return } // 若僅一頁則不需讀取
const observer = new MutationObserver((records) => {
  // 建立觀測器,觀測新加入的商品卡
  records.forEach((record) => {
    record.addedNodes.forEach((newNode) => {
      if (!$(newNode).hasClass('items-card')) { return }
      // 依照商品卡種類,增加計數和取得目前出價
      const itemCard = new ItemCard($(newNode));
      itemCard.registerCard();
      if (itemCard.type === TYPE_TAG.bid) { itemCard.fetchCurrentBid() }
    })
  })
});
observer.observe(document.querySelector('.item-list-box'), { childList: true });

for (let page = 2; page <= maxPage; page++) {
  fetch(`https://fuli.gamer.com.tw/shop.php?page=${page}`, { method: 'GET' })
    .then(res => res.text())
    .then(data => {
      const parser = new DOMParser();
      const virtualDoc = parser.parseFromString(data, 'text/html');

      const $items = $(virtualDoc).find('.item-list-box a.items-card');
      $items.each((i, item) => { $itemListBox.append(item) });
    });
}