微博帖子一键收藏、屏蔽、新页面打开

在微博网页端,为每个帖子创建一个收藏和新页面打开按钮。

// ==UserScript==
// @name         微博帖子一键收藏、屏蔽、新页面打开
// @namespace    http://tampermonkey.net/
// @version      20241002.2
// @description  在微博网页端,为每个帖子创建一个收藏和新页面打开按钮。
// @author       Fat Cabbage
// @license      MIT
// @match        https://www.weibo.com/*
// @match        https://weibo.com/*
// @match        https://s.weibo.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=weibo.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @require      https://code.jquery.com/jquery-3.5.1.min.js
// ==/UserScript==
/* globals jQuery, $, waitForKeyElements */

let blockConfig = new Map();
let onScrollFlag = false;
let needUpdateNodeList = [];
let blogCaches = new Map();
let menuSettingFav;
let menuSettingBlock;

class ClassName {
    static button = 'button_a656';
    static buttonFavorite = 'button_a656_favorite';
    static buttonOpenNewTab = 'button_a656_open_new_tab';
}

class Domain {
    static WEIBO = 'weibo.com';
    static WEIBO_S = 's.weibo.com';
    static WEIBO_3W = 'www.weibo.com';
    static domain = location.hostname;
}

class Const {
    static ID = 'ID';
    static BLOG_ID = 'blogID';
    static IS_FAVORITE = 'isFavorites';
    static LAST_UPDATED = 'lastUpdated';
    static IS_LOADING = 'isLoading';

    static RES_OK = 'ok';
    static RES_CODE = 'code';

    static FAV_TOTAL_FAV = 'fav_total_num';
    static FAV_TAGS = 'tags';
    static FAV_TAGS_TAG = 'tag';
    static FAV_TAGS_COUNT = 'count';
    static FAV_TOTAL_TAG = 'total_number';
    static FAV_SELECT_TAGS = 'CONST_FAV_SELECT_TAGS';
    static BLOCK_SELECT_LEVEL = 'CONST_BLOCK_SELECT_LEVEL';

    static LIST_TO_BE_BLOCK = 'LIST_TO_BE_BLOCK';
    static LIST_TO_BE_BLOCK_UPDATING = 'LIST_TO_BE_BLOCK_UPDATING';

    static BLOCK_FULL_LIST_STORE = 'BLOCK_FULL_LIST_STORE';
    static BLOCK_FULL_LIST_STORE_LAST_UPDATE_DATE = 'BLOCK_FULL_LIST_STORE_LAST_UPDATE_DATE';

    static PROMPT_TIME_MS = 1000;
}

class Selector {
    static settingButtonSelector = 'button[title="设置"]';
    static rootNodeClass;
    static postNodeFullClass;
    static buttonLocateSelector;
    static userASector;
    static userTitleSector;
    static timeASector;
    static forwardNodeStartClass;
    static forwardNodeSelector;
    static likeButtonSelector;

    static {
        if (Domain.domain === Domain.WEIBO || Domain.domain === Domain.WEIBO_3W) {
            Selector.rootNodeClass = 'vue-recycle-scroller__item-wrapper';
            Selector.rootNodeClass = 'Main_full';
            Selector.postNodeFullClass = `Feed_wrap`;
            Selector.buttonLocateSelector = 'div[class*="head_main"]';
            Selector.userASector = `a[class*="head_name"]`;
            Selector.userTitleSector = `a[class*="head_name"] > span`;
            Selector.timeASector = `a[class^="head-info_time"]`;
            Selector.forwardNodeStartClass = 'retweet Feed_retweet'
            Selector.forwardNodeSelector = 'div.retweet[class*="Feed_retweet"]'
            Selector.likeButtonSelector = 'button[title="赞"]';
        } else if (Domain.domain === Domain.WEIBO_S) {
            Selector.rootNodeClass = 'main-full';
            Selector.postNodeFullClass = 'card';
            Selector.buttonLocateSelector = 'div.menu.s-fr';
            // buttonLocateSelector = 'div.from > a:last-child';
            Selector.userASector = `div.info > :nth-child(2) > a:first-child`;
            Selector.userTitleSector = Selector.userASector;
            Selector.timeASector = `div.from > a:first-child`;
            Selector.likeButtonSelector = 'a[title="赞"]';
        }
    }
}

class BlogCache {
    static get(blogID, key) {
        if (!blogCaches.has(blogID)) {
            let blogCache = {};
            blogCaches.set(blogID, blogCache);
        }
        return blogCaches.get(blogID)[key];
    }

    static set(blogID, key, value) {
        let blogCache = blogCaches.get(blogID);
        if (blogCache == null) {
            blogCache = {};
        }
        blogCache[key] = value;
        blogCache[Const.LAST_UPDATED] = new Date();
        blogCaches.set(blogID, blogCache);
    }
}

class TimeConvert {
    static toSeconds(milliseconds) {
        return milliseconds / 1000;
    }

    static toMinutes(milliseconds) {
        return TimeConvert.toSeconds(milliseconds) / 60;
    }

    static toHours(milliseconds) {
        return TimeConvert.toMinutes(milliseconds) / 60;
    }

    static toDays(milliseconds) {
        return TimeConvert.toHours(milliseconds) / 24;
    }
}

class Do {
    static setFavTags() {
        function getFavTagInfo() {
            let url = 'https://weibo.com/ajax/favorites/tags?is_show_total=1';
            let token = Cookie.get('XSRF-TOKEN');
            return $.ajax({
                url: url, type: 'GET', headers: {
                    'Content-Type': 'application/json; charset=utf-8', 'X-Xsrf-Token': `${token}`
                }
            }).then(res => {
                if (typeof res === `string`) {
                    console.log(res);
                    return false;
                }
                return res;
            });
        }

        getFavTagInfo().then(async res => {
            if (res) {
                let tagList = res[Const.FAV_TAGS];
                let tagText = [];
                for (let i = 0; i < tagList.length; i++) {
                    let t = tagList[i];
                    let name = t[Const.FAV_TAGS_TAG];
                    let count = t[Const.FAV_TAGS_COUNT];
                    tagText.push(`${i} ${name} ( 共 ${count} 条 )`);
                }
                let tagTmp = tagText.join('\n');

                GM_.get(Const.FAV_SELECT_TAGS).then(selected => {
                    if (selected === '') {
                        selected = '无';
                    }

                    let info = `收藏总数: ${res[Const.FAV_TOTAL_FAV]}
标签总数:${res[Const.FAV_TOTAL_TAG]}
当前标签选择:${selected}
1. 输入标签索引,以空格分隔,最多可设置两个标签
2. 输入为空表示不设置标签
3. 如需新建标签,请通过微博手动添加
示例:要选择第1个与第3个已有标签,输入“0 2”\n
${tagTmp}`;

                    let input = prompt(info).trim()
                    let nameTmp;
                    if (input === '') {
                        alert('标签设置已清空');
                        nameTmp = '';
                    } else {
                        let split = input.split(' ');
                        let nameList = [];
                        for (let i = 0; i < split.length; i++) {
                            let j = parseInt(split[i]);
                            if (j < 0 || j > tagList.length - 1) {
                                alert(`第${i + 1}个输入不合法,需要重新输入`);
                                return;
                            }
                            let t = tagList[j];
                            let name = t[Const.FAV_TAGS_TAG];
                            nameList.push(name);
                        }
                        nameTmp = nameList.join(' ');
                        alert(`标签已设置: ${nameTmp}`);
                    }
                    GM_.set(Const.FAV_SELECT_TAGS, nameTmp);

                    GM_.unregisterMenuCommand(menuSettingFav);
                    if (nameTmp === '') {
                        nameTmp = '无';
                    }
                    menuSettingFav = GM_.registerMenuCommand(`设置收藏标签 当前:${nameTmp}`, () => {
                        Do.setFavTags();
                    });
                });
            } else {
                alert('获取标签列表失败');
            }
        })
    }

    static setBlockLevels() {
        let levelList = ['不看微博', '禁止互动', '禁止关注'];
        let levelText = [];
        for (let i = 0; i < levelList.length; i++) {
            let item = levelList[i];
            levelText.push(`${i} ${item}`);
        }
        let tagTmp = levelText.join('\n');

        GM_.get(Const.BLOCK_SELECT_LEVEL).then(selected => {
            if (selected === '') {
                selected = '不看微博';
            }

            let info = `当前屏蔽设置:${selected}
1. 输入屏蔽级别索引,以空格分隔
2. 输入为空表示默认设置(${levelList[0]})
示例:要选择第1个与第3个屏蔽级别,输入“0 2”\n
${tagTmp}`;

            let input = prompt(info).trim()
            let nameTmp;
            if (input === '') {
                alert('屏蔽已设为默认');
                nameTmp = '不看微博';
            } else {
                let split = input.split(' ');
                let nameList = [];
                for (let i = 0; i < split.length; i++) {
                    let j = parseInt(split[i]);
                    if (j < 0 || j > levelList.length - 1) {
                        alert(`第${i + 1}个输入不合法,需要重新输入`);
                        return;
                    }
                    let name = levelList[j];
                    nameList.push(name);
                }
                nameTmp = nameList.join(' ');
                alert(`屏蔽已设置: ${nameTmp}`);
            }
            GM_.set(Const.BLOCK_SELECT_LEVEL, nameTmp);

            GM_.unregisterMenuCommand(menuSettingBlock);
            if (nameTmp === '') {
                nameTmp = '无';
            }
            menuSettingBlock = GM_.registerMenuCommand(`设置屏蔽级别 当前:${nameTmp}`, () => {
                Do.setBlockLevels();
            });
        });
    }
}

class Locate {
    static articleNode(node, type = 'default') {
        let postList = node.querySelectorAll(`article[class*="Feed_wrap"]`);
        if (postList.length > 0) {
            return postList[0];
        }
        while (node != null) {
            if (node.className == null) {
                return null;
            }
            if (type === 'default') {
                if (node.className.indexOf(Selector.postNodeFullClass) >= 0) {
                    return node;
                }
            } else if (type === 'forward') {
                if (node.className.indexOf(Selector.forwardNodeStartClass) >= 0) {
                    return node;
                }
            }
            node = node.parentNode;
        }
        return null;
    }

    static likeButton(articleNode) {
        return articleNode.querySelector(Selector.likeButtonSelector)
    }
}

class BlogView {
    static getBlogID(articleNode) {
        let timeA = articleNode.querySelector(Selector.timeASector);
        let url = timeA.href;
        let index = url.lastIndexOf('/');
        return url.substring(index + 1);
    }

    static getUserID(articleNode) {
        let url = articleNode.querySelector(Selector.userASector).href;
        let slashIndex = url.lastIndexOf('/');
        let questionIndex = url.lastIndexOf('?');
        if (questionIndex >= 0) {
            return url.substring(slashIndex + 1, questionIndex);
        } else {
            return url.substring(slashIndex + 1);
        }
    }

    static getUsername(articleNode) {
        return articleNode.querySelector(Selector.userTitleSector).text;
    }

    static getBlogIDNum(articleNode) {
        if (Domain.domain === Domain.WEIBO_S) {
            let node = articleNode;
            while (node != null) {
                let actionType = node.getAttribute(`action-type`);
                if (actionType === `feed_list_item`) {
                    return node.getAttribute(`mid`);
                }

                node = node.parentNode;
            }
            return null;
        }
        return null;
    }

    static getLink(articleNode) {
        return articleNode.querySelector(Selector.timeASector).href;
    }
}

class Button {
    static buttonClassList;

    static addBaseClass(node) {
        if (Button.buttonClassList == null) {
            let settingButton = document.querySelector(Selector.settingButtonSelector)
            let classList = settingButton.classList
            Button.buttonClassList = Array.from(classList).filter(className => className.startsWith('IconBox_'));
        }
        for (let cl of Button.buttonClassList) {
            node.classList.add(cl);
        }
    }

    static createFavorite(type = 'default', like = false) {
        let buttonNode = document.createElement('button');

        buttonNode.classList.add(ClassName.button);
        buttonNode.classList.add(ClassName.buttonFavorite);
        Button.addBaseClass(buttonNode);

        buttonNode.style.width = 'auto';
        buttonNode.style.minWidth = '46.667px';
        buttonNode.style.height = '28px';
        buttonNode.style.marginLeft = '0px';
        buttonNode.style.marginRight = '5px';
        buttonNode.style.paddingLeft = '10px';
        buttonNode.style.paddingRight = '10px';

        buttonNode.addEventListener('click', ev => {
            let buttonNode = ev.target;
            let articleNode = Locate.articleNode(buttonNode, type);
            let blogID = BlogView.getBlogID(articleNode);
            let ID = BlogCache.get(blogID, Const.ID)

            let isFavorites = BlogCache.get(blogID, Const.IS_FAVORITE)
            let error = false;

            if (isFavorites) {
                Favorite.remove(ID).then(res => {
                    if (res) {
                        BlogCache.set(blogID, Const.IS_FAVORITE, false);
                        buttonNode.innerText = `已取消收藏`;
                        setTimeout(() => {
                            buttonNode.innerText = `收藏`;
                        }, Const.PROMPT_TIME_MS);
                    } else {
                        buttonNode.innerText = `取消收藏失败`;
                        error = true;
                    }
                });
            } else {
                Favorite.set(ID).then(res => {
                    if (res) {
                        BlogCache.set(blogID, Const.IS_FAVORITE, true);
                        buttonNode.innerText = `已收藏`;
                        setTimeout(() => {
                            buttonNode.innerText = `取消收藏`;
                        }, Const.PROMPT_TIME_MS);
                    } else {
                        buttonNode.innerText = `收藏失败`;
                        error = true;
                    }
                });
            }
        });
        return buttonNode;
    }

    static createOpen(type = 'default') {
        let buttonNode = document.createElement('button');
        buttonNode.textContent = '新页面打开';

        buttonNode.classList.add(ClassName.button);
        buttonNode.classList.add(ClassName.buttonOpenNewTab);
        Button.addBaseClass(buttonNode);

        buttonNode.setAttribute('href', '#woo_svg_nav_sun');

        buttonNode.style.width = 'auto';
        buttonNode.style.minWidth = '46.667px';
        buttonNode.style.height = '28px';
        buttonNode.style.marginRight = '5px';
        buttonNode.style.marginLeft = '0px';
        buttonNode.style.paddingLeft = '10px';
        buttonNode.style.paddingRight = '10px';

        buttonNode.addEventListener('click', ev => {
            let buttonNode = ev.target;
            let articleNode = Locate.articleNode(buttonNode, type);
            let link = BlogView.getLink(articleNode);
            window.open(link);
        });
        return buttonNode;
    }

    static createBlock(type = 'default') {
        let buttonNode = document.createElement('button');
        buttonNode.textContent = '屏蔽用户';

        buttonNode.classList.add(ClassName.button);
        buttonNode.classList.add(ClassName.buttonOpenNewTab);
        Button.addBaseClass(buttonNode);

        buttonNode.style.width = 'auto';
        buttonNode.style.minWidth = '46.667px';
        buttonNode.style.height = '28px';
        buttonNode.style.marginLeft = '0px';
        buttonNode.style.marginRight = '5px';
        buttonNode.style.paddingLeft = '10px';
        buttonNode.style.paddingRight = '10px';

        buttonNode.addEventListener('click', ev => {
            let buttonNode = ev.target;
            let articleNode = Locate.articleNode(buttonNode, type);
            let userID = BlogView.getUserID(articleNode);
            let username = BlogView.getUsername(articleNode);
            Block.set(userID, username).then(res => {
                if (res) {
                    buttonNode.innerText = `已屏蔽`;
                } else {
                    buttonNode.innerText = `屏蔽失败`;
                }
            });
        });
        return buttonNode;
    }
}

class Favorite {
    static get(ID) {
        BlogCache.set(ID, Const.IS_LOADING, true);
        if (Domain.domain === Domain.WEIBO || Domain.domain === Domain.WEIBO_3W) {

            let url;
            if (Domain.domain === Domain.WEIBO) {
                url = `https://weibo.com/ajax/statuses/show?id=${ID}`;
            } else {
                url = `https://www.weibo.com/ajax/statuses/show?id=${ID}`;
            }

            return $.ajax({
                url: url, type: 'GET',
            }).then(res => {
                BlogCache.set(ID, Const.ID, res['id']);
                BlogCache.set(ID, Const.BLOG_ID, ID);
                BlogCache.set(ID, Const.IS_FAVORITE, res['favorited']);
                BlogCache.set(ID, Const.IS_LOADING, false);
            });
        } else if (Domain.domain === Domain.WEIBO_S) {
            return new Promise(() => {
                BlogCache.set(ID, Const.BLOG_ID, ID);
                BlogCache.set(ID, Const.IS_FAVORITE, false);
                BlogCache.set(ID, Const.IS_LOADING, false);
            })
        }
    }

    static set(ID) {
        if (Domain.domain === Domain.WEIBO || Domain.domain === Domain.WEIBO_3W) {
            let data = JSON.stringify({'id': `${ID}`});
            let token = Cookie.get('XSRF-TOKEN');

            let url;
            if (Domain.domain === Domain.WEIBO) {
                url = `https://weibo.com/ajax/statuses/createFavorites`;
            } else {
                url = `https://www.weibo.com/ajax/statuses/createFavorites`;
            }

            return $.ajax({
                url: url, type: 'POST', data: data, headers: {
                    'Content-Type': 'application/json; charset=utf-8', 'X-Xsrf-Token': `${token}`
                }
            }).then(async res => {
                if (typeof res === `string`) {
                    console.log(res);
                    return false;
                }
                return GM_.get(Const.FAV_SELECT_TAGS).then(tags => {
                    if (tags !== '') {
                        tags = tags.split(' ');
                        tags = tags.join(',');
                        Favorite.setTags(ID, tags);
                    }
                    return res[Const.RES_OK] === 1;
                });
            });
        } else if (Domain.domain === Domain.WEIBO_S) {
            let data = {
                'mid': `${ID}`
            };
            return $.ajax({
                url: `https://s.weibo.com/ajax_Mblog/favAdd`, type: 'POST', data: data
            }).then(res => {
                if (typeof res === `string`) {
                    console.log(res);
                    return false;
                }
                return res[Const.RES_CODE] === `100000`;
            });
        }
    }

    static remove(ID) {
        if (Domain.domain === Domain.WEIBO || Domain.domain === Domain.WEIBO_3W) {
            let data = JSON.stringify({'id': `${ID}`});
            let token = Cookie.get('XSRF-TOKEN');

            let url;
            if (Domain.domain === Domain.WEIBO) {
                url = `https://weibo.com/ajax/statuses/destoryFavorites`;
            } else {
                url = `https://www.weibo.com/ajax/statuses/destoryFavorites`;
            }

            return $.ajax({
                url: url, type: 'POST', data: data, headers: {
                    'Content-Type': 'application/json; charset=utf-8', 'X-Xsrf-Token': `${token}`
                }
            }).then(res => {
                if (typeof res === `string`) {
                    console.log(res);
                    return false;
                }
                return res[Const.RES_OK] === 1;
            });
        } else if (Domain.domain === Domain.WEIBO_S) {
            let data = {
                'mid': `${ID}`
            };
            return $.ajax({
                url: `https://s.weibo.com/ajax_Mblog/favDel`, type: 'POST', data: data
            }).then(res => {
                if (typeof res === `string`) {
                    console.log(res);
                    return false;
                }
                return res[Const.RES_CODE] === `100000`;
            });
        }
    }

    static setTags(ID, tags) {
        let data = JSON.stringify({'id': `${ID}`, 'tags': `${tags}`});
        let token = Cookie.get('XSRF-TOKEN');

        let url = 'https://weibo.com/ajax/favorites/tags/update';
        return $.ajax({
            url: url, type: 'POST', data: data, headers: {
                'Content-Type': 'application/json; charset=utf-8', 'X-Xsrf-Token': `${token}`
            }
        }).then(res => {
            if (typeof res === `string`) {
                console.log(res);
                return false;
            }
            return res[Const.RES_OK] === 1;
        });
    }
}

class Block {
    static get(ID) {
        return Block.getList().then(list => {
            for (let i = 0; i < list.length; i++) {
                if (list[i] === id) {
                    return true;
                }
            }
            return false;
        });
    }

    static set(ID, username) {
        if (Domain.domain === Domain.WEIBO || Domain.domain === Domain.WEIBO_3W) {
            let data = JSON.stringify({
                'uid': `${ID}`,
                'screen_name': `${username}`,
                'status': 1,
                'interact': 0,
                'follow': 0
            });
            let token = Cookie.get('XSRF-TOKEN');

            let url;
            if (Domain.domain === Domain.WEIBO) {
                url = `https://weibo.com/ajax/statuses/filterUser`;
            } else {
                url = `https://www.weibo.com/ajax/statuses/`;
            }

            return $.ajax({
                url: url, type: 'POST', data: data, headers: {
                    'Content-Type': 'application/json; charset=utf-8', 'X-Xsrf-Token': `${token}`
                }
            }).then(async res => {
                if (typeof res === `string`) {
                    console.log(res);
                    return false;
                }
                return res[Const.RES_OK] === 1;
            });
        } else if (Domain.domain === Domain.WEIBO_S) {
            return GMHelper.addBlockItem(ID, username);
        }
    }

    static remove(ID, username) {
        if (Domain.domain === Domain.WEIBO || Domain.domain === Domain.WEIBO_3W) {
            let data = JSON.stringify({
                'uid': `${ID}`,
                'screen_name': `${username}`
            });
            let token = Cookie.get('XSRF-TOKEN');

            let url;
            if (Domain.domain === Domain.WEIBO) {
                url = `https://weibo.com/ajax/setting/deleteFilteredUser`;
            } else {
                url = `https://www.weibo.com/ajax/statuses/`;
            }

            return $.ajax({
                url: url, type: 'POST', data: data, headers: {
                    'Content-Type': 'application/json; charset=utf-8', 'X-Xsrf-Token': `${token}`
                }
            }).then(async res => {
                if (typeof res === `string`) {
                    console.log(res);
                    return false;
                }
                return res[Const.RES_OK] === 1;
            });
        } else if (Domain.domain === Domain.WEIBO_S) {
            return GMHelper.removeBlockItem(ID);
        }
    }

    static getList() {
        return GM_.get(Const.BLOCK_FULL_LIST_STORE).then(res => {
            return res.split(' ');
        });
    }

    static addFromServer() {
        function loadPage(page) {
            let token = Cookie.get('XSRF-TOKEN');
            let url = `https://weibo.com/ajax/setting/getFilteredUsers?page=${page}`;
            return $.ajax({
                url: url, type: 'GET', headers: {
                    'Content-Type': 'application/json; charset=utf-8', 'X-Xsrf-Token': `${token}`
                }
            }).then(async res => {
                if (typeof res === `string`) {
                    console.log(res);
                    return false;
                }
                return res;
            });
        }

        function getBlockListRec(list, page, total = 0, rememberTotal = true) {
            return new Promise(resolve => {
                loadPage(page).then(res => {
                    if (rememberTotal) {
                        total = res['total'];
                    }

                    res['card_group'].forEach(item => {
                        let scheme = item['scheme'];
                        let lastIndex = scheme.lastIndexOf('=');
                        let ID = scheme.substring(lastIndex + 1);
                        list.push(ID);
                    });

                    if (list.length < total) {
                        let random = Math.random() * (200 - 50) + 50;
                        let timeout = 300 + random;
                        setTimeout(() => {
                            getBlockListRec(list, page + 1, total, false).then(() => resolve());
                        }, timeout);
                    } else {
                        resolve();
                    }
                });
            });
        }

        if (Domain.domain === Domain.WEIBO) {
            let blockList = [];
            return getBlockListRec(blockList, 1).then(() => {
                Block.getList().then(list => {
                    let split = blockList.join(' ');
                    list = list.concat(split);
                    list = [...new Set(list)];
                    GM_.set(Const.BLOCK_FULL_LIST_STORE, list);
                    GM_.set(Const.BLOCK_FULL_LIST_STORE_LAST_UPDATE_DATE, new Date().toString());
                });
            });
        }
    }

    static logStore() {
        Block.getList().then(list => {
            GM_.get(Const.BLOCK_FULL_LIST_STORE_LAST_UPDATE_DATE).then(res => {
                console.log(`存储的用户屏蔽数量:${list.length},上次更新时间:${res}`);
            });
        });
    }
}

class GMHelper {
    static waitValue(name, expectedValue, timeout = 100, nullValue = expectedValue) {
        GM_.get(name, nullValue).then(res => {
            if (res === expectedValue) {
                return;
            }
            let inspection = setInterval(() => {
                GM_.get(name, nullValue).then(res => {
                    if (res === expectedValue) {
                        clearInterval(inspection);
                    }
                });
            }, timeout);
        });
    }

    static addBlockItem(ID, username) {
        GMHelper.waitValue(Const.LIST_TO_BE_BLOCK_UPDATING, 'false');
        GM_.set(Const.LIST_TO_BE_BLOCK_UPDATING, 'true');

        GM_.get(Const.LIST_TO_BE_BLOCK, '').then(res => {
            let split = res.split(';');
            for (let i = 0; i < split.length; i++) {
                let iID = split[i].split(' ')[0];
                if (iID === ID) {
                    GM_.set(Const.LIST_TO_BE_BLOCK_UPDATING, 'false');
                    return;
                }
            }
            let item = `${ID} ${username}`;
            split.push(item);
            let text = split.join(' ');
            GM_.set(Const.LIST_TO_BE_BLOCK, text);
            GM_.set(Const.LIST_TO_BE_BLOCK_UPDATING, 'false');
        });
        return Promise.resolve();
    }

    static removeBlockItem(ID) {
        GMHelper.waitValue(Const.LIST_TO_BE_BLOCK_UPDATING, 'false');
        GM_.set(Const.LIST_TO_BE_BLOCK_UPDATING, 'true');

        GM_.get(Const.LIST_TO_BE_BLOCK, '').then(res => {
            let split = res.split(';');
            split = split.filter(s => {
                let sID = s.split(' ')[0];
                return sID === ID;
            })
            let text = split.join(' ');
            GM_.set(Const.LIST_TO_BE_BLOCK, text);
            GM_.set(Const.LIST_TO_BE_BLOCK_UPDATING, 'false');
        });
    }
}

class GM_ {
    static async get(name, defaultV = '') {
        let value = await GM.getValue(name);
        if (value === undefined || value.trim() === '') {
            value = defaultV;
        }
        return Promise.resolve(value);
    }

    static set(name, value) {
        GM.setValue(name, value);
    }

    static registerMenuCommand(name, fun) {
        return GM_registerMenuCommand(name, fun);
    }

    static unregisterMenuCommand(name) {
        GM_unregisterMenuCommand(name)
    }
}

class Cookie {
    static get(name) {
        let cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            let cookie = cookies[i].trim();
            if (cookie.startsWith(name + '=')) {
                return cookie.substring(name.length + 1);
            }
        }
        return null;
    }
}

(function () {
    'use strict';

    GM_.get(Const.FAV_SELECT_TAGS, '无').then(selected => {
        menuSettingFav = GM_.registerMenuCommand(`设置收藏标签 当前:${selected}`, () => {
            Do.setFavTags();
        });
    });

    GM_.get(Const.BLOCK_SELECT_LEVEL, '不看微博').then(selected => {
        menuSettingBlock = GM_.registerMenuCommand(`设置屏蔽 当前:${selected}`, () => {
            Do.setBlockLevels();
        });
    });

    setTimeout(() => {
        document.addEventListener('DOMContentLoaded', function () {
            onScrollFlag = true;
        });
        window.onscroll = () => {
            onScrollFlag = true;
        }
        updateFavoriteButton();
        updateFavoriteButton2();
        listenRootBlock();

        GM_.get(Const.BLOCK_FULL_LIST_STORE_LAST_UPDATE_DATE, '').then(lastDate => {
            Block.logStore();
            if (lastDate !== '') {
                let time_diff = new Date() - new Date(lastDate);
                time_diff = TimeConvert.toDays(time_diff);
                if (time_diff <= 1) {
                    return;
                }
            }
            // Block.addFromServer().then(() => Block.logStore());
        });
    }, 2000);
})();

function updateFavoriteButton() {
    onScrollFlag = true;
    setInterval(() => {
        if (onScrollFlag) {
            for (let [articleNode, config] of blockConfig) {
                function isInViewPortOfOne(el) {
                    let viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight

                    let screenTop = document.documentElement.scrollTop
                    let screenBottom = screenTop + viewPortHeight

                    let bounding = el.getBoundingClientRect();
                    let top = screenTop + bounding.top;
                    let bottom = bounding.bottom;

                    return screenTop <= top && top <= screenBottom
                }

                let isVisible = isInViewPortOfOne(articleNode)
                if (isVisible) {
                    needUpdateNodeList.push(articleNode);
                } else {
                    needUpdateNodeList = needUpdateNodeList.filter(item => item !== articleNode);
                }
            }
            onScrollFlag = false;
        }
    }, 100);
}

function updateFavoriteButton2() {
    setInterval(() => {
        let articleNode = needUpdateNodeList.pop();
        if (articleNode) {
            let res = updateFavoriteButton3(articleNode);
            if (!res) {
                needUpdateNodeList.push(articleNode);
            }
            let forwardNode = articleNode.querySelector(Selector.forwardNodeSelector);
            if (forwardNode != null) {
                updateFavoriteButton3(forwardNode);
            }
        }
    }, 100);
}

function updateFavoriteButton3(articleNode) {
    let blogID = BlogView.getBlogID(articleNode);

    let isLoading = BlogCache.get(blogID, Const.IS_LOADING);
    if (isLoading) {
        return false;
    }

    let buttonNode = articleNode.querySelector(`button[class*="${ClassName.buttonFavorite}"]`);

    if (blogCaches.has(blogID)) {

        if (Domain.domain === Domain.WEIBO || Domain.domain === Domain.WEIBO_3W) {
            let lastUpdate = BlogCache.get(blogID, Const.LAST_UPDATED);
            if (lastUpdate) {
                let time_diff = new Date() - new Date(lastUpdate);
                time_diff = TimeConvert.toHours(time_diff);

                // Greater than 1 hour
                if (time_diff > 1) {
                    Favorite.get(blogID).then(() => {
                        updateButtonText(blogID, buttonNode)
                    });
                } else {
                    updateButtonText(blogID, buttonNode);
                }
            } else {
                Favorite.get(blogID).then(() => {
                    updateButtonText(blogID, buttonNode)
                });
            }

        } else if (Domain.domain === Domain.WEIBO_S) {
            // s.weibo.com do not update status, due to lack of API support
            updateButtonText(blogID, buttonNode);
        }

    } else {
        Favorite.get(blogID).then(() => {
            updateButtonText(blogID, buttonNode)
        });
    }

    let likeButton = Locate.likeButton(articleNode)
    likeButton.parentElement.onclick = () => {
        likeButton.click();
    }

    return true;
}

function listenRootBlock() {
    setInterval(() => {
        let rootNode = document.querySelector(`div[class*="${Selector.rootNodeClass}"]`);
        if (rootNode == null) {
            return;
        }

        let isLoadEvent = rootNode.getAttribute('data_a656_is_load_event');
        if (isLoadEvent != null) {
            return;
        }

        rootNode.setAttribute('data_a656_is_load_event', true.toString());

        if (Domain.domain === Domain.WEIBO || Domain.domain === Domain.WEIBO_3W) {

            new MutationObserver((mutationsLi) => {
                for (let mutations of mutationsLi) {
                    if (mutations.type === 'childList') {
                        mutations.addedNodes.forEach(node => {
                            if (node.nodeName === '#text' || node.nodeName === '#comment') {
                                return;
                            }

                            let articleNode = Locate.articleNode(node, 'default');
                            if (articleNode == null) {
                                return;
                            }

                            if (blockConfig.get(articleNode) == null) {
                                blockConfig.set(articleNode, {});
                            }

                            placeButtons(articleNode);
                        })
                    }
                }
            }).observe(rootNode, {childList: true, subtree: true});
            onScrollFlag = true;

            let postList = rootNode.querySelectorAll(`article[class*="Feed_wrap"]`);
            postList.forEach(articleNode => {
                if (!blockConfig.has(articleNode)) {
                    blockConfig.set(articleNode, {});
                }

                let blogID = BlogView.getBlogID(articleNode);
                Favorite.get(blogID);
                placeButtons(articleNode)
            });

        } else if (Domain.domain === Domain.WEIBO_S) {
            let postList = rootNode.querySelectorAll(`div[class="${Selector.postNodeFullClass}"]`);
            onScrollFlag = true;

            postList = Array.from(postList).filter(function (div) {
                return !div.querySelector('div.wb-ad-tile');
            });

            postList.forEach(articleNode => {
                if (!blockConfig.has(articleNode)) {
                    blockConfig.set(articleNode, {});
                }

                let blogID = BlogView.getBlogID(articleNode);

                let id = BlogView.getBlogIDNum(articleNode);
                BlogCache.set(blogID, Const.ID, id);

                Favorite.get(blogID);
                placeButtons(articleNode)
            });
        }

    }, 500);
}

function placeButtons(node) {
    if (node == null) {
        return;
    }
    if (node.getAttribute('data_a656_value1') === 'true') {
        return;
    }

    node.setAttribute('data_a656_value1', true.toString());

    let favoriteButtonNode = Button.createFavorite();
    let openButton = Button.createOpen();
    let blockButton = Button.createBlock();

    let targetNode = node.querySelector(Selector.buttonLocateSelector);

    if (Domain.domain === Domain.WEIBO || Domain.domain === Domain.WEIBO_3W) {
        targetNode.parentNode.insertBefore(favoriteButtonNode, targetNode.nextSibling);
        targetNode.parentNode.insertBefore(openButton, targetNode.nextSibling);
        if (Domain.domain === Domain.WEIBO) {
            targetNode.parentNode.insertBefore(blockButton, targetNode.nextSibling);
        }
    } else if (Domain.domain === Domain.WEIBO_S) {
        let wrap = document.createElement('div');
        wrap.style.position = 'absolute';
        wrap.style.top = '-10px';
        wrap.style.right = '18px';
        wrap.style.width = '300px';
        wrap.style.display = 'flex';
        wrap.style.flexDirection = 'row-reverse';

        wrap.appendChild(favoriteButtonNode);
        wrap.appendChild(openButton);
        // wrap.appendChild(blockButton);
        targetNode.appendChild(wrap);
    }

    let forwardNode = node.querySelector(Selector.forwardNodeSelector);
    if (forwardNode != null) {
        let favoriteButtonNode = Button.createFavorite('forward');
        let openButton = Button.createOpen('forward');
        let blockButton = Button.createBlock('forward');

        let targetNode = forwardNode.querySelector('a').parentNode;
        if (Domain.domain === Domain.WEIBO || Domain.domain === Domain.WEIBO_3W) {
            openButton.style.marginLeft = 'auto';
            favoriteButtonNode.style.marginRight = '26px';
            targetNode.appendChild(blockButton);
            targetNode.appendChild(openButton);
            targetNode.appendChild(favoriteButtonNode);
        }
    }
}

function updateButtonText(blogID, buttonNode) {
    if (BlogCache.get(blogID, Const.IS_FAVORITE)) {
        buttonNode.innerText = '取消收藏';
    } else {
        buttonNode.innerText = '收藏';
    }
}

function toast(msg, duration) {
    duration = isNaN(duration) ? 3000 : duration;
    let m = document.createElement('div');
    m.innerHTML = msg;

    m.style.setProperty('font-size', '20px', 'important');
    m.style.setProperty('color', 'rgb(255, 255, 255)', 'important');
    m.style.setProperty('background-color', 'rgba(0,0,0,0.6)', 'important');
    m.style.setProperty('border-style', 'solid', 'important');
    m.style.setProperty('border-color', '#ffffff', 'important');
    m.style.setProperty('z-index', '256', 'important');

    m.style.cssText = 'font-size: 20px; ' + 'color: rgb(255, 255, 255); ' + 'background-color: rgba(0,0,0,0.6); ' + 'border-style: solid; ' + 'border-color: #ffffff; ' + 'z-index: 256; ' + 'padding: 10px 15px; ' + 'margin: 0 0 0 -60px; ' + 'border-radius: 4px; ' + 'position: fixed; ' + 'top: 50%; ' + 'left: 50%; ' + 'width: 130px; ' + 'text-align: center;';

    document.body.appendChild(m);
    setTimeout(function () {
        let d = 0.5;
        m.style.opacity = '0';
        setTimeout(function () {
            document.body.removeChild(m)
        }, d * 1000);
    }, duration);
}