// ==UserScript==
// @name 微博帖子一键收藏、新页面打开
// @namespace http://tampermonkey.net/
// @version 20240909
// @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 = [];
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 CONST_FAV_TOTAL_FAV = 'fav_total_num';
const CONST_FAV_TAGS = 'tags';
const CONST_FAV_TAGS_TAG = 'tag';
const CONST_FAV_TAGS_COUNT = 'count';
const CONST_FAV_TOTAL_TAG = 'total_number';
const CONST_FAV_SELECT_TAGS = 'CONST_FAV_SELECT_TAGS';
const promptTimeMs = 1000;
let menuSettingFav;
(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';
// 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_'));
getFavSelected().then(selected => {
if (selected === '') {
selected = '无';
}
menuSettingFav = GM_registerMenuCommand(`设置收藏标签 当前:${selected}`, () => {
setFavTags();
});
});
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 res = updateFavoriteButton3(articleNode);
if (!res) {
needUpdateNodeList.push(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 false;
}
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)
});
}
let likeButton = locateLikeButton(articleNode)
likeButton.parentElement.onclick = () => {
likeButton.click();
}
return true;
}
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) {
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);
targetNode.appendChild(wrap);
}
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', like = false) {
let buttonNode = document.createElement('button');
buttonNode.classList.add(buttonClassName);
buttonNode.classList.add(buttonFavoriteClassName);
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 = 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 = 'auto';
buttonNode.style.minWidth = '46.667px';
buttonNode.style.height = '28px';
buttonNode.style.marginRight = '5px';
buttonNode.style.paddingLeft = '10px';
buttonNode.style.paddingRight = '10px';
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 locateLikeButton(article_node) {
return article_node.querySelector('button[title="赞"]')
}
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(async res => {
if (typeof res === `string`) {
return false;
}
return getFavSelected().then(tags => {
console.log(tags);
if (tags !== '') {
tags = tags.split(' ');
tags = tags.join(',');
updateFavoriteTags(ID, tags);
}
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 updateFavoriteTags(ID, tags) {
let data = JSON.stringify({'id': `${ID}`, 'tags': `${tags}`});
let token = getCookie('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 => {
console.log(res);
if (typeof res === `string`) {
return false;
}
return res[CONST_RES_OK] === 1;
});
}
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 setFavTags() {
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');
getFavSelected().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 === '') {
console.log('空输入');
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.setValue(CONST_FAV_SELECT_TAGS, nameTmp);
GM_unregisterMenuCommand(menuSettingFav);
if (nameTmp === '') {
nameTmp = '无';
}
GM_registerMenuCommand(`设置收藏标签 当前:${nameTmp}`, () => {
setFavTags();
});
console.log(currentMenuStr);
})
} else {
alert('获取标签列表失败');
}
})
}
function getFavTagInfo() {
let url = 'https://weibo.com/ajax/favorites/tags?is_show_total=1';
let token = getCookie('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;
});
}
async function getFavSelected() {
let selected = await GM.getValue(CONST_FAV_SELECT_TAGS);
if (selected === undefined || selected === '') {
selected = '';
}
return Promise.resolve(selected);
}
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) {
if (getBlogCacheValue(blogID, CONST_IS_FAVORITE)) {
buttonNode.innerText = '取消收藏';
} else {
buttonNode.innerText = '收藏';
}
}
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);
}