Greasy Fork

来自缓存

Greasy Fork is available in English.

加入或修改收藏时标签功能加强

加入或修改收藏时高亮或自动填充自己与他人的共同标签,高亮其中的元标签,点击展开所有标签

// ==UserScript==
// @name         加入或修改收藏时标签功能加强
// @version      0.3.0
// @description  加入或修改收藏时高亮或自动填充自己与他人的共同标签,高亮其中的元标签,点击展开所有标签
// @author       ooo
// @include      http*://bgm.tv/*
// @include      http*://chii.in/*
// @include      http*://bangumi.tv/*
// @license      MIT
// @namespace    http://greasyfork.icu/users/1337615
// ==/UserScript==

(function() {
    const user = document.querySelector('#dock a').href.split('/').pop();
    const colTypes = ['anime', 'book', 'game', 'music', 'real'];
    const colDoings = ['wish', 'collect', 'do', 'on_hold', 'dropped'];
    const colPaths = colTypes.flatMap(type => colDoings.map(doing => `/${type}/list/${user}/${doing}`));

    const exclusiveTagSets = [
        new Set(["剧场版", "TV", "OVA", "CM", "WEB", "PV"]),
        new Set(["原创", "漫画改", "游戏改", "小说改"]),
    ];
    function isMutuallyExclusive(tag1, tag2) {
        return exclusiveTagSets.some(set => {
            return set.has(tag1) && set.has(tag2);
        });
    }

    const pink = '#F09199';
    const storage = JSON.parse(localStorage.getItem('incheijs')) || {
        'anime': [],
        'book': [],
        'game': [],
        'music': [],
        'real': [],
        'autofill': false,
    };

    function updStorage(key, value) {
        storage[key] = value;
        localStorage.setItem('incheijs', JSON.stringify(storage));
        return storage[key];
    }
    function addToStorage(type, tags) {
        return updStorage(type, [...new Set(storage[type].concat(tags))]);
    }
    function getTagsFromDOM(dom) {
        const tags = [...dom.querySelectorAll("#userTagList a.l")].map(tag => tag.childNodes[1].textContent);
        return tags;
    }

    const isColPage = colPaths.includes(location.pathname);
    if (isColPage) {
        const type = location.pathname.split('/')[1];
        addToStorage(type, getTagsFromDOM(document));
    }

    // Microsoft Copilot start
    const cache = new Proxy({}, {
        get(_, property) {
            const data = sessionStorage.getItem('incheijs');
            if (data) {
                const jsonData = JSON.parse(data);
                return jsonData[property];
            }
            return undefined;
        },
        set(_, property, value) {
            const data = sessionStorage.getItem('incheijs');
            let jsonData = {};
            if (data) {
                jsonData = JSON.parse(data);
            }
            jsonData[property] = value;
            sessionStorage.setItem('incheijs', JSON.stringify(jsonData));
            return true;
        }
    });
    // end

    const currentID = location.pathname.match(/(?<=subject\/)\d+/)?.[0];
    if (currentID) {
        markTags(currentID);
    } else if (location.pathname === "/") {
        document.querySelectorAll('.progress_percent_text > a').forEach(x => x.addEventListener('click', iframeHandler))
    } else {
        document.querySelectorAll('a.thickbox').forEach(x => x.addEventListener('click', iframeHandler));
    }

    async function markTags(subjectID, dom = document) {
        const tagInput = dom.querySelector('#tags');
        const tagLists = dom.querySelectorAll('.tagList');
        const myList = tagLists[tagLists.length - 1];
        const myListLabel = dom.querySelectorAll('.tip_j.ll')[1];

        const [ subjectType, subjectTags, metaTags, tagCounts ] = (cache[subjectID] ??= await getSubject(subjectID));
        const myTags = [...myList.querySelectorAll('a')].map(tag => tag.textContent);
        let storedTags = addToStorage(subjectType, myTags);
        let commonTags = subjectTags.filter(tag => storedTags.includes(tag));

        function renderLists() {
            if (tagLists.length > 1) {
                const othersList = tagLists[0];
                renderList(othersList, commonTags, metaTags, subjectTags, tagInput);
            }
            renderList(myList, commonTags, metaTags, storedTags, tagInput);
        }
        renderLists();

        if (storage.autofill) fillInput(tagInput, commonTags, metaTags, tagCounts);

        const label = document.createElement('label');
        label.for = 'autofill';
        label.innerText = '自动填充';
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.id = 'autofill';
        checkbox.checked = storage.autofill;
        checkbox.onclick = () => {
            updStorage('autofill', !storage.autofill);
            if (storage.autofill) fillInput(tagInput, commonTags, metaTags, tagCounts);
        };
        label.prepend(checkbox);
        tagInput.after(document.createElement('br'), label);

        const syncBtn = document.createElement('button');
        syncBtn.textContent = '📡';
        syncBtn.style = 'cursor: pointer; float: right; padding: 0; border: none; background: none; font-family: Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;';
        syncBtn.addEventListener('click', async function listener(e) {
            e.preventDefault();
            syncBtn.textContent = '⏳';
            syncBtn.style.cursor = 'wait';
            try {
                storedTags = await syncMyTags(subjectType);
                commonTags = subjectTags.filter(tag => storedTags.includes(tag));
                renderLists();
                syncBtn.textContent = '✔️';
                syncBtn.removeEventListener('click', listener);
                syncBtn.style.cursor = 'default';
            } catch {
                syncBtn.textContent = '❌';
                syncBtn.style.cursor = 'pointer';
                setTimeout(() => syncBtn.textContent = '📡', 3000);
            }
        });
        myListLabel.append(document.createElement('br'), syncBtn);
    }

    function fillInput(tagInput, tags, metaTags, tagCounts) {
        const allTags = new Set([...tagInput.value.split(/\s+/), ...tags]);
        if (!metaTags || !tagCounts) return tagInput.value = [...allTags].join(' ');

        const selectedTags = [];
        const usedTags = new Set();

        for (const tag of allTags) {
            if (usedTags.has(tag)) continue;

            const conflictTags = [...allTags].filter(
                otherTag => !usedTags.has(otherTag) && tag!== otherTag && isMutuallyExclusive(tag, otherTag)
            );

            if (conflictTags.length === 0) {
                selectedTags.push(tag);
                usedTags.add(tag);
            } else {
                const allConflictTags = [tag, ...conflictTags];
                const metaConflictTags = allConflictTags.filter(t => metaTags.includes(t));
                const selected = metaConflictTags.length > 0
                   ? metaConflictTags[0]
                    : allConflictTags.reduce((prev, current) =>
                        (tagCounts[current] || 0) > (tagCounts[prev] || 0)? current : prev
                    );

                selectedTags.push(selected);
                allConflictTags.forEach(t => usedTags.add(t));
            }
        }

        tagInput.value = selectedTags.join(' ');
    }

    async function getSubject(ID) {
        let result = null;
        if (currentID) {
            result = getSubjectFromDOM(document);
        } else {
            result = await getSubjectByAPI(ID);
            result ??= await getSubjectByHTML(ID); // 受限条目无法用 API 获取
        }
        return result;
    }

    async function getSubjectByAPI(ID) {
        try {
            const response = await fetch(`https://api.bgm.tv/v0/subjects/${ID}`);
            if (!response.ok) throw new Error('API request failed');
            let { type, tags, meta_tags: metaTags } = await response.json();
            metaTags = [...new Set(metaTags)];
            type = ['book', 'anime', 'music', 'game', , 'real'][type - 1];
            tags = tags.map(tag => tag.name);
            const tagCounts = tags.reduce((acc, { name, count }) => {
                acc[name] = count;
                return acc;
            }, {});
            return [ type, tags, metaTags, tagCounts ];
        } catch (error) {
            console.error('标签功能增强: Error fetching subject info from API: ', error);
        }
    }

    async function getSubjectByHTML(ID) {
        try {
            const dom = await getDOM(`/subject/${ID}`)
            return getSubjectFromDOM(dom);
        } catch (error) {
            console.error('标签功能增强: Error fetching and parsing page:', error);
        }
    }

    function getSubjectFromDOM(dom) {
        const type = dom.querySelector('.focus').href.split('/').pop();
        const toText = elem => elem.textContent.split(' ')[0];
        const tagElems = [...dom.querySelectorAll('.subject_tag_section a')].filter(elem => elem.id !== 'show_user_tags');
        const tags = tagElems.map(toText);
        const metaTags = tagElems.filter(elem => elem.classList.contains('meta')).map(toText);
        const tagCounts = tagElems.reduce((acc, elem) => {
            acc[toText(elem)] = elem.textContent.split(' ')[1];
            return acc;
        }, {});

        return [ type, tags, metaTags, tagCounts ];
    }

    function renderList(list, commonTags, metaTags, fullTags, tagInput) {
        const tagElems = [...list.querySelectorAll('a')].map(elem => {
            if (fullTags.includes(elem.textContent)) return elem;
            elem.remove();
        }).filter(elem => elem);
        const addedTags = new Set();
        const elemMap = tagElems.reduce((map, elem) => {
            map[elem.textContent] = elem;
            return map;
        }, {});
        list.style.maxHeight = 'unset';
        const frag = document.createDocumentFragment();

        commonTags.forEach(tag => {
            const tagElem = elemMap[tag];
            if (tagElem) {
                tagElem.style.color = pink;
            } else {
                const toInsert = newTag(tag, tagInput);
                toInsert.style.color = pink;
                frag.append(toInsert);
                addedTags.add(tag);
            }
        });

        metaTags.forEach(tag => {
            const tagElem = elemMap[tag];
            if (tagElem) tagElem.style.border = `1px solid ${pink}`;
        });

        if (tagElems.length + addedTags.size !== fullTags.length) {
            const rest = fullTags.filter(tag => !elemMap[tag] && !addedTags.has(tag)).map(text => {
                const elem = newTag(text, tagInput);
                if (metaTags.includes(text)) elem.style.border = `1px solid ${pink}`;
                return elem;
            });
            const more = newTagBase(' ... ', () => more.replaceWith(...rest));
            frag.append(more);
        }

        list.querySelector('.inner').append(frag);
    }

    function newTagBase(text, onclick) {
        const tag = document.createElement('a');
        tag.classList.add('btnGray');
        tag.href = '#;';
        tag.textContent = text;
        tag.onclick = onclick;
        return tag;
    }

    function newTag(text, tagInput) {
        return newTagBase(text, () => fillInput(tagInput, [text]));
    }

    async function iframeHandler() {
        const data = await getIframeData();
        markTags(...data);
    }

    function getIframeData() {
        return new Promise(resolve => {
            new MutationObserver((mutations, observer) => {
                if (!mutations.some(({ removedNodes }) => [...removedNodes].some(node => node.id === 'TB_load'))) return;
                const iframe = document.querySelector('#TB_iframeContent');
                const subjectID = new URL(iframe.src).pathname.split('/')[2];
                const iframeDOM = iframe.contentDocument;
                resolve([ subjectID, iframeDOM ]);
                observer.disconnect();
            }).observe(document.body, { 'childList': true });
        });
    };

    async function syncMyTags(type) {
        try {
            const paths = colPaths.filter(path => path.startsWith(`/${type}`));
            const doms = await Promise.all(paths.map(getDOM));
            const allTags = doms.flatMap(getTagsFromDOM);
            const uniqueTags = [...new Set(allTags)];
            return updStorage(type, uniqueTags);
        } catch (error) {
            console.error('Error syncing tags:', error);
            throw error;
        }
    }

    async function getDOM(url) {
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error('HTTP request failed');
            const html = await response.text();
            const dom = new DOMParser().parseFromString(html, 'text/html');

            return dom;
        } catch (error) {
            console.error('标签功能增强: Error fetching and parsing page:', error);
        }
    }

})();