Greasy Fork is available in English.

AVG 163 Butler

The ultimate tool to take over the AVG 163 commnunity

// ==UserScript==
// @name         AVG 163 Butler
// @namespace    http://tampermonkey.net/
// @version      0.57
// @description  The ultimate tool to take over the AVG 163 commnunity
// @author       AVG 163 Butler
// @match        https://avg.163.com/*
// @grant        GM_setClipboard
// @grant        GM_info
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    const E = encodeURIComponent;

    const VER = E(`${GM_info.script.name} - ${GM_info.script.version}`);

    const D = window.document;

    const L = window.location;

    const LS = window.localStorage;

    const communityTopicListQuery = '.avg-list.forums-list > ul';
    const searchTopicListQuery = '.search-result-container';

    const configMigrationOnUpgrade = () => {
        try {
            const supportedFields = ['已配置', '动态类型', '回复人', '隐藏作者', '主题排序', '展开工具栏', '启用话题页面发言报告自动复制'];
            const versionParts = GM_info.script.version.split('.');
            const minorVersionDigit = parseInt(versionParts[versionParts.length - 1], 10);
            const getVerStr = ver => E(`${GM_info.script.name} - ${ver}`);
            const getFieldKeyWithVer = (field, ver) => `${getVerStr(ver)}-${E(field)}`;
            const getFieldWithVersion = (field, ver) => LS.getItem(getFieldKeyWithVer(field, ver));
            const setFieldWithVersion = (field, value, ver) => LS.setItem(getFieldKeyWithVer(field, ver), value);
            const removeFieldWithVersion = (field, ver) => LS.removeItem(getFieldKeyWithVer(field, ver));
            // support all minor versions
            if (supportedFields.every(f => typeof getFieldWithVersion(f, GM_info.script.version) !== 'string')) {
                console.info('未找到当前版本配置 - 尝试从以前的版本升级');
                for (let i = minorVersionDigit - 1; i >= 0; i--) {
                    const previousVersionParts = versionParts.slice(0);
                    previousVersionParts[versionParts.length - 1] = String(i);
                    const previousVer = previousVersionParts.join('.');
                    if (supportedFields.some(f => typeof getFieldWithVersion(f, previousVer) === 'string')) {
                        for (const f of supportedFields) {
                            if (typeof getFieldWithVersion(f, previousVer) === 'string') {
                                setFieldWithVersion(f, getFieldWithVersion(f, previousVer), GM_info.script.version);
                                removeFieldWithVersion(f, previousVer);
                            }
                        }
                        console.info(`升级配置完成 - ${previousVer} => ${GM_info.script.version}`);
                        break;
                    }

                }
            }
            setFieldWithVersion('已配置', JSON.stringify(true), GM_info.script.version);
        } catch (err) {
            createToastWithText(`升级配置时发生错误`);
            console.error('升级配置时发生错误 - ', err);
        }
    };

    configMigrationOnUpgrade();

    const {
        addStorageKeyListener,
        removeStorageKeyListener
    } = (() => {
        // This is used to sync changes from other pages across the same domain
        // NOT on the same page!!!
        const keyListeners = [];
        window.addEventListener('storage', (event) => {
            const {
                key,
                newValue
            } = event;
            for (const keyListener of keyListeners) {
                if (key === keyListener.key) {
                    keyListener.callback(newValue);
                }
            }
        });
        return {
            addStorageKeyListener: (key, callback) => {
                keyListeners.push({
                    key,
                    callback
                });
            },
            removeStorageKeyListener: (key, callback) => {
                const index = keyListeners.findIndex(
                    (keyListener) => keyListener.key === key &&
                    keyListener.callback === callback
                );
                if (index >= 0) {
                    keyListeners.splice(index, 1);
                    return true;
                } else {
                    return false;
                }
            }
        };
    })();

    const getBinaryIntByStorageKey = (key) => {
        const binaryStatusValue = parseInt(LS.getItem(key), 10);
        if (isNaN(binaryStatusValue)) {
            return 1;
        }
        // should only be 1(enabled) or 0(disabled)
        if (binaryStatusValue !== 0) {
            return 1;
        }
        return 0;
    };

    const createDebouncerByTimeout = (
        debounceBeginCallback = () => {},
        debounceEndCallback = () => {},
        timeoutInMs = 1000
    ) => {
        let timeoutId = null;
        const setTimeoutWithCallback = () => setTimeout(
            () => {
                timeoutId = null;
                debounceEndCallback();
            },
            timeoutInMs
        );
        const debouncer = () => {
            if (timeoutId === null) {
                debounceBeginCallback();
            } else {
                clearTimeout(timeoutId);
            }
            timeoutId = setTimeoutWithCallback();
        };
        return debouncer;
    };

    const waitForElement = (query, callback) => {
        if (D.querySelector(query) instanceof HTMLElement) {
            callback();
        } else {
            setTimeout(
                () => waitForElement(query, callback),
                1000
            );
        }
    };

    const createOrderMemorizeStore = () => {
        // a weak map that keep the original index of the elements
        let store = new WeakMap();
        const memorize = (arr) => {
            // This array might be partially sorted
            // so we only memorize those are new
            for (let i = 0; i < arr.length; i++) {
                if (!store.has(arr[i])) {
                    store.set(arr[i], i);
                }
            }
        };
        const getOriginalIndexByElement = (element) => store.has(element) ? store.get(element) : -1;
        const clear = () => {
            store = new WeakMap();
        };

        return {
            memorize,
            getOriginalIndexByElement,
            clear
        };
    };

    const restoreHTMLElementsOrderInParentByQueryAndOriginalIndexGetter = (parentQuery, getOriginalIndexByElement) => {
        const parent = D.querySelector(parentQuery);
        if (parent instanceof HTMLElement) {
            let sortedChildren = [...parent.children];
            sortedChildren.sort(
                (l, r) => getOriginalIndexByElement(l) - getOriginalIndexByElement(r)
            );
            parent.append(...sortedChildren);
            return true;
        }
        return false;
    };

    const sortHTMLElementsInParentByQueryAndCompare = (parentQuery, compareOrCompares, memorize) => {
        const parent = D.querySelector(parentQuery);
        if (parent instanceof HTMLElement) {
            let sortedChildren = [...parent.children];
            if (typeof memorize === 'function') {
                // used for order restoration
                memorize(sortedChildren);
            }
            if (typeof compareOrCompares === 'function') {
                sortedChildren.sort(compareOrCompares);
            } else if (
                Array.isArray(compareOrCompares) &&
                compareOrCompares.every(c => typeof c === 'function')
            ) {
                compareOrCompares.forEach(
                    (compare) => {
                        sortedChildren.sort(compare);
                    }
                );
            } else {
                return false;
            }
            parent.append(...sortedChildren);
            return true;
        }
        return false;
    };

    // eventTypes must be event does bubble, focus event does not bubble
    const onElementEventTypesByClassList = (eventTypes = ['mouseover'], classList, callback, config = {
        capture: false,
        once: false,
        passive: true,
        subtree: true
    }) => {
        const isElementWithClassList = (element) => (element instanceof HTMLElement) && classList.every(c => element.classList.contains(c));
        const callbackWithListenerRemoval = (event, element) => {
            callback(event, element);
            if (config.once) {
                for (const eventType of eventTypes) {
                    window.document.removeEventListener(eventType, wrappedCallback);
                }
            }
        };
        const wrappedCallback = (event) => {
            if (isElementWithClassList(event.target)) {
                callbackWithListenerRemoval(event, event.target);
            } else if (config.subtree) {
                let currentElement = event.target.parentElement;
                while (currentElement instanceof HTMLElement) {
                    if (isElementWithClassList(currentElement)) {
                        callbackWithListenerRemoval(event, currentElement);
                        break;
                    } else {
                        currentElement = currentElement.parentElement;
                    }
                }
            }
        };
        for (const eventType of eventTypes) {
            window.document.addEventListener(
                eventType,
                wrappedCallback,
                Object.assign({},
                    config, {
                        // first event may not be triggered by target
                        once: false
                    }
                )
            );
        }
    };

    const isUserLoggedIn = () => {
        const userNavButton = D.querySelector('.nav-tool-button.nav-user-button');
        if (userNavButton instanceof HTMLElement) {
            return true;
        }
        return false;
    };

    const getCurrentPageLink = () => {
        return window.location.href;
    };

    const getMyInfo = async () => {
        if (isUserLoggedIn()) {
            const infoEndpoint = new URL(L.href);
            infoEndpoint.search = '';
            infoEndpoint.pathname = '/avg-portal-api/user/info';
            if (!!getCsrfToken()) {
                const params = {
                    csrf_token: getCsrfToken()
                };
                Object.keys(params).forEach(key => infoEndpoint.searchParams.append(key, params[key]));
            }
            const infoResult = await fetch(infoEndpoint).then(r => r.json());
            const {
                state: {
                    code: infoCode,
                    message: infoMessage
                } = {
                    code: 0,
                    message: '未知错误'
                },
                data = {
                    id: -1
                }
            } = infoResult;
            if (infoCode !== 200000) {
                createToastWithText(`当前用户信息加载错误 - ${infoMessage}`);
            } else {
                return data;
            }
        }
        createToastWithText('当前用户信息加载失败 - 用户未登陆');
        return {
            id: -1
        };
    };

    const getComments = () => {
        return [...D.querySelectorAll('.topic-comment-list .topic-comment-item')];
    };

    const getOnPageCommentsCountByAuthorId = (authorId) => {
        return getComments().filter(
            (comment) => {
                if (comment.querySelector(`[href="/user/${authorId}"]`) instanceof HTMLElement) {
                    // const topicTime = comment.querySelector('.topic-time');
                    // if (topicTime instanceof HTMLElement && topicTime.innerText === getCurrentDateInCommentFormat()) return true;
                    return true;
                }
                return false;
            }
        ).length;
    };

    const getCommentsCountFromDataByAuthorId = (comments, myAuthorId) => {
        let count = 0;
        if (Array.isArray(comments)) {
            for (const comment of comments) {
                const {
                    authorId,
                    children = []
                } = comment;
                if (authorId === myAuthorId) {
                    count++;
                }
                count += getCommentsCountFromDataByAuthorId(children, myAuthorId);
            }
        }
        return count;
    };

    const getMyAuthorId = async () => {
        if (isUserLoggedIn()) {
            const {
                id = -1
            } = await getMyInfo();
            return id;
        }
        return -1;
    };

    const getCsrfToken = () => {
        const CSRF_TOKEN_KEY = 'TOKEN';
        const cookies = D.cookie.split(';').map(p => String(p).trim()).map(kv => kv.split('=')).reduce(
            (cookiesMap, [k, v]) => {
                cookiesMap[k] = v;
                return cookiesMap;
            }, {}
        );
        return cookies[CSRF_TOKEN_KEY];
    };

    const createMyUserNameAndAuthorIdStore = () => {
        let currnetUserName = '';
        let currentAuthorId = -1;
        const updateStore = async () => {
            const {
                userName = '', id = -1
            } = await getMyInfo();
            currnetUserName = userName;
            currentAuthorId = id;
        };
        const getMyUserNameAndAuthorId = () =>
            ({
                userName: currnetUserName,
                authorId: currentAuthorId
            });
        waitForElement(
            '.nav-bar',
            updateStore
        );
        return {
            updateStore,
            getMyUserNameAndAuthorId
        };
    };

    const createElementWithInnerTextAndStyle = (ElementName, innerText, style = {}) => {
        const element = D.createElement(ElementName);
        if (typeof innerText === 'string') {
            element.innerText = innerText;
        }
        Object.assign(
            element.style,
            style
        );
        return element;
    };

    const createToastWithText = (() => {
        const timeoutIdToElementMap = new Map();
        const showToast = (text, disappearDelayInMs = 1000) => {
            const defaultStyle = {
                position: 'fixed',
                top: '10vh',
                zIndex: 9999,
                transform: 'translateX(-50%)',
                left: '50%',
                transition: 'all 0.25s ease',
                borderRadius: '2em',
                backgroundColor: 'black',
                padding: '0.5em 2em',
                color: 'white',
                textAlign: 'center',
                opacity: '0'
            };
            const toast = createElementWithInnerTextAndStyle('div', text, defaultStyle);
            D.body.append(toast);
            const timeoutId = setTimeout(
                () => {
                    timeoutIdToElementMap.delete(timeoutId);
                    D.body.removeChild(toast);
                },
                disappearDelayInMs
            );
            timeoutIdToElementMap.set(timeoutId, toast);
            setTimeout(
                () => {
                    Object.assign(
                        toast.style, {
                            opacity: '100%'
                        }
                    );
                },
                0
            );
            setTimeout(
                () => {
                    Object.assign(
                        toast.style, {
                            opacity: '0'
                        }
                    );
                },
                disappearDelayInMs - 250
            );
        };
        return showToast;
    })();

    const createSingletonObserverByQueryAndClassList = (
        ancestorQuery,
        classList,
        observeStartCallback = () => {},
        nodeAddedCallback = () => {},
        nodeRemovedCallback = () => {},
        config = {
            attributes: false,
            childList: true,
            subtree: true
        }
    ) => {
        let singletonObserver = null;
        let currentTargetNode = null;

        const addObserver = () => {
            const targetNode = D.querySelector(ancestorQuery);
            if (targetNode === currentTargetNode) {
                return;
            }
            currentTargetNode = targetNode;
            observeStartCallback();
            if (singletonObserver instanceof MutationObserver) {
                singletonObserver.disconnect();
            }

            // Callback function to execute when mutations are observed
            const callback = function (mutationsList, observer) {
                // Use traditional 'for loops' for IE 11
                for (const mutation of mutationsList) {
                    if (mutation.type === 'childList') {
                        for (const n of mutation.addedNodes) {
                            if (
                                n instanceof HTMLElement &&
                                classList.some(c => n.classList.contains(c))
                            ) {
                                nodeAddedCallback(n);
                                break;
                            }
                        }
                        for (const n of mutation.removedNodes) {
                            if (
                                n instanceof HTMLElement &&
                                classList.some(c => n.classList.contains(c))
                            ) {
                                nodeRemovedCallback(n);
                                break;
                            }
                        }
                    }
                }
            };

            // Create an observer instance linked to the callback function
            const observer = new MutationObserver(callback);

            // Start observing the target node for configured mutations
            observer.observe(targetNode, config);
            singletonObserver = observer;
            // Return artifacts
            return {
                disconnect: () => singletonObserver.disconnect(),
                reconnect: () => observer.observe(targetNode, config),
                getObserver: () => singletonObserver
            };
        };
        return addObserver;
    };

    const copyTextToClipboard = (text) => {
        try {
            const type = 'text/plain';
            navigator.clipboard.write([
                new ClipboardItem({
                    [type]: new Blob([text], {
                        type
                    })
                }),
            ]).catch(
                (err) => {
                    GM_setClipboard(text, 'text/plain');
                }
            );
        } catch (err) {
            createToastWithText(`复制到剪贴板时出错 - 无法复制内容`);
            console.error('复制到剪贴板时出错 - 无法复制内容', err);
            return false;
        }
        return true;
    };

    const copyFormattedReportForCurrentPageByUserNameAndAuthorId = (userName, authorId, foreseeSubmit = false) => {
        // 动态链接 - 该动态回复数量 - 回复人不同的异次元昵称 - 动态类型 - 回复人
        const topicType = LS.getItem(`${VER}-${E('动态类型')}`) || '';
        const owner = LS.getItem(`${VER}-${E('回复人')}`) || '';
        let currentOnPageCommentsCountByAuthorId = getOnPageCommentsCountByAuthorId(authorId);
        if (foreseeSubmit) {
            currentOnPageCommentsCountByAuthorId += 1;
        }
        let formatted = `${getCurrentPageLink()}\t${currentOnPageCommentsCountByAuthorId}\t${userName}`;
        if (!!topicType) {
            formatted = `${formatted}\t${topicType}`;
        }
        if (!!owner) {
            formatted = `${formatted}\t${owner}`;
        }
        return copyTextToClipboard(formatted);
    };

    const getAllHiddenAuthor = () => {
        let authors = [];
        try {
            authors = JSON.parse(LS.getItem(`${VER}-${E('隐藏作者')}`) || '[]').filter(
                author => (typeof author.id === 'number' || typeof author.name === 'string')
            );
        } catch (err) {
            createToastWithText('配置读取错误 - 无法取得已经隐藏的作者');
            console.error('配置读取错误 - 无法取得已经隐藏的作者', err);
        }
        return authors;
    };

    const getHiddenAuthorById = (authorId) => getAllHiddenAuthor().find(a => a.id === authorId);

    const getAllHiddenAuthorId = () =>
        getAllHiddenAuthor().map(author => author.id);

    const isAuthorIdHidden = (authorId) =>
        getAllHiddenAuthorId().includes(authorId);

    const addHiddenAuthorByIdAndName = (authorId, authorName = '无名氏') => {
        const authorIds = getAllHiddenAuthorId();
        const authors = getAllHiddenAuthor();
        if (!authorIds.includes(authorId)) {
            authors.push({
                id: authorId,
                name: authorName
            });
            LS.setItem(`${VER}-${E('隐藏作者')}`, JSON.stringify(authors));
        }
    };

    const removeHiddenAuthorById = (authorId) => {
        const authorIds = getAllHiddenAuthorId();
        const authors = getAllHiddenAuthor();
        if (authorIds.includes(authorId)) {
            authors.splice(authorIds.indexOf(authorId), 1);
            LS.setItem(`${VER}-${E('隐藏作者')}`, JSON.stringify(authors));
        }
    };

    const clearHiddenAuthor = () => LS.removeItem(`${VER}-${E('隐藏作者')}`);

    const sortableFields = [
        // { field: 'authorName' },
        // { field: 'authorId' },
        { name: 'VIP 用户', field: 'vip' },
        { name: '点击数', field: 'clickCount' },
        { name: '评论数', field: 'commentCount' },
        { name: '收藏数', field: 'favoriteCount' },
        { name: '图片数', field: 'imageCount' },
        { name: '音频数', field: 'audioCount' },
        { name: '视频数', field: 'videoCount' },
        { name: '字数', field: 'charCount' },
        { name: '我的回复数', field: 'myCommentsCount' },
        { name: '发布时间', field: 'createTime' }
    ];

    const sortableOrders = [
        { name: '升序', order: 'ASC' },
        { name: '降序', order: 'DESC' }
    ];

    const getAllSortConfigs = () => {
        const sortConfigs = [];
        try {
            const parsedSortConfigs = JSON.parse(LS.getItem(`${VER}-${E('主题排序')}`) || '[]');
            for (const parsedSortConfig of parsedSortConfigs) {
                const {
                    field,
                    order
                } = parsedSortConfig;
                if (
                    sortableFields.some(f => f.field === field) &&
                    sortableOrders.some(o => o.order === order)) {
                    sortConfigs.push({
                        field,
                        order
                    });
                }
            }
        } catch (err) {
            createToastWithText('配置读取错误 - 无法取得主题排序设置');
            console.error('配置读取错误 - 无法取得主题排序设置', err);
        }
        return sortConfigs;
    };

    const saveAllSortConfigs = (sortConfigs) => {
        const uniqueSortConfigs = [];
        for (const sortConfig of sortConfigs) {
            if (uniqueSortConfigs.findIndex(c => c.field === sortConfig.field) === -1) {
                uniqueSortConfigs.push(sortConfig);
            }
        }
        LS.setItem(`${VER}-${E('主题排序')}`, JSON.stringify(uniqueSortConfigs))
    };

    const getSortConfigByField = (field) => getAllSortConfigs().find(c => c.field === field);

    const deleteSortConfigByField = (field) => {
        const sortConfigs = getAllSortConfigs();
        const index = sortConfigs.findIndex(c => c.field === field);
        if (index >= 0) {
            const deletedSortConfig = sortConfigs.splice(
                index,
                1
            );
            saveAllSortConfigs(sortConfigs);
            return deletedSortConfig;
        }
    };

    const addSortConfig = (sortConfig, index = 0) => {
        const sortConfigs = getAllSortConfigs();
        if (index < 0) {
            sortConfigs.push(sortConfig);
        } else {
            sortConfigs.splice(index, 0, sortConfig);
        }
        saveAllSortConfigs(sortConfigs);
    };

    // createMyUserNameAndAuthorIdStore to avoid reading userName and authorId async
    const myUserNameAndAuthorIdStore = createMyUserNameAndAuthorIdStore();

    const addExpandAndShrinkButtonToToolBelt = (toolBelt) => {
        const isToolBeltExpanded = () => getBinaryIntByStorageKey(`${VER}-${E('展开工具栏')}`) === 1;

        const expandToolBeltButton = createElementWithInnerTextAndStyle(
            'button',
            '⇲', {
                position: 'relative',
                padding: '5px 10px',
                borderRadius: '5px',
                display: !isToolBeltExpanded() ? 'initial' : 'none'
            }
        );

        const expandToolBelt = () => {
            [...toolBelt.children].forEach(
                (element) => {
                    if (
                        element instanceof HTMLElement &&
                        element !== expandToolBeltButton
                    ) {
                        element.style.display = 'initial';
                    }
                    expandToolBeltButton.style.display = 'none';
                    LS.setItem(`${VER}-${E('展开工具栏')}`, '1');
                }
            );
        };

        expandToolBeltButton.addEventListener(
            'click',
            expandToolBelt
        );

        toolBelt.append(expandToolBeltButton);

        const shrinkToolBeltButton = createElementWithInnerTextAndStyle(
            'button',
            '⇱', {
                position: 'absolute',
                right: '5px',
                bottom: '5px',
                padding: '5px 10px',
                borderRadius: '5px',
                display: isToolBeltExpanded() ? 'initial' : 'none'
            }
        );

        const shrinkToolBelt = () => {
            [...toolBelt.children].forEach(
                (element) => {
                    if (
                        element instanceof HTMLElement &&
                        element !== expandToolBeltButton
                    ) {
                        element.style.display = 'none';
                    }
                    expandToolBeltButton.style.display = 'initial';
                    LS.setItem(`${VER}-${E('展开工具栏')}`, '0');
                }
            );
        };

        shrinkToolBeltButton.addEventListener(
            'click',
            shrinkToolBelt
        );

        if (!isToolBeltExpanded()) {
            shrinkToolBelt();
        } else {
            expandToolBelt();
        }

        addStorageKeyListener(
            `${VER}-${E('展开工具栏')}`,
            () => {
                if (isToolBeltExpanded()) {
                    expandToolBelt();
                } else {
                    shrinkToolBelt();
                }
            }
        );

        toolBelt.append(shrinkToolBeltButton);
    };

    const createTopicToolBelt = () => {
        const toolBelt = createElementWithInnerTextAndStyle(
            'div',
            null, {
                position: 'fixed',
                left: '20px',
                bottom: '40px',
                padding: '5px 10px',
                backgroundColor: 'rgba(0, 0, 0, 0.5)',
                borderRadius: '5px'
            }
        );

        const copyFormattedReport = createElementWithInnerTextAndStyle(
            'button',
            '复制报告到剪贴板', {
                padding: '0.5em',
                width: '12em'
            }
        );

        const copyFormattedReportHandler = (event) => {
            if (
                copyFormattedReport.disabled === false
            ) {
                copyFormattedReport.disabled = true;
                const originalText = copyFormattedReport.innerText;
                const restore = () => {
                    copyFormattedReport.innerText = originalText;
                    copyFormattedReport.disabled = false;
                };
                const {
                    userName,
                    authorId
                } = myUserNameAndAuthorIdStore.getMyUserNameAndAuthorId();
                if (copyFormattedReportForCurrentPageByUserNameAndAuthorId(userName, authorId)) {
                    copyFormattedReport.innerText = '复制成功✓';
                    createToastWithText('报告复制成功✓');
                    setTimeout(restore, 2000);
                } else {
                    restore();
                }
            }
        };
        copyFormattedReport.addEventListener(
            'click',
            copyFormattedReportHandler
        );

        const addSingletonCommentsObserver = createSingletonObserverByQueryAndClassList(
            '.topic-comment-list',
            ['topic-comment-item', 'topic-comment-reply-containter', 'topic-comment-reply-item'],
            () => {
                myUserNameAndAuthorIdStore.updateStore();
                // show toolBelt after comment input active
                D.body.append(toolBelt);
            }
        );

        onElementEventTypesByClassList(
            ['mouseover', 'focusin'],
            ['text-input'],
            () => waitForElement('.topic-comment-list', addSingletonCommentsObserver), {
                once: true
            }
        );

        toolBelt.append(copyFormattedReport);

        const topicTypeInput = createElementWithInnerTextAndStyle(
            'input',
            null, {
                width: '8em'
            }
        );

        topicTypeInput.placeholder = '动态类型';
        topicTypeInput.value = LS.getItem(`${VER}-${E('动态类型')}`) || '';
        topicTypeInput.addEventListener(
            'keyup',
            (event) => {
                LS.setItem(`${VER}-${E('动态类型')}`, event.target.value);
            }
        );
        addStorageKeyListener(
            `${VER}-${E('动态类型')}`,
            (newValue) => {
                topicTypeInput.value = newValue;
            }
        );

        toolBelt.append(topicTypeInput);

        const ownerInput = createElementWithInnerTextAndStyle(
            'input',
            null, {
                width: '5em'
            }
        );

        ownerInput.placeholder = '回复人';
        ownerInput.value = LS.getItem(`${VER}-${E('回复人')}`) || '';
        ownerInput.addEventListener(
            'keyup',
            (event) => {
                LS.setItem(`${VER}-${E('回复人')}`, event.target.value);
            }
        );
        addStorageKeyListener(
            `${VER}-${E('回复人')}`,
            (newValue) => {
                ownerInput.value = newValue;
            }
        );

        toolBelt.append(ownerInput);

        const enableCommentReportAutoCopy = createElementWithInnerTextAndStyle(
            'input',
            null, {
                marginLeft: '0.5em',
                width: '1.8em',
                height: '1.8em',
                verticalAlign: 'middle'
            }
        );

        const isCommentReportAutoCopyEnabled = () => getBinaryIntByStorageKey(`${VER}-${E('启用话题页面发言报告自动复制')}`) === 1;

        if (isCommentReportAutoCopyEnabled()) {
            createToastWithText('自动报告复制已启用');
        }

        enableCommentReportAutoCopy.id = 'enable-comment-report-auto-copy';

        enableCommentReportAutoCopy.type = 'checkbox';

        enableCommentReportAutoCopy.checked = isCommentReportAutoCopyEnabled();

        enableCommentReportAutoCopy.addEventListener(
            'change',
            (event) => {
                if (!!event.target.checked) {
                    LS.setItem(`${VER}-${E('启用话题页面发言报告自动复制')}`, '1');
                    createToastWithText('自动报告复制已启用');
                } else {
                    LS.setItem(`${VER}-${E('启用话题页面发言报告自动复制')}`, '0');
                }
            }
        );

        addStorageKeyListener(
            `${VER}-${E('启用话题页面发言报告自动复制')}`,
            () => {
                enableCommentReportAutoCopy.checked = isCommentReportAutoCopyEnabled();
            }
        );

        toolBelt.append(enableCommentReportAutoCopy);

        const enableCommentReportAutoCopyLabel = createElementWithInnerTextAndStyle(
            'label',
            '自动复制报告', {
                padding: '0.5em',
                width: '12em',
                color: 'white'
            }
        );

        enableCommentReportAutoCopyLabel.for = enableCommentReportAutoCopy.id;

        toolBelt.append(enableCommentReportAutoCopyLabel);

        // make some room for shrink button
        const shrinkButtonPlaceholder = createElementWithInnerTextAndStyle(
            'div',
            null, {
                padding: '0.8em'
            }
        );

        toolBelt.append(shrinkButtonPlaceholder);

        addExpandAndShrinkButtonToToolBelt(toolBelt);

        // Do not show toolBelt by default
        // D.body.append(toolBelt);
    };

    if (window.location.pathname.startsWith('/topic/detail')) {
        createTopicToolBelt();
    }

    const createTopicListToolBelt = () => {
        const toolBelt = createElementWithInnerTextAndStyle(
            'div',
            null, {
                position: 'fixed',
                left: '20px',
                bottom: '40px',
                padding: '5px 10px',
                backgroundColor: 'rgba(0, 0, 0, 0.5)',
                borderRadius: '5px',
                // display only after topic detail enabled
                display: 'none'
            }
        );
        toolBelt.classList.add('topic-list-tool-belt');

        const removeHiddenAuthorSelect = createElementWithInnerTextAndStyle('select');
        removeHiddenAuthorSelect.name = 'remove-hidden-author';
        const hintOption = createElementWithInnerTextAndStyle('option', '--- 选择作者以取消隐藏 ---');
        hintOption.value = 0 - Math.round(Math.random() * 1000);
        hintOption.disabled = true;
        removeHiddenAuthorSelect.append(hintOption);
        removeHiddenAuthorSelect.value = hintOption.value;
        const updateHiddenAuthorSelectOptions = () => {
            // create latest list of hidden authors
            while (removeHiddenAuthorSelect.firstChild) {
                removeHiddenAuthorSelect.removeChild(removeHiddenAuthorSelect.firstChild);
            }
            removeHiddenAuthorSelect.append(hintOption);
            for (const {
                    id,
                    name
                } of getAllHiddenAuthor()) {
                const option = createElementWithInnerTextAndStyle('option', name);
                option.value = id;
                removeHiddenAuthorSelect.append(option);
            }
        };
        updateHiddenAuthorSelectOptions();
        window.addEventListener(
            'topicdisplaytoggle',
            // addHiddenAuthorByIdAndName or removeHiddenAuthorById must be called before event dispatch
            updateHiddenAuthorSelectOptions
        );
        addStorageKeyListener(
            `${VER}-${E('隐藏作者')}`,
            updateHiddenAuthorSelectOptions
        );

        const removeHiddenAuthorByIdAndDispatch = (authorId) => {
            removeHiddenAuthorById(authorId);
            window.dispatchEvent(
                new CustomEvent(
                    'topicdisplaytoggle', {
                        detail: {
                            authorId,
                            display: true
                        },
                        bubbles: true,
                        cancelable: true
                    }
                )
            );
        };
        removeHiddenAuthorSelect.addEventListener(
            'change',
            (event) => {
                try {
                    const authorId = parseInt(event.target.value, 10);
                    if (isNaN(authorId)) {
                        throw new TypeError('authorId 无法转换为数字 - ', event.target.value);
                    }
                    const selectedAuthor = getHiddenAuthorById(authorId);
                    if (typeof selectedAuthor === 'undefined') {
                        createToastWithText('取消作者隐藏错误 - 该作者当前没有被隐藏');
                        return;
                    }
                    const {
                        name: authorName
                    } = selectedAuthor;
                    removeHiddenAuthorSelect.value = hintOption.value;
                    removeHiddenAuthorByIdAndDispatch(authorId);
                    createToastWithText(`已经取消对作者 ${authorName} 的隐藏`);
                } catch (err) {
                    clearHiddenAuthor();
                    createToastWithText('取消作者隐藏错误 - 已\清除所有隐藏的作者');
                    console.error('取消作者隐藏错误', err);
                }
            }
        );

        toolBelt.append(removeHiddenAuthorSelect);

        const clearAllHiddenAuthor = createElementWithInnerTextAndStyle(
            'button',
            '取消所有作者隐藏', {
                padding: '0.5em',
                width: '12em'
            }
        );

        clearAllHiddenAuthor.addEventListener(
            'click',
            () => {
                getAllHiddenAuthorId().forEach(
                    (authorId) => removeHiddenAuthorByIdAndDispatch(authorId)
                );
                createToastWithText('已经取消对所有作者的隐藏');
            }
        );

        toolBelt.append(clearAllHiddenAuthor);

        toolBelt.append(createElementWithInnerTextAndStyle('br'));

        const addSortConfigWithToastAndDispatchEvent = (sortConfig, index) => {
            const initialSortConfigCount = getAllSortConfigs().length;
            addSortConfig(sortConfig, index);
            window.dispatchEvent(
                new CustomEvent(
                    'topiclistreadytosort', {
                        bubbles: true,
                        cancelable: true
                    }
                )
            );
            if (initialSortConfigCount === 0) {
                createToastWithText('主题列表自动排序已启用');
            } else {
                createToastWithText('主题列表自动排序规则已变化');
            }
        };

        const deleteSortConfigByFieldWithToastAndDispatchEvent = (field) => {
            const initialSortConfigCount = getAllSortConfigs().length;
            deleteSortConfigByField(field);
            window.dispatchEvent(
                new CustomEvent(
                    'topiclistreadytosort', {
                        bubbles: true,
                        cancelable: true
                    }
                )
            );
            if (initialSortConfigCount === 1) {
                createToastWithText('主题列表自动排序已停用\n已恢复初始排序');
                window.dispatchEvent(
                    new CustomEvent(
                        'topiclistreadytorestore', {
                            bubbles: true,
                            cancelable: true
                        }
                    )
                );
            } else {
                createToastWithText('主题列表自动排序规则已变化');
            }
        }

        const sortByTopicTotalCommentsAsc = createElementWithInnerTextAndStyle(
            'input',
            null, {
                width: '1.8em',
                height: '1.8em',
                verticalAlign: 'middle'
            }
        );

        sortByTopicTotalCommentsAsc.id = 'sort-by-topic-total-comments';

        sortByTopicTotalCommentsAsc.type = 'checkbox';

        sortByTopicTotalCommentsAsc.checked = !!getSortConfigByField('commentCount');

        sortByTopicTotalCommentsAsc.addEventListener(
            'change',
            (event) => {
                console.warn('=== commentCount change event ===', event)
                if (!!event.target.checked) {
                    addSortConfigWithToastAndDispatchEvent({
                        field: 'commentCount',
                        order: 'ASC'
                    }, -1);
                } else {
                    deleteSortConfigByFieldWithToastAndDispatchEvent('commentCount');
                }
            }
        );

        // Sync checkbox status is pointless as sort status won't sync
        // Syncing sort status is too resource heavy
        /*
        addStorageKeyListener(
            `${VER}-${E('主题排序')}`,
            () => {
                const sortByCommentCountEnabled = !!getSortConfigByField('commentCount');
                sortByTopicTotalCommentsAsc.checked = sortByCommentCountEnabled;
            }
        );
        */

        toolBelt.append(sortByTopicTotalCommentsAsc);

        const sortByTopicTotalCommentsAscLabel = createElementWithInnerTextAndStyle(
            'label',
            '按主题回复数量升序排列', {
                padding: '0.5em',
                width: '12em',
                color: 'white'
            }
        );

        sortByTopicTotalCommentsAscLabel.for = sortByTopicTotalCommentsAsc.id;

        toolBelt.append(sortByTopicTotalCommentsAscLabel);

        toolBelt.append(createElementWithInnerTextAndStyle('br'));

        const sortByMyCommentsCountAsc = createElementWithInnerTextAndStyle(
            'input',
            null, {
                width: '1.8em',
                height: '1.8em',
                verticalAlign: 'middle'
            }
        );

        sortByMyCommentsCountAsc.id = 'sort-by-topic-my-comments-count';

        sortByMyCommentsCountAsc.type = 'checkbox';

        sortByMyCommentsCountAsc.checked = !!getSortConfigByField('myCommentsCount');

        sortByMyCommentsCountAsc.addEventListener(
            'change',
            (event) => {
                if (!!event.target.checked) {
                    addSortConfigWithToastAndDispatchEvent({
                        field: 'myCommentsCount',
                        order: 'ASC'
                    });
                } else {
                    deleteSortConfigByFieldWithToastAndDispatchEvent('myCommentsCount');
                }
            }
        );

        // Sync checkbox status is pointless as sort status won't sync
        // Syncing sort status is too resource heavy
        /*
        addStorageKeyListener(
            `${VER}-${E('主题排序')}`,
            () => {
                const sortByMyCommentsCountEnabled = !!getSortConfigByField('myCommentsCount');
                sortByMyCommentsCountAsc.checked = sortByMyCommentsCountEnabled;
            }
        );
        */

        toolBelt.append(sortByMyCommentsCountAsc);

        const sortByMyCommentsCountAscLabel = createElementWithInnerTextAndStyle(
            'label',
            '优先按我的回复数量升序排列', {
                padding: '0.5em',
                width: '12em',
                color: 'white'
            }
        );

        sortByMyCommentsCountAscLabel.for = sortByMyCommentsCountAscLabel.id;

        toolBelt.append(sortByMyCommentsCountAscLabel);

        toolBelt.append(createElementWithInnerTextAndStyle('br'));

        const topicCountDisplay = createElementWithInnerTextAndStyle(
            'span',
            '尚未开始对主题计数', {
                padding: '0.5em',
                width: '12em',
                color: 'white'
            }
        );

        window.addEventListener(
            'topiclistreadytocount',
            () => {
                let topicCount = -1;
                if (D.querySelector(communityTopicListQuery) instanceof HTMLElement) {
                    topicCount = D.querySelector(communityTopicListQuery).children.length;
                }
                if (D.querySelector(searchTopicListQuery) instanceof HTMLElement) {
                    topicCount = D.querySelector(searchTopicListQuery).querySelectorAll('.avg-content-block').length;
                }
                topicCountDisplay.innerText = `共有 ${topicCount} 条主题`;
                topicCountDisplay.dataset.topicCount = topicCount;
                window.dispatchEvent(
                    new CustomEvent(
                        'topiclistcountfinish', {
                            bubbles: true,
                            cancelable: true
                        }
                    )
                );
            }
        );

        toolBelt.append(topicCountDisplay);

        const autoLoadUntilTotalCountSelect = createElementWithInnerTextAndStyle('select');
        const updateAutoLoadUntilTotalCountSelectOptions = () => {
            while (autoLoadUntilTotalCountSelect.firstChild) {
                autoLoadUntilTotalCountSelect.removeChild(autoLoadUntilTotalCountSelect.firstChild);
            }
            const hintOption = createElementWithInnerTextAndStyle('option', '--- 仅在当前页面生效 ---');
            hintOption.value = 0 - Math.round(Math.random() * 1000);
            hintOption.disabled = true;
            autoLoadUntilTotalCountSelect.append(hintOption);
            const defaultOption = createElementWithInnerTextAndStyle('option', '--- 不启动自动读取 ---');
            defaultOption.value = 0 - Math.round(Math.random() * 1000);
            autoLoadUntilTotalCountSelect.append(defaultOption);
            autoLoadUntilTotalCountSelect.value = defaultOption.value;
            const loadUntilTotalCountOption = [
                50,
                150,
                350,
                650,
                1050
            ];
            for (const totalCount of loadUntilTotalCountOption) {
                const option = createElementWithInnerTextAndStyle('option', `读取至少 ${totalCount} 条主题`);
                option.value = totalCount;
                autoLoadUntilTotalCountSelect.append(option);
            }
        };
        updateAutoLoadUntilTotalCountSelectOptions();
        const attemptLoadMore = () => {
            const loadMoreButton = D.querySelector('.avg-load-more-footer.list-load-more button.avg-button');
            if (loadMoreButton instanceof HTMLElement) {
                loadMoreButton.dispatchEvent(new MouseEvent('click', {
                    bubbles: true,
                    cancelable: true
                }));
                return true;
            }
            return false;
        };
        const autoLoadUntilTotalCountHandler = () => {
            const desiredTopicCount = parseInt(autoLoadUntilTotalCountSelect.value, 10);
            const currentTopicCount = parseInt(topicCountDisplay.dataset.topicCount, 10);
            if (desiredTopicCount < 0) {
                return;
            }
            if (desiredTopicCount > currentTopicCount) {
                if (attemptLoadMore()) {
                    if (getAllSortConfigs().length > 0) {
                        createToastWithText('正在读取更多主题\n自动读取启用时建议取消勾选排序选项');
                    } else {
                        createToastWithText('正在读取更多主题');
                    }
                } else {
                    createToastWithText('没有更多主题可供读取\n页面可能不支持无限滚动');
                }
            }
        };
        autoLoadUntilTotalCountSelect.addEventListener(
            'change',
            autoLoadUntilTotalCountHandler
        );
        window.addEventListener(
            'topiclistcountfinish',
            autoLoadUntilTotalCountHandler
        );

        toolBelt.append(autoLoadUntilTotalCountSelect);

        addExpandAndShrinkButtonToToolBelt(toolBelt);

        D.body.append(toolBelt);
    };

    // topic list can load everywhere, thanks to SPA
    createTopicListToolBelt();

    const createPageMagic = () => {
        // auto agreed to tos
        onElementEventTypesByClassList(
            ['mouseover', 'focusin'],
            ['avg-login-form'],
            () => {
                const tosCheckbox = event.target.parentElement.querySelector('.avg-login-agree-info button');
                if (tosCheckbox instanceof HTMLElement && !tosCheckbox.classList.contains('checked')) {
                    createToastWithText('自动同意条款与政策');
                    tosCheckbox.dispatchEvent(new MouseEvent('click', {
                        bubbles: true,
                        cancelable: true
                    }));
                }
            }
        );

        const isCommentReportAutoCopyEnabled = () => getBinaryIntByStorageKey(`${VER}-${E('启用话题页面发言报告自动复制')}`) === 1;
        const copyCommentReport = () => {
            const {
                userName,
                authorId
            } = myUserNameAndAuthorIdStore.getMyUserNameAndAuthorId();
            copyFormattedReportForCurrentPageByUserNameAndAuthorId(userName, authorId, true);
            createToastWithText('报告复制成功✓');
        };

        // submit on ctrl + enter
        window.addEventListener(
            'keypress',
            (event) => {
                const {
                    keyCode,
                    ctrlKey,
                    shiftKey,
                    altKey
                } = event;
                if (keyCode === 13 && (ctrlKey || shiftKey || altKey)) {
                    if (
                        D.activeElement instanceof HTMLElement &&
                        D.activeElement.classList.contains('text-input')
                    ) {
                        let currentElement = D.activeElement;
                        let commentInputArea = null;
                        while (currentElement instanceof HTMLElement) {
                            if (currentElement.classList.contains('comment-input-area')) {
                                commentInputArea = currentElement;
                                break;
                            } else {
                                currentElement = currentElement.parentElement;
                            }
                        }
                        if (
                            commentInputArea instanceof HTMLElement &&
                            commentInputArea.querySelector('button.avg-button.avg-button-primary') instanceof HTMLElement
                        ) {
                            const submit = commentInputArea.querySelector('button.avg-button.avg-button-primary');
                            createToastWithText('正在使用快捷键提交');
                            event.preventDefault();
                            if (isCommentReportAutoCopyEnabled()) {
                                // copy before submit to avoid delay
                                copyCommentReport();
                            }
                            submit.dispatchEvent(
                                new MouseEvent(
                                    'click', {
                                        bubbles: true,
                                        cancelable: true
                                    }
                                )
                            );
                        }
                    }
                }
            }
        );

        // copy formatted report on submit button click
        onElementEventTypesByClassList(
            ['click'],
            ['submit-button', 'avg-button', 'avg-button-primary'],
            (event) => {
                if (isCommentReportAutoCopyEnabled()) {
                    copyCommentReport();
                }
            }
        );

        // copy formatted report on submit button click
        onElementEventTypesByClassList(
            ['click'],
            ['avg-button-text'],
            (event) => {
                if (isCommentReportAutoCopyEnabled()) {
                    copyCommentReport();
                }
            }
        );

        // show topic insight
        const fetchTopicDetail = (element, myAuthorId = -1) => {
            if (element.querySelector('.topic-detail') !== null) {
                return;
            }

            // topic detail block creation
            const topicDetail = createElementWithInnerTextAndStyle(
                'div',
                null, {
                    margin: '1em 0',
                    lineHeight: '2em'
                }
            );
            topicDetail.classList.add('topic-detail');
            const contentBlock = element.classList.contains('avg-content-block') ? element : element.querySelector('.avg-content-block');
            const itemContent = contentBlock.querySelector(':scope > .item-content');
            if (itemContent instanceof HTMLElement) {
                contentBlock.insertBefore(topicDetail, itemContent);
            } else {
                // not on topic search result
                createToastWithText('页面可能不包含主题 - 对象结构无法探知');
                console.error('页面可能不包含主题 - 对象结构无法探知', contentBlock);
                return;
            }

            // topic option block creation
            const topicOption = createElementWithInnerTextAndStyle(
                'div',
                null, {
                    margin: '1em 0',
                    lineHeight: '2em'
                }
            );
            topicOption.classList.add('topic-option');
            contentBlock.prepend(topicOption);

            let topicId = -1;
            if (
                element.querySelector('.avg-content-block') instanceof HTMLElement &&
                typeof element.querySelector('.avg-content-block').id === 'string'
            ) {
                const contentBlockId = element.querySelector('.avg-content-block').id;
                const idRegex = new RegExp(/\d+$/);
                if (contentBlockId.match(idRegex) && contentBlockId.match(idRegex)[0]) {
                    topicId = contentBlockId.match(idRegex)[0];
                } else {
                    createToastWithText('页面可能包含主题 - 但无法通过结构探知取得 topicId');
                    console.error('页面可能包含主题 - 但无法通过结构探知取得 topicId', element.querySelector('.avg-content-block'));
                    return;
                }
            } else if (element.querySelector('.content-body') instanceof HTMLElement) {
                // fallback to simulated mouse click
                let topicHref = '';
                const originalOpen = unsafeWindow.open;
                unsafeWindow.open = (href) => {
                    topicHref = href
                };
                element.querySelector('.content-body').dispatchEvent(new MouseEvent('click', {
                    bubbles: true,
                    cancelable: true
                }));
                unsafeWindow.open = originalOpen;
                if (topicHref.startsWith('/topic/detail/')) {
                    // topicHref should be shaped like '/topic/detail/123456'
                    topicId = topicHref.replace('/topic/detail/', '');
                } else {
                    createToastWithText('页面可能包含主题 - 但无法通过模拟点击取得 topicId');
                    console.error('页面可能包含主题 - 但无法通过模拟点击取得 topicId', element.querySelector('.content-body'));
                    return;
                }
            } else {
                createToastWithText('详情加载失败 - 无法取得 topicId');
                console.error('详情加载失败 - 无法取得 topicId', contentBlock);
                return;
            }
            const topicEndpoint = new URL(L.href);
            topicEndpoint.search = '';
            topicEndpoint.pathname = `/avg-portal-api/topic/${topicId}`;
            const commentEndpoint = new URL(L.href);
            commentEndpoint.search = '';
            commentEndpoint.pathname = `/avg-portal-api/topic/${topicId}/comment/new`;
            if (!!getCsrfToken()) {
                const params = {
                    csrf_token: getCsrfToken()
                };
                Object.keys(params).forEach(key => topicEndpoint.searchParams.append(key, params[key]));
                Object.keys(params).forEach(key => commentEndpoint.searchParams.append(key, params[key]));
            }

            return Promise.all([fetch(topicEndpoint), fetch(commentEndpoint)]).then(
                ([tr, cr]) => Promise.all([tr.json(), cr.json()])
            ).then(
                async ([topicResult, commentResult]) => {
                    const {
                        state: {
                            code: topicCode,
                            message: topicMessage
                        } = {
                            code: 0,
                            message: '未知错误'
                        }
                    } = topicResult;
                    const {
                        state: {
                            code: commentCode,
                            message: commentMessage
                        } = {
                            code: 0,
                            message: '未知错误'
                        }
                    } = commentResult;
                    if (topicCode !== 200000 || commentCode !== 200000) {
                        createToastWithText(`${topicId} 详情加载错误 - 内容:${topicMessage} - 回复:${commentMessage}`);
                        console.error(`${topicId} 详情加载错误 - 内容:${topicMessage} - 回复:${commentMessage}`);
                    } else {
                        const {
                            data: topicData
                        } = topicResult;
                        // console.warn('=== topicData ===', topicData.id, topicData)
                        const {
                            clickCount = -1,
                                commentCount = -1,
                                favoriteCount = -1,
                                authorId = -1,
                                createTime = 0,
                                author: {
                                    verificationInfo = '',
                                    lv = -1,
                                    vip = 0
                                } = {
                                    verificationInfo: '',
                                    lv: -1,
                                    vip: 0
                                },
                                authorName = '无名氏',
                                content = '',
                                imageInfo = '',
                                audioInfo = '',
                                videoInfo = '',
                                themes = []
                        } = topicData;
                        const vipStatus = vip === 1 ? 'VIP 用户' : '';
                        const parseCountFromInfo = (info) => {
                            let count = 0;
                            if (info.length > 0) {
                                try {
                                    count = JSON.parse(info).length;
                                } catch (err) {
                                    count = -1;
                                    createToastWithText(`${topicId} 回复数量计算错误`);
                                    console.error(`${topicId} 回复数量计算错误`, err);
                                }
                            }
                            return count;
                        };
                        const imageCount = parseCountFromInfo(imageInfo);
                        const audioCount = parseCountFromInfo(audioInfo);
                        const videoCount = parseCountFromInfo(videoInfo);
                        const charCount = escape(content).split('%u').length - 1;
                        const valueStyle = {
                            margin: '0 0.1em',
                            color: 'red'
                        };
                        const highlightStyle = {
                            color: 'red',
                            borderRadius: '2em',
                            padding: '0.25em 0.8em',
                            color: 'white',
                            backgroundColor: 'red'
                        };
                        const defaultStyle = {
                            margin: '0 0.1em'
                        };
                        const {
                            data: commentData
                        } = commentResult;
                        // console.warn('=== commentData ===', topicData.id, commentData)
                        const createDetailInParent = (parentElement) => {
                            if (!!vipStatus) {
                                parentElement.append(
                                    createElementWithInnerTextAndStyle('span', vipStatus, valueStyle)
                                );
                            }
                            parentElement.append(
                                createElementWithInnerTextAndStyle('span', '发布于', defaultStyle)
                            );
                            parentElement.append(
                                createElementWithInnerTextAndStyle('span', (new Date(createTime)).toLocaleString(), valueStyle)
                            );
                            parentElement.append(
                                createElementWithInnerTextAndStyle('span', '共', defaultStyle)
                            );
                            parentElement.append(
                                createElementWithInnerTextAndStyle('span', String(charCount), valueStyle)
                            );
                            parentElement.append(
                                createElementWithInnerTextAndStyle('span', '字 总点击', defaultStyle)
                            );
                            parentElement.append(
                                createElementWithInnerTextAndStyle('span', String(clickCount), valueStyle)
                            );
                            parentElement.append(
                                createElementWithInnerTextAndStyle('span', '总评论', defaultStyle)
                            );
                            parentElement.append(
                                createElementWithInnerTextAndStyle('span', String(commentCount), valueStyle)
                            );
                            parentElement.append(
                                createElementWithInnerTextAndStyle('span', '总收藏', defaultStyle)
                            );
                            parentElement.append(
                                createElementWithInnerTextAndStyle('span', String(favoriteCount), valueStyle)
                            );
                            [imageCount, audioCount, videoCount].forEach(
                                (count, idx) => {
                                    if (count !== 0) {
                                        const label = ['图片', '音频', '视频'];
                                        parentElement.append(
                                            createElementWithInnerTextAndStyle('span', label[idx], defaultStyle)
                                        );
                                        parentElement.append(
                                            createElementWithInnerTextAndStyle('span', String(count), valueStyle)
                                        );
                                    }
                                }
                            );
                            const myCommentsCount = getCommentsCountFromDataByAuthorId(commentData, myAuthorId);
                            if (myCommentsCount > 0) {
                                parentElement.append(createElementWithInnerTextAndStyle('br'));
                                parentElement.append(
                                    createElementWithInnerTextAndStyle('span', `已经回复了 ${myCommentsCount} 次`, highlightStyle)
                                );
                            }

                            // dataset assigning for sorting
                            Object.assign(
                                parentElement.dataset, {
                                    authorName,
                                    authorId,
                                    vip,
                                    clickCount,
                                    commentCount,
                                    favoriteCount,
                                    imageCount,
                                    audioCount,
                                    videoCount,
                                    charCount,
                                    myCommentsCount,
                                    createTime
                                }
                            );
                        };
                        createDetailInParent(topicDetail);
                        const createOptionInParent = (parentElement) => {
                            const hideTopicFromAuthorButton = createElementWithInnerTextAndStyle(
                                'button',
                                '隐藏此作者的主题', {
                                    padding: '0.5em'
                                }
                            );
                            hideTopicFromAuthorButton.classList.add('hide-topic-from-author');
                            hideTopicFromAuthorButton.addEventListener(
                                'click',
                                () => {
                                    addHiddenAuthorByIdAndName(authorId, authorName);
                                    window.dispatchEvent(
                                        new CustomEvent(
                                            'topicdisplaytoggle', {
                                                detail: {
                                                    authorId,
                                                    display: false
                                                },
                                                bubbles: true,
                                                cancelable: true
                                            }
                                        )
                                    );
                                    createToastWithText(`已经设置对作者 ${authorName} 的隐藏`);
                                }
                            );
                            parentElement.append(hideTopicFromAuthorButton);
                        };
                        createOptionInParent(topicOption);

                        // event based automata
                        const handleTopicDisplay = (display) => {
                            const getCurrentDisplayStatus = () => {
                                let display = true;
                                for (const child of contentBlock.children) {
                                    if (!child.classList.contains('topic-option')) {
                                        if (child.style.display === 'none') {
                                            display = false;
                                            break;
                                        }
                                    }
                                }
                                return display;
                            };
                            for (const child of contentBlock.children) {
                                if (!child.classList.contains('topic-option')) {
                                    child.style.display = display ? '' : 'none';
                                }
                            }
                            const hideTopicFromAuthorButton = topicOption.querySelector('.hide-topic-from-author');
                            if (hideTopicFromAuthorButton instanceof HTMLElement) {
                                hideTopicFromAuthorButton.style.display = display ? '' : 'none';
                            }
                            const toggleTopicDisplayButton = topicOption.querySelector('.toggle-topic-display');
                            if (!display) {
                                const getTopicDisplayButtonText = () => getCurrentDisplayStatus() ? '已隐藏该作者 - 不想看了' : '已隐藏该作者 - 想看一眼';
                                if (!(toggleTopicDisplayButton instanceof HTMLElement)) {
                                    // add a button to toggle current display
                                    const toggleTopicDisplayButton = createElementWithInnerTextAndStyle(
                                        'button',
                                        getTopicDisplayButtonText(), {
                                            padding: '0.5em'
                                        }
                                    );
                                    toggleTopicDisplayButton.classList.add('toggle-topic-display');
                                    toggleTopicDisplayButton.addEventListener(
                                        'click',
                                        () => {
                                            const currentDisplayStatus = getCurrentDisplayStatus();
                                            for (const child of contentBlock.children) {
                                                if (!child.classList.contains('topic-option')) {
                                                    child.style.display = !currentDisplayStatus ? '' : 'none';
                                                }
                                            }
                                            toggleTopicDisplayButton.innerText = getTopicDisplayButtonText();
                                        }
                                    );
                                    topicOption.append(toggleTopicDisplayButton);
                                } else {
                                    toggleTopicDisplayButton.innerText = getTopicDisplayButtonText();
                                }
                            } else {
                                // author removed from hidden list
                                if (toggleTopicDisplayButton instanceof HTMLElement) {
                                    topicOption.removeChild(toggleTopicDisplayButton);
                                }
                            }
                        };
                        handleTopicDisplay(!isAuthorIdHidden(authorId));
                        window.addEventListener(
                            'topicdisplaytoggle',
                            (event) => {
                                const {
                                    detail: {
                                        authorId: targetAuthorId = -1,
                                        display = true
                                    }
                                } = event;
                                if (targetAuthorId === authorId) {
                                    handleTopicDisplay(display);
                                }
                            }
                        );
                    }
                }
            ).catch(
                (err) => {
                    createToastWithText(`${topicId} 详情加载失败`);
                    console.error(`${topicId} 详情加载失败`, err);
                }
            );
        };

        // remember to disconnect observer before calling this
        const sortTopicsWithParentQuery = (parentQuery, memorize) => {
            // last sort means highest priority
            const sortConfigs = getAllSortConfigs().reverse();
            if (sortConfigs.length > 0) {
                const compares = [];
                for (const {
                        field,
                        order
                    } of sortConfigs) {
                    const compare = (l, r) => {
                        const getDataset = (element) => {
                            if (element instanceof HTMLElement) {
                                const topicDetail = element.querySelector('.topic-detail');
                                if (topicDetail instanceof HTMLElement) {
                                    return topicDetail.dataset;
                                }
                            }
                            return null;
                        };

                        if (
                            getDataset(l) !== null && getDataset(r) !== null &&
                            typeof getDataset(l)[field] !== 'undefined' &&
                            typeof getDataset(r)[field] !== 'undefined'
                        ) {
                            switch (order) {
                                case 'DESC':
                                    return getDataset(r)[field] - getDataset(l)[field];
                                case 'ASC':
                                default:
                                    return getDataset(l)[field] - getDataset(r)[field];
                            }
                        }
                        return 0;
                    };
                    compares.push(compare);
                }
                sortHTMLElementsInParentByQueryAndCompare(
                    parentQuery,
                    compares,
                    memorize
                );
                createToastWithText('主题列表排序完成');
            }
        };

        const singletonMutationObserverStore = (() => {
            let disconnect = () => {};
            let reconnect = () => {};
            let getObserver = () => null;
            return {
                saveObserverArtifacts: ({
                    disconnect: originalDisconnect,
                    reconnect: originalReconnect,
                    getObserver: originalGetObserver
                }) => {
                    disconnect = originalDisconnect;
                    reconnect = originalReconnect;
                    getObserver = originalGetObserver;
                },
                disconnect: () => disconnect(),
                reconnect: () => reconnect(),
                getObserver: () => getObserver()
            };
        })();

        const topicListOrderMemorizeStore = createOrderMemorizeStore();

        window.addEventListener(
            'topiclistreadytosort',
            () => {
                singletonMutationObserverStore.disconnect();
                if (D.querySelector(communityTopicListQuery) instanceof HTMLElement) {
                    sortTopicsWithParentQuery(communityTopicListQuery, topicListOrderMemorizeStore.memorize);
                }
                if (D.querySelector(searchTopicListQuery) instanceof HTMLElement) {
                    sortTopicsWithParentQuery(searchTopicListQuery, topicListOrderMemorizeStore.memorize);
                }
                singletonMutationObserverStore.reconnect();
            }
        );

        window.addEventListener(
            'topiclistreadytorestore',
            () => {
                singletonMutationObserverStore.disconnect();
                if (D.querySelector(communityTopicListQuery) instanceof HTMLElement) {
                    restoreHTMLElementsOrderInParentByQueryAndOriginalIndexGetter(
                        communityTopicListQuery,
                        topicListOrderMemorizeStore.getOriginalIndexByElement
                    );
                }
                if (D.querySelector(searchTopicListQuery) instanceof HTMLElement) {
                    restoreHTMLElementsOrderInParentByQueryAndOriginalIndexGetter(
                        searchTopicListQuery,
                        topicListOrderMemorizeStore.getOriginalIndexByElement
                    );
                }
                singletonMutationObserverStore.reconnect();
            }
        );

        const {
            topicObserveStartCallback,
            topicNodeAddedCallback
        } = (() => {
            let myCurrentAuthorIdPromise = Promise.resolve(-1);
            const debounceSort = createDebouncerByTimeout(
                () => createToastWithText('侦测到主题列表发生变动'),
                () => {
                    window.dispatchEvent(
                        new CustomEvent(
                            'topiclistreadytosort', {
                                bubbles: true,
                                cancelable: true
                            }
                        )
                    );
                    window.dispatchEvent(
                        new CustomEvent(
                            'topiclistreadytocount', {
                                bubbles: true,
                                cancelable: true
                            }
                        )
                    );
                }
            );
            const topicObserveStartCallback = () => {
                createToastWithText('自动读取主题详情已启用');
                // clear topic list order memorize store
                topicListOrderMemorizeStore.clear();
                // we only upadte authorId once when observe start
                myCurrentAuthorIdPromise = getMyAuthorId();
                const topicListToolBelt = D.querySelector('.topic-list-tool-belt');
                if (topicListToolBelt instanceof HTMLElement) {
                    topicListToolBelt.style.display = '';
                }
            };
            const topicNodeAddedCallback = async (element) => {
                const myCurrentAuthorId = await myCurrentAuthorIdPromise;
                const fetchTopicDetailFinished = await fetchTopicDetail(element, myCurrentAuthorId);
                debounceSort();
                return fetchTopicDetailFinished;
            };
            return {
                topicObserveStartCallback,
                topicNodeAddedCallback
            };
        })();

        const addSingletonListObserver = createSingletonObserverByQueryAndClassList(
            communityTopicListQuery,
            ['avg-list-line'],
            topicObserveStartCallback,
            topicNodeAddedCallback
        );

        const addDedupListInit = (() => {
            let targetNode = null;
            const addListInit = async (event, element) => {
                if (targetNode === element) {
                    return;
                }
                targetNode = element;
                const myAuthorId = await getMyAuthorId();
                const allFetchTopicDetailPromises = [...element.querySelectorAll('.avg-list-line')].map(
                    (element) => fetchTopicDetail(element, myAuthorId)
                );
                await Promise.all(allFetchTopicDetailPromises);
                sortTopicsWithParentQuery(communityTopicListQuery);
                singletonMutationObserverStore.saveObserverArtifacts(addSingletonListObserver());
                window.dispatchEvent(
                    new CustomEvent(
                        'topiclistreadytocount', {
                            bubbles: true,
                            cancelable: true
                        }
                    )
                );
            };
            return addListInit;
        })();

        onElementEventTypesByClassList(
            ['mouseover'],
            ['avg-list', 'forums-list'],
            addDedupListInit
        );

        const addSingletonSearchObserver = createSingletonObserverByQueryAndClassList(
            searchTopicListQuery,
            ['avg-forums-list-item', 'avg-content-block'],
            topicObserveStartCallback,
            topicNodeAddedCallback
        );

        const addDedupSearchInit = (() => {
            let targetNode = null;
            const addSearchInit = async (event, element) => {
                if (targetNode === element) {
                    return;
                }
                targetNode = element;
                const myAuthorId = await getMyAuthorId();
                const allFetchTopicDetailPromises = [...element.querySelectorAll('.avg-forums-list-item.avg-content-block')].map(
                    (element) => fetchTopicDetail(element, myAuthorId)
                );
                await Promise.all(allFetchTopicDetailPromises);
                sortTopicsWithParentQuery(searchTopicListQuery);
                singletonMutationObserverStore.saveObserverArtifacts(addSingletonSearchObserver());
                window.dispatchEvent(
                    new CustomEvent(
                        'topiclistreadytocount', {
                            bubbles: true,
                            cancelable: true
                        }
                    )
                );
            };
            return addSearchInit;
        })();

        onElementEventTypesByClassList(
            ['mouseover'],
            ['search-result-container'],
            addDedupSearchInit
        );
    };

    createPageMagic();
})();