在微博网页端,为每个帖子创建一个收藏和新页面打开按钮。
当前为 
// ==UserScript==
// @name         微博帖子一键收藏、新页面打开
// @namespace    http://tampermonkey.net/
// @version      20240905
// @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        none
// @require      https://code.jquery.com/jquery-3.5.1.min.js
// ==/UserScript==
/* globals jQuery, $, waitForKeyElements */
let blockConfig = new Map();
let onScrollFlag = false;
let needUpdateNodeList = [];
const buttonClassName = 'button_a656';
const buttonFavoriteClassName = 'button_a656_favorite';
const buttonOpenNewTabClassName = 'button_a656_open_new_tab';
let settingButtonSelector;
let rootNodeClass;
let postNodeFullClass;
let buttonLocateSelector;
let timeNodeSector = ``;
let forwardNodeStartClass;
let forwardNodeSelector;
let buttonClassList;
let blogCaches = new Map();
let domain;
const domainWeibo = 'weibo.com';
const domainSWeibo = 's.weibo.com';
const domain3WWeibo = 'www.weibo.com';
const CONST_ID = 'ID';
const CONST_BLOG_ID = 'blogID';
const CONST_IS_FAVORITE = 'isFavorites';
const CONST_LAST_UPDATED = 'lastUpdated';
const CONST_IS_LOADING = 'isLoading';
const CONST_RES_OK = 'ok';
const CONST_RES_CODE = 'code';
const promptTimeMs = 1000;
(function () {
    'use strict';
    domain = location.hostname;
    settingButtonSelector = 'button[title="设置"]'
    if (domain === domainWeibo || domain === domain3WWeibo) {
        rootNodeClass = 'vue-recycle-scroller__item-wrapper';
        rootNodeClass = 'Main_full';
        postNodeFullClass = `Feed_wrap`;
        buttonLocateSelector = 'div[class*="head_main"]';
        timeNodeSector = `a[class^="head-info_time"]`;
        forwardNodeStartClass = 'retweet Feed_retweet'
        forwardNodeSelector = 'div.retweet[class*="Feed_retweet"]'
    } else if (domain === domainSWeibo) {
        rootNodeClass = 'main-full';
        postNodeFullClass = 'card';
        // buttonLocateSelector = 'div.menu.s-fr > a';
        buttonLocateSelector = 'div.from > a:last-child';
        timeNodeSector = `div.from > a:first-child`;
    } else {
        return;
    }
    let settingButton = document.querySelector(settingButtonSelector)
    let classList = settingButton.classList
    buttonClassList = Array.from(classList).filter(
        className => className.startsWith('IconBox_')
    );
    setTimeout(() => {
        document.addEventListener('DOMContentLoaded', function () {
            onScrollFlag = true;
        });
        window.onscroll = () => {
            onScrollFlag = true;
        }
        updateFavoriteButton();
        updateFavoriteButton2();
        listenRootBlock();
    }, 2000);
})();
function updateFavoriteButton() {
    onScrollFlag = true;
    setInterval(() => {
        if (onScrollFlag) {
            for (let [articleNode, config] of blockConfig) {
                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) {
            updateFavoriteButton3(articleNode);
            let forwardNode = articleNode.querySelector(forwardNodeSelector);
            if (forwardNode != null) {
                updateFavoriteButton3(forwardNode);
            }
        }
    }, 100);
}
function updateFavoriteButton3(articleNode) {
    let blogID = getBlogID(articleNode);
    let isLoading = getBlogCacheValue(blogID, CONST_IS_LOADING);
    if (isLoading) {
        return;
    }
    let buttonNode = articleNode.querySelector(`button[class*="${buttonFavoriteClassName}"]`);
    if (blogCaches.has(blogID)) {
        if (domain === domainWeibo || domain === domain3WWeibo) {
            let lastUpdate = getBlogCacheValue(blogID, CONST_LAST_UPDATED);
            if (lastUpdate) {
                let time_diff = new Date() - new Date(lastUpdate);
                time_diff /= 1000;
                // Greater than 60 seconds
                // if (time_diff > 60) {
                if (time_diff > 1e50) {
                    getFavoriteStatus(blogID).then(() => {
                        updateButtonText(blogID, buttonNode)
                    });
                } else {
                    updateButtonText(blogID, buttonNode);
                }
            } else {
                getFavoriteStatus(blogID).then(() => {
                    updateButtonText(blogID, buttonNode)
                });
            }
        } else if (domain === domainSWeibo) {
            // s.weibo.com do not update status, due to lack of API support
            updateButtonText(blogID, buttonNode);
        }
    } else {
        getFavoriteStatus(blogID).then(() => {
            updateButtonText(blogID, buttonNode)
        });
    }
}
function listenRootBlock() {
    setInterval(() => {
        let rootNode = document.querySelector(`div[class*="${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 === domainWeibo || domain === domain3WWeibo) {
            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 = getArticleNode(node, 'default');
                            if (articleNode == null) {
                                return;
                            }
                            if (blockConfig.get(articleNode) == null) {
                                blockConfig.set(articleNode, {});
                            }
                            placeFavoriteButton(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 = getBlogID(articleNode);
                getFavoriteStatus(blogID);
                placeFavoriteButton(articleNode)
            });
        } else if (domain === domainSWeibo) {
            let postList = rootNode.querySelectorAll(`div[class="${postNodeFullClass}"]`);
            onScrollFlag = true;
            postList.forEach(articleNode => {
                if (!blockConfig.has(articleNode)) {
                    blockConfig.set(articleNode, {});
                }
                let blogID = getBlogID(articleNode);
                let id = getBlogIDNum(articleNode);
                setBlogCaches(blogID, CONST_ID, id);
                getFavoriteStatus(blogID);
                placeFavoriteButton(articleNode)
            });
        }
    }, 500);
}
function placeFavoriteButton(node) {
    if (node == null) {
        return;
    }
    if (node.getAttribute('data_a656_value1') === 'true') {
        return;
    }
    node.setAttribute('data_a656_value1', true.toString());
    let favoriteButtonNode = createFavoriteButton();
    let openButton = createOpenButton();
    let targetNode = node.querySelector(buttonLocateSelector);
    if (domain === domainWeibo || domain === domain3WWeibo) {
        targetNode.parentNode.insertBefore(favoriteButtonNode, targetNode.nextSibling);
        targetNode.parentNode.insertBefore(openButton, targetNode.nextSibling);
    } else if (domain === domainSWeibo) {
        favoriteButtonNode.style.position = 'absolute';
        favoriteButtonNode.style.top = '10px';
        favoriteButtonNode.style.right = '40px';
        openButton.style.position = 'absolute';
        openButton.style.top = '10px';
        openButton.style.right = '100px';
        openButton.style.marginRight = '0px';
        targetNode.parentNode.insertBefore(favoriteButtonNode, targetNode.nextSibling);
        targetNode.parentNode.insertBefore(openButton, targetNode.nextSibling);
    }
    let forwardNode = node.querySelector(forwardNodeSelector);
    if (forwardNode != null) {
        let favoriteButtonNode = createFavoriteButton('forward');
        let openButton = createOpenButton('forward');
        let targetNode = forwardNode.querySelector('a').parentNode;
        if (domain === domainWeibo || domain === domain3WWeibo) {
            openButton.style.marginLeft = 'auto';
            favoriteButtonNode.style.marginRight = '26px';
            targetNode.appendChild(openButton);
            targetNode.appendChild(favoriteButtonNode);
        }
    }
}
function createFavoriteButton(type = 'default') {
    let buttonNode = document.createElement('button');
    buttonNode.classList.add(buttonClassName);
    buttonNode.classList.add(buttonFavoriteClassName);
    addBaseClass(buttonNode);
    buttonNode.style.width = '50px';
    buttonNode.style.height = '28px';
    buttonNode.style.marginLeft = '0px';
    buttonNode.style.marginRight = '5px';
    buttonNode.style.padding = '0';
    buttonNode.addEventListener('click', ev => {
        let buttonNode = ev.target;
        let articleNode = getArticleNode(buttonNode, type);
        let blogID = getBlogID(articleNode);
        let ID = getBlogCacheValue(blogID, CONST_ID)
        let isFavorites = getBlogCacheValue(blogID, CONST_IS_FAVORITE)
        let error = false;
        if (isFavorites) {
            removeFavorite(ID).then(res => {
                if (res) {
                    setBlogCaches(blogID, CONST_IS_FAVORITE, false);
                    buttonNode.innerText = `已取消收藏`;
                    setTimeout(() => {
                        buttonNode.innerText = `收藏`;
                    }, promptTimeMs);
                } else {
                    buttonNode.innerText = `取消收藏失败`;
                    error = true;
                }
            });
        } else {
            setFavorite(ID).then(res => {
                if (res) {
                    setBlogCaches(blogID, CONST_IS_FAVORITE, true);
                    buttonNode.innerText = `已收藏`;
                    setTimeout(() => {
                        buttonNode.innerText = `取消收藏`;
                    }, promptTimeMs);
                } else {
                    buttonNode.innerText = `收藏失败`;
                    error = true;
                }
            })
        }
    });
    return buttonNode;
}
function createOpenButton(type = 'default') {
    let buttonNode = document.createElement('button');
    buttonNode.textContent = '新页面打开';
    buttonNode.classList.add(buttonClassName);
    buttonNode.classList.add(buttonOpenNewTabClassName);
    addBaseClass(buttonNode);
    buttonNode.setAttribute('href', '#woo_svg_nav_sun');
    buttonNode.style.width = '90px';
    buttonNode.style.height = '28px';
    buttonNode.style.marginRight = '5px';
    buttonNode.style.padding = '0';
    buttonNode.addEventListener('click', ev => {
        let buttonNode = ev.target;
        let articleNode = getArticleNode(buttonNode, type);
        let link = articleNode.querySelector(timeNodeSector).href;
        window.open(link);
    });
    return buttonNode;
}
function addBaseClass(node) {
    if (buttonClassList == null) {
        return;
    }
    for (let cl of buttonClassList) {
        node.classList.add(cl);
    }
}
function getBlogID(article_node) {
    let time_a_node = article_node.querySelector(timeNodeSector);
    let url = time_a_node.href;
    let index = url.lastIndexOf('/');
    return url.substring(index + 1);
}
function getBlogIDNum(article_node) {
    if (domain === domainSWeibo) {
        let node = article_node;
        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;
}
function getFavoriteStatus(blogID) {
    setBlogCaches(blogID, CONST_IS_LOADING, true);
    if (domain === domainWeibo || domain === domain3WWeibo) {
        let url;
        if (domain === domainWeibo) {
            url = `https://weibo.com/ajax/statuses/show?id=${blogID}`;
        } else {
            url = `https://www.weibo.com/ajax/statuses/show?id=${blogID}`;
        }
        return $.ajax({
            url: url, type: 'GET',
        }).then(res => {
            setBlogCaches(blogID, CONST_ID, res.id);
            setBlogCaches(blogID, CONST_BLOG_ID, blogID);
            setBlogCaches(blogID, CONST_IS_FAVORITE, res.favorited);
            setBlogCaches(blogID, CONST_IS_LOADING, false);
        });
    } else if (domain === domainSWeibo) {
        return new Promise(() => {
            setBlogCaches(blogID, CONST_BLOG_ID, blogID);
            setBlogCaches(blogID, CONST_IS_FAVORITE, false);
            setBlogCaches(blogID, CONST_IS_LOADING, false);
        })
    }
}
function setFavorite(ID) {
    if (domain === domainWeibo || domain === domain3WWeibo) {
        let data = JSON.stringify({'id': `${ID}`});
        let token = getCookie('XSRF-TOKEN');
        let url;
        if (domain === domainWeibo) {
            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(res => {
            if (typeof res === `string`) {
                return false;
            }
            return res[CONST_RES_OK] === 1;
        });
    } else if (domain === domainSWeibo) {
        let data = {
            'mid': `${ID}`
        };
        return $.ajax({
            url: `https://s.weibo.com/ajax_Mblog/favAdd`, type: 'POST', data: data
        }).then(res => {
            if (typeof res === `string`) {
                return false;
            }
            return res[CONST_RES_CODE] === `100000`;
        });
    }
}
function removeFavorite(ID) {
    if (domain === domainWeibo || domain === domain3WWeibo) {
        let data = JSON.stringify({'id': `${ID}`});
        let token = getCookie('XSRF-TOKEN');
        let url;
        if (domain === domainWeibo) {
            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`) {
                return false;
            }
            return res[CONST_RES_OK] === 1;
        });
    } else if (domain === domainSWeibo) {
        let data = {
            'mid': `${ID}`
        };
        return $.ajax({
            url: `https://s.weibo.com/ajax_Mblog/favDel`, type: 'POST', data: data
        }).then(res => {
            if (typeof res === `string`) {
                return false;
            }
            return res[CONST_RES_CODE] === `100000`;
        });
    }
}
function getBlogCacheValue(blogID, key) {
    if (!blogCaches.has(blogID)) {
        let blogCache = {};
        blogCaches.set(blogID, blogCache);
    }
    return blogCaches.get(blogID)[key];
}
function setBlogCaches(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);
}
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
}
function getArticleNode(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(postNodeFullClass) >= 0) {
                return node;
            }
        } else if (type === 'forward') {
            if (node.className.indexOf(forwardNodeStartClass) >= 0) {
                return node;
            }
        }
        node = node.parentNode;
    }
    return null;
}
function updateButtonText(blogID, buttonNode) {
    let text;
    if (getBlogCacheValue(blogID, CONST_IS_FAVORITE)) {
        text = '取消收藏'
    } else {
        text = '收藏'
    }
    buttonNode.innerText = text;
}
function getCookie(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 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 () {
        var d = 0.5;
        m.style.opacity = '0';
        setTimeout(function () {
            document.body.removeChild(m)
        }, d * 1000);
    }, duration);
}