Greasy Fork is available in English.

微博帖子一键收藏

在微博网页端,为每个帖子创建一个收藏按钮,来试图解决帖子收藏的成本较高的问题。

// ==UserScript==
// @name         微博帖子一键收藏
// @namespace    http://tampermonkey.net/
// @version      20240408.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        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 rootNodeClass;
let postNodeFullClass;
let buttonLocateSelector;
let timeNodeSector = ``;

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;
    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"]`;
    } 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;
    }

    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) {
            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)
                });
            }
        }
    }, 100);
}

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) {

            rootNode.addEventListener('DOMNodeInserted', ev => {
                if (ev.target.nodeName === '#text' || ev.target.nodeName === '#comment') {
                    return;
                }

                let articleNode = getArticleNode(ev.target);
                if (articleNode == null) {
                    return;
                }

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

                placeFavoriteButton(articleNode);
            });
            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 = '110px';

        targetNode.parentNode.insertBefore(favoriteButtonNode, targetNode.nextSibling);
        targetNode.parentNode.insertBefore(openButton, targetNode.nextSibling);
    }
}

function createFavoriteButton() {
    let buttonNode = document.createElement('button');

    buttonNode.classList.add(buttonClassName);
    buttonNode.classList.add(buttonFavoriteClassName);
    addBaseClass(buttonNode);

    buttonNode.style.width = '64px';
    buttonNode.style.height = '28px';
    buttonNode.style.marginRight = '5px';
    buttonNode.style.padding = '0';

    buttonNode.addEventListener('click', ev => {
        let buttonNode = ev.target;
        let articleNode = getArticleNode(buttonNode);
        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() {
    let buttonNode = document.createElement('button');
    buttonNode.textContent = '新页面打开';

    buttonNode.classList.add(buttonClassName);
    buttonNode.classList.add(buttonOpenNewTabClassName);
    addBaseClass(buttonNode);

    buttonNode.style.width = '70px';
    buttonNode.style.height = '28px';
    buttonNode.style.marginRight = '5px';
    buttonNode.style.padding = '0';

    buttonNode.addEventListener('click', ev => {
        let buttonNode = ev.target;
        let articleNode = getArticleNode(buttonNode);
        let link = articleNode.querySelector(timeNodeSector).href;
        window.open(link);
    });
    return buttonNode;
}

function addBaseClass(node) {
    node.classList.add(`woo-button-main`);
    node.classList.add(`woo-button-line`);
    node.classList.add(`woo-button-primary`);
    node.classList.add(`woo-button-s`);
    node.classList.add(`woo-button-round`);
}

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) {
    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 (node.className.indexOf(postNodeFullClass) >= 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);
}