// ==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);
}