Greasy Fork

时光机查询特定条目评价

经济的同步率查询

// ==UserScript==
// @name         时光机查询特定条目评价
// @namespace    https://bgm.tv/group/topic/411925
// @version      0.2.1
// @description  经济的同步率查询
// @author       mmv
// @include      /^https?:\/\/(((fast\.)?bgm\.tv)|chii\.in|bangumi\.tv)\/user\/[^/]+$/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bgm.tv
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.innerHTML = `
        div.userSynchronize.userSynchronizeSpecial {
            #subjectList {
                .tip {
                    color: #666;
                }
                li:hover small {
                    color: #EEE;
                }
                img.avatar {
                    border-radius: 5px;
                }
            }
            input[type=search]:focus {
                &:focus {
                    outline: none;
                }
            }
            select {
                color: #222;
            }
        }
        html[data-theme="dark"] div.userSynchronize.userSynchronizeSpecial {
            #subjectList {
                .tip {
                    color: #d8d8d8;
                }
                small {
                    color: #999;
                }
                li:hover small {
                    color: #EEE;
                }
            }
            select {
                color: #e0e0e1;
            }
        }
    `;
    document.body.append(style);

    const username = location.pathname.split('/').pop();
    const synchronize = document.querySelector('.userSynchronize');
    if (!synchronize) return;
    const frag = document.createDocumentFragment();

    const box = document.createElement('div');
    box.classList.add('userSynchronize', 'userSynchronizeSpecial');

    const inner = document.createElement('div');

    const title = document.createElement('h3');
    title.textContent = '特定同步率';

    const searchPanel = document.createElement('div');
    const dataPanel = document.createElement('div');
    const searchInputs = document.createElement('div');

    const input = document.createElement('input');
    input.classList.add('inputtext');
    input.enterkeyhint = 'search';
    input.type = 'search';
    input.autocomplete = 'false';
    input.addEventListener('keydown', (event) => {
        if (event.key === 'Enter') searchAndRender();
    });

    const searchResult = document.createElement('div');
    searchResult.classList.add('subjectListWrapper');
    searchResult.style = `
      max-height: 200px;
      overflow-y: scroll;
    `;
    const dataResult = document.createElement('div');

    const searchSelect = document.createElement('select');
    searchSelect.onchange = searchAndRender;

    const searchBtn = makeBtn('🔍');
    const makeSearching = () => document.createTextNode('搜索中……');
    searchBtn.onclick = searchAndRender;

    const dataBtn = makeBtn('🆔');
    dataBtn.onclick = async () => {
        const subject_id = input.value;
        if (!/\d+/.test(subject_id)) return;
        dataResult.innerHTML = '查询中……';
        const collection = await getUserCollection(subject_id);
        const name = collection.subject?.name;
        renderCollection(collection, dataResult, `/subject/${ subject_id }`, name);
    }

    frag.append(box);
    box.append(title, inner);
    inner.append(searchPanel, dataPanel);
    searchPanel.append(searchInputs, searchResult);
    searchInputs.append(searchSelect, input, searchBtn, dataBtn);
    dataPanel.append(dataResult);
    searchSelect.innerHTML = `<option value="all">全部</option>
                              <option value="2">动画</option>
                              <option value="1">书籍</option>
                              <option value="4">游戏</option>
                              <option value="3">音乐</option>
                              <option value="6">三次元</option>`;
    inner.style = `display: flex;
                   flex-wrap: wrap;`;
    searchPanel.style.flex = '0 1 300px';
    dataPanel.style.flex = '1 1 200px';
    searchInputs.style = `width: fit-content;
                          border-radius: 100px;
                          box-shadow: none;
                          border: 1px solid rgba(200, 200, 200, 0.5);
                          background-color: rgba(255, 255, 255, 0.2);`;
    searchSelect.style = `font-size: 1em;
                          padding: 4px 0 4px 5px;
                          width: fit-content;
                          border: none;
                          outline: none;
                          box-shadow: none;
                          background-color: transparent;
                          background-image: none;
                          -webkit-appearance: none;
                          -moz-appearance: none;
                          appearance: none;
                          border-radius: 0;
                          border-right: 1px solid rgba(200, 200, 200, 0.5)`;
    input.style = `font-size: 1em;
                   width: 120px;
                   -webkit-appearance: none;
                   -moz-appearance: none;
                   box-shadow: none;
                   background: transparent;
                   line-height: 20px;
                   border: none;`;

    synchronize.after(frag);

    async function searchAndRender() {
        const keyword = input.value;
        if (keyword === '') return;
        searchResult.innerHTML = '';
        const searching = makeSearching();
        searchResult.append(searching);
        const type = searchSelect.value;
        const data = await search(keyword, type);
        const list = data?.list;
        if (!list) {
            searchResult.innerText = '搜索失败';
            return;
        }
        if (list.length === 0) {
            searchResult.innerText = '未找到相关条目';
            return;
        }
        renderList(list, keyword, type, searchResult, async ({ href, textContent }) => {
            const subject_id = href.split('/').pop();
            dataResult.innerHTML = '查询中……';
            renderCollection((await getUserCollection(subject_id)), dataResult, href, textContent);
        });
        searching.remove();
    }

    function makeBtn(text) {
        const btn = document.createElement('a');
        btn.href = 'javascript:;';
        btn.innerText = text;
        btn.style = `text-wrap: nowrap;
                     border: none;
                     border-left: 1px solid rgba(200, 200, 200, 0.5);
                     padding: 4px 5px;
                     cursor: pointer;`
        return btn;
    }

    async function search(keyword, type, start=0) {
        try {
          const response = await fetch(`https://api.bgm.tv/search/subject/${encodeURI(keyword)}?type=${type}&max_results=10&start=${start}`);
          if (!response.ok) throw new Error(`API request ${ response.status } ${ response.statusText }`);
          return await response.json();
        } catch (error) {
          console.error(error);
          return null;
        }
    }

    function listHTML(list) {
        return list.reduce((m, { id, type, images, name, name_cn }) => {
            type = ['书籍', '动画', '音乐', '游戏', '', '三次元'][type - 1];
            const grid = images?.grid;
            m += `<li class="clearit">
                    <a href="/subject/${id}" class="avatar h">
                      ${grid ? `<img src="${grid}" class="avatar ll">` : ''}
                    </a>
                    <div class="inner">
                      <small class="grey rr">${type}</small>
                      <p><a href="/subject/${id}" class="avatar h">${name}</a></p>
                      <small class="tip">${name_cn}</small>
                    </div>
                  </li>`;
            return m;
        }, '');
    }

    function renderList(list, keyword, type, container, clickHandler) {
        const ul = document.createElement('ul');
        ul.id = 'subjectList';
        ul.classList.add('subjectList', 'ajaxSubjectList');
        ul.innerHTML = listHTML(list);

        const more = document.createElement('li');
        more.classList.add('clearit');
        more.textContent = '加载更多';
        more.style.cursor = 'pointer';
        more.style.textAlign = 'center';
        more.style.listStyle = 'none';
        more.start = list.length + 1;
        more.onclick = async () => {
            const searching = makeSearching();
            more.before(searching);
            const moreData = await search(keyword, type, more.start);
            const newlist = moreData.list;
            if (!newlist) {
                searching.remove();
                return;
            }
            more.start += newlist.length;
            ul.insertAdjacentHTML('beforeend', listHTML(newlist));
            applyHandler();
            searching.remove();
        }

        container.append(ul, more);
        applyHandler();

        function applyHandler() {
          ul.querySelectorAll('a').forEach(a => {
              a.addEventListener('click', e => {
                  e.preventDefault();
                  clickHandler(a);
              });
            });
        }
    }

    async function getUserCollection(subject_id) {
        try {
            const headers = {};
            if (accessToken) headers.Authorization = `Bearer ${ accessToken }`;
            const response = await fetch(`https://api.bgm.tv/v0/users/${username}/collections/${subject_id}`, { headers });
            if (response.ok) {
                return await response.json();
            } else if (response.status === 404) {
                return { not_found: true };
            } else if (response.status === 401) {
                return { auth_failed: true };
            } else {
              throw new Error(`API request ${ response.status } ${ response.statusText }`);
            }
        } catch (error) {
          console.error(error);
          return null;
        }
    }

    function renderCollection(data, container, fallbackLink='', fallbackName='本作') {
        if (!data) {
            container.innerHTML = '查询失败';
            return;
        }
        if (data.not_found || data.auth_failed) {
            let message = '';
            if (data.not_found) {
                message = `未找到${ fallbackLink ? `<a class="l" href="${fallbackLink}" target="_blank">${fallbackName}</a>` : fallbackName }的收藏记录`;
            } else if (data.auth_failed) {
                message = '个人令牌认证失败';
            }
            if (!accessToken || data.auth_failed) {
                message += '<br>试试<a class="l" href="javascript:" id="incheiat">填写</a>个人令牌后再试一遍?你可以在<a class="l" href="https://next.bgm.tv/demo/access-token/create" target="_blank">这里</a>创建个人令牌';
            }
            container.innerHTML = message;
            container.querySelector('#incheiat')?.addEventListener('click', () => {
                accessToken = prompt('请填写个人令牌 token');
                if (!accessToken) return;
                if (!accessToken.match(/^[a-zA-Z0-9]+$/)) {
                    accessToken = null;
                    alert('格式错误,请重新填写');
                    return;
                }
            });
            return;
        }

        const { rate, subject_type, type, comment, updated_at, ep_status, vol_status, subject } = data;
        const { id, name, name_cn, volumes, eps } = subject;
        const verb = ['读', '看', '听', '玩', '', '看'][subject_type - 1];
        const html = `<li id="item_${id}" class="item even clearit" style="list-style: none;">
                        <div class="inner" style="margin-left: 10px">
                        <h3>
                          ${ name_cn ? `<a href="/subject/${id}" class="l">${name_cn}</a> <small class="grey">${name}</small>`
                                     : `<a href="/subject/${id}" class="l">${name}</a>`
                          }
                        </h3>
                        <p class="collectInfo">
                          ${ rate ? `<span class="starstop-s"><span class="starlight stars${rate}"></span></span>`
                                  : ''
                          }
                          <span class="tip_j">${updated_at.slice(0, 10)}</span>
                          <span class="tip_i">/</span>
                          <span class="tip"> ${[`想${verb}`, `${verb}过`, `在${verb}`, '搁置', '抛弃'][type - 1]}</span>
                          ${ ep_status ? `
                          <span class="tip_i">/</span>
                          <span class="tip">
                          ${ ep_status }${ eps ? ` / ${eps}` : ''}话
                          </span>
                          ` : ''}
                          ${ vol_status ? `
                          <span class="tip_i">/</span>
                          <span class="tip">
                          ${ vol_status }${ eps ? ` / ${volumes}` : ''}卷
                          </span>
                          ` : ''}
                        </p>
                        ${ comment ? `
                          <div id="comment_box"><div class="item"><div class="text_main_even" style="float:none;width:unset">
                            <div class="text"> ${comment}</div>
                        <div class="text_bottom"></div>
                      </div></div></div></div>
                        ` : '' }
                    </li>`;
        container.innerHTML = html;
    }
})();