// ==UserScript==
// @name shire helper
// @namespace https://greasyfork.org/zh-CN/scripts/461311-shire-helper
// @version 1.0.12.3
// @description Download shire thread content.
// @author 80824
// @match https://www.shireyishunjian.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=shireyishunjian.com
// @grant unsafeWindow
// @grant GM.getValue
// @grant GM_getValue
// @grant GM.setValue
// @grant GM_setValue
// @grant GM.deleteValue
// @grant GM_deleteValue
// @grant GM.listValues
// @grant GM_listValues
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM.download
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/loglevel/1.9.2/loglevel.min.js
// ==/UserScript==
(function () {
'use strict';
// ========================================================================================================
// 高频工具
// ========================================================================================================
const qS = (selector, scope = document) => scope.querySelector(selector);
const qSA = (selector, scope = document) => scope.querySelectorAll(selector);
const docre = tag => document.createElement(tag);
String.prototype.parseURL = function () {
const url = new URL(this);
let obj = { hostname: url.hostname, hash: url.hash, pathname: url.pathname };
obj.loc = obj.pathname?.match(/([^\/]+)\.[^\.]*/)?.at(1);
for (let [k, v] of url.searchParams.entries()) {
obj[k] = v;
}
return obj;
};
// ========================================================================================================
// 初始化设置
// ========================================================================================================
const helper_default_setting = {
// 消息通知设置
enable_notification: true,
max_noti_threads: 3,
important_fids: ['78'],
// 历史记录设置
enable_history: true,
max_history: 100,
// 下载设置
enable_text_download: true,
enable_postfile_download: true,
enable_attach_download: true,
enable_op_download: true,
files_pack_mode: 'all',
default_merge_mode: 'main',
// 自动回复设置
enable_auto_reply: false,
auto_reply_message: '感谢分享!',
// 自动换行设置
enable_auto_wrap: false,
min_wrap_length: 100,
typical_wrap_length: 200,
max_wrap_length: 300,
wrap_dot: '.。??!!',
wrap_comma: ',,、;;',
// 代表作设置
data_cache_time: 168 * 3600 * 1000, // 7天
masterpiece_num: 10,
default_masterpiece_sort: 'view',
// 屏蔽词设置
enable_block_keyword: false,
block_keywords: [],
// 黑名单设置
enable_blacklist: false,
blacklist: [],
};
const hs = GM_getValue('helper_setting', {});
let default_updated = false;
for (let key in helper_default_setting) {
if (!(key in hs)) {
hs[key] = helper_default_setting[key];
log.info(`Setting ${key} not found, set to default value.`);
default_updated = true;
}
}
if (default_updated) {
GM.setValue('helper_setting', hs);
}
// ========================================================================================================
// 判断脚本启用条件
// ========================================================================================================
const location_params = document.URL.parseURL();
log.log('Location params:', location_params);
const is_desktop = location_params.mobile == 'no' || Array.from(qSA('meta')).some(meta => meta.getAttribute('http-equiv') === 'X-UA-Compatible');
if (location_params.loc != undefined && !is_desktop) {
log.info('Mobile version detected, shire-helper disabled.');
return;
}
// ========================================================================================================
// 常量
// ========================================================================================================
const mobileUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1 Edg/125.0.0.0';
const large_page_num = 1024;
const magic_num = Math.sqrt(large_page_num);
const MIME_type_map = {
'image/jpeg': 'jpg',
'image/bmp': 'bmp',
'image/png': 'png',
'image/gif': 'gif',
'image/webp': 'webp',
'video/mp4': 'mp4',
'video/x-msvideo': 'avi',
'video/x-matroska': 'mkv',
'video/x-flv': 'flv',
'video/mpeg': 'mpg',
'video/quicktime': 'mov',
'audio/mp3': 'mp3',
'audio/mpeg': 'mp3',
'audio/wav': 'wav',
'audio/wave': 'wav',
'audio/x-wav': 'wav',
'text/plain': 'txt'
};
// ========================================================================================================
// 通用工具
// ========================================================================================================
const decimalCeil = (num, precision = 4) => Math.ceil(num * Math.pow(10, precision)) / Math.pow(10, precision);
const commonPrefix = ((str1, str2) => {
[str1, str2] = [str1, str2].map(str => str.replaceAll(/[\s\n\r]/g, '').replaceAll(/[(《\[\{\(\<]/g, '【').replaceAll(/[)》\]\}\)\>]/g, '】'));
return str1 === '' ? str2 : (str2 === '' ? str1 : (() => {
let i = 0;
while (i < str1.length && i < str2.length && str1[i] === str2[i]) i++;
return str1.slice(0, i);
})());
});
const startWithChinese = str => /^[\p{Script=Han}]/u.test(str);
const extractFileAndExt = str => {
const exts = Array.from(Object.values(MIME_type_map)).join('|');
const regex = new RegExp(`([^\\/]+)\\.(${exts})$`, 'i');
const match = str.match(regex);
return match ? [match[1], match[2]] : [str, ''];
};
const timeAgo = timestamp => {
const diff = Date.now() - timestamp;
const units = [
{ info: '年前', dt: 365 * 24 * 60 * 60 * 1000 },
{ info: '个月前', dt: 30 * 24 * 60 * 60 * 1000 },
{ info: '天前', dt: 24 * 60 * 60 * 1000 },
// { info: '小时前', dt: 60 * 60 * 1000 },
// { info: '分钟前', dt: 60 * 1000, bound: 5 * 60 * 1000 }
];
const unit = units.find(({ dt, bound }) => diff >= bound ?? dt); // 如果有bound则以bound为界限,否则以dt为界限
return unit ? `${Math.floor(diff / unit.dt)}${unit.info}` : '今天内';
};
const checkVariableDefined = (variable_name, timeout = 15000, time_interval = 100) => new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
if (typeof unsafeWindow[variable_name] !== 'undefined') {
resolve(unsafeWindow[variable_name]);
} else if (Date.now() - startTime >= timeout) {
reject(new Error(`Check ${variable_name} timeout exceeded`));
} else {
setTimeout(check, time_interval);
}
}
check();
});
const executeIfLoctionMatch = (func, params) => {
const matchFunc = ([k, v]) => {
const loc_v = location_params[k] == '' ? undefined : location_params[k];
return Array.isArray(v) ? v.includes(loc_v) : v == loc_v
};
if (Object.entries(params).every(matchFunc)) {
func();
}
};
// ========================================================================================================
// GM Value 工具
// ========================================================================================================
function updateListElements(list, elem, status, equal = (a, b) => a == b) {
// 根据equal判断独立elem,根据status判断新list中是否有elem
if (status && !list.some(e => equal(e, elem))) { // 存入元素
list.push(elem);
}
if (list.some(e => equal(e, elem))) {
const new_list = list.filter(e => !equal(e, elem)); // 删除元素
list.length = 0;
list.push(...new_list);
if (status) {
list.push(elem); // 更新元素
}
}
return list;
}
function updateGMList(list_name, list) {
if (list.length == 0) {
GM.deleteValue(list_name);
}
else {
GM.setValue(list_name, list);
}
}
// ========================================================================================================
// 自定义表情
// ========================================================================================================
// const original_smilies_types = ['4'];
// const new_smilies = [];
// Element:{name:name, type:type, path:path, info:[[id, smile_code, file_name, width, height, weight]]}
// Test images: 'data/attachment/album/202207/04/192158kg0urgxtw2805yrs.png','static/image/smiley/ali/1love1.gif',''https://p.upyun.com/demo/webp/webp/animated-gif-0.webp'
// ========================================================================================================
// 自定义样式
// ========================================================================================================
// 设置弹窗框架
GM_addStyle(`
#helper-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50%;
min-width: 600px;
min-height: 300px;
max-height: 85%;
background-color: white;
color: black !important;
border: 1px solid #ccc;
z-index: 2000;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
}
#helper-title-container {
display: flex;
align-items: center;
padding: 20px;
font-size: 1.5rem;
font-weight: bold;
text-align: left;
border-bottom: 1px solid #ccc;
}
#helper-title {
flex: 1;
}
#helper-content-container {
display: flex;
flex: 1;
overflow: hidden;
background-color: inherit;
}
#helper-tab-btn-container {
display: flex;
flex-direction: column;
flex-shrink: 0;
}
#helper-tab-content-container {
flex: 1;
padding: 10px;
font-size: 0.75rem;
}`);
// 消息弹窗
GM_addStyle(`
#helper-notification-popup {
position: fixed;
bottom: 20px;
right: 20px;
width: 35%;
min-width: 300px;
max-height: 80%;
background-color: rgba(0, 0, 0, 0.9);
color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
z-index: 10000;
}
.helper-noti-message {
width: 100%;
overflow: hidden;
white-space: nowrap;
}`);
// 弹窗关闭按钮
GM_addStyle(`
.helper-close-btn {
border: none;
cursor: pointer;
margin-left: 10px;
border-radius: 50%;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
transition: background-color 0.3s;
}
.helper-close-btn {
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>');
}
.helper-close-btn.helper-redx {
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>');
}
.helper-close-btn:hover {
background-color: #ddd;
}`);
// 蒙版与加载动画
GM_addStyle(`
#helper-loading-overlay{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 3000;
}
#helper-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#helper-overlay.helper-redirect-layer{
cursor: pointer;
background-color: transparent;
}
#helper-overlay.helper-shield-layer{
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.helper-spinner {
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}`);
// 顶部进度条
GM_addStyle(`
#helper-top-progressbar-container {
position: fixed;
top: 0;
width: 100%;
background-color: #eee;
z-index: 3000;
}
#helper-top-progressbar {
height: 5px;
background-color: #4caf50;
width: 0%;
transition: width 0.5s ease-in-out
}`);
// 开关控件
GM_addStyle(`
label:has(> .helper-toggle-switch) > input {
display: none;
}
.helper-toggle-switch {
position: relative;
display: inline-block;
width: 32px;
background-color: #ddd;
transition: background-color 0.3s;
}
.helper-toggle-switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
background-color: white;
border-radius: 50%;
transition: transform 0.3s;
}
label:has(> .helper-toggle-switch) > input:checked + .helper-toggle-switch {
background-color: #4caf50;
}
label:has(> .helper-toggle-switch) > input:checked + .helper-toggle-switch::after {
transform: translateX(15px);
}`);
// 下拉控件
GM_addStyle(`
.helper-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="black" class="bi bi-chevron-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/></svg>')
no-repeat right 10px center;
background-color: inherit;
color: inherit;
border: 1px solid #ccc;
padding: 0 30px 0 10px;
width: max-content;
transition: background-color 0.3s, border-color 0.3s;
cursor: pointer;
outline: none;
}
.helper-select:focus {
background-color: #ddd;
border-color: #ccc;
}`);
// 多选控件
GM_addStyle(`
.helper-multicheck-container {
display: flex;
border: 1px solid #ccc;
box-sizing: border-box;
overflow: hidden;
}
.helper-multicheck-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.3s;
position: relative;
}
.helper-multicheck-item:not(:first-child)::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 1px;
background-color: #eee;
}
.helper-multicheck-item:not(:first-child) {
padding-left: 1px;
}
.helper-multicheck-item:not(:last-child)::before {
display: block;
}
.helper-multicheck-item input[type="checkbox"] {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
margin: 0;
cursor: pointer;
}
.helper-multicheck-item input[type="checkbox"]:checked + .helper-multicheck-text {
background-color: #4caf50;
}
.helper-multicheck-item .helper-multicheck-text {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 10px;
background-color: inherit;
border: 1px solid transparent;
transition: background-color 0.3s;
box-sizing: border-box;
white-space: nowrap;
}`);
// 单行输入控件
GM_addStyle(`
.helper-input {
width: 80%;
height: 2em;
border: 1px solid #ccc;
border-radius: 5px;
padding-left: 5px;
margin-left: 5px;
}`);
// 标签控件
GM_addStyle(`
.helper-tag-container {
display: flex;
flex-wrap: wrap;
margin-top: 10px;
}
.helper-tag {
background-color: #d2553d;
color: white;
padding: 2px 5px 2px 10px;
margin: 5px;
border-radius: 20px;
display: flex;
align-items: center;
}
.helper-tag .helper-tag-remove-btn {
background-color: transparent;
border: none;
color: white;
margin-left: 5px;
cursor: pointer;
}`);
// 执行按钮控件
GM_addStyle(`
.helper-setting-button {
padding: 0 20px;
background-color: inherit;
color: inherit;
border: 1px solid #ccc;
cursor: pointer;
transition: background-color 0.3s;
}`);
// 关注、屏蔽按钮控件
GM_addStyle(`
.helper-f-button,
.helper-b-button {
padding: 2px;
width: 5.5rem;
cursor: pointer;
border: none;
border-radius: 8px;
color: white;
transition: background-color 0.3s ease;
}
.helper-b-button {
background-color: #d2553d;
}
.helper-b-button::before {
content: '已屏蔽';
}
.helper-f-button:not([data-hfb-followed]) {
background-color: #1772f6;
}
.helper-f-button:not([data-hfb-followed]):hover {
background-color: #0063e6;
}
.helper-f-button[data-hfb-followed] {
background-color: #8491a5;
}
.helper-f-button[data-hfb-followed]:hover {
background-color: #758195;
}
.helper-f-button.hfb-normal:not([data-hfb-followed])::before {
content: '关注';
}
.helper-f-button.hfb-special:not([data-hfb-followed])::before {
content: '特别关注';
}
.helper-f-button.hfb-thread:not([data-hfb-followed])::before {
content: '在本帖关注';
}
.helper-f-button.hfb-normal[data-hfb-followed]::before {
content: '已关注';
}
.helper-f-button.hfb-normal[data-hfb-followed]:hover::before {
content: '取消关注';
}
.helper-f-button.hfb-special[data-hfb-followed]::before {
content: '已特关';
}
.helper-f-button.hfb-special[data-hfb-followed]:hover::before {
content: '取消特关';
}
.helper-f-button.hfb-thread[data-hfb-followed]::before {
content: '已在本帖关注';
}
.helper-f-button.hfb-thread[data-hfb-followed]:hover::before {
content: '在本帖取关';
}`);
// 复选框控件
GM_addStyle(`
.helper-checkbox {
appearance: none;
width: 10px;
height: 10px;
border: 1px solid black;
background-color: transparent;
display: inline-block;
position: relative;
margin-right: 5px;
cursor: pointer;
}
.helper-checkbox:before {
content: '';
background-color: black;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
width: 5px;
height: 5px;
transition: all 0.1s ease-in-out;
}
.helper-checkbox:checked:before {
transform: translate(-50%, -50%) scale(1);
}
.helper-checkbox-label {
color: black;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
}`);
// 控件通用样式
GM_addStyle(`
.helper-active-component {
height: 32px;
border-radius: 32px;
}
.helper-halfheight-active-component {
height: 16px;
border-radius: 16px;
}`);
// 弹窗表格
GM_addStyle(`
.helper-popup-table {
width: 100%;
height: 100%;
border-collapse: collapse;
}
.helper-scroll-component:has(> .helper-popup-table) {
padding-top: 0 !important;
}
.helper-popup-table th,
.helper-popup-table td {
padding: 8px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 10rem;
}
.helper-sticky-header {
position: sticky;
top: 0px;
padding-top: 10px;
background-color: white;
}
th.helper-sortby::after {
content: '▼';
}`);
// 弹窗表格中复用移动版主题span
GM_addStyle(`
.helper-popup-table .micon{
background-color: #6db1d5;
color: white;
padding: 1px;
margin-right: 3px;
border-radius: 2px;
overflow: hidden;
}
.helper-popup-table .top{
background-color: #ff9900;
}
.helper-popup-table .lock{
background-color: #ff5656;
}
.helper-popup-table .digest{
background-color: #b3cc0d;
}`);
// 其它弹窗样式
GM_addStyle(`
.helper-hr {
margin: 0;
border: 0;
border-top: 1px solid #ccc;
}
.helper-scroll-component {
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #888 #eee;
}
.helper-tab-btn {
padding: 10px;
border: none;
background-color: transparent;
color: inherit;
cursor: pointer;
text-align: center;
font-size: 0.75rem;
font-weight: 500;
margin: 5px;
border-radius: 12px;
transition: background-color 0.3s;
white-space: nowrap;
}
.helper-tab-selected {
background-color: #ddd;
}
div.helper-center-message {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
padding: 0;
}
.helper-footnote {
position: absolute;
bottom: 2px;
left: 5px;
font-size: 70%;
color: #ccc;
background-color: inherit;
}
.helper-setting-container {
display: flex;
min-height: 36px;
justify-content: space-between;
align-items: center;
padding: 5px;
}
div:has(> .helper-setting-container) .helper-setting-container:not(:last-of-type) {
border-bottom: 1px solid #ccc;
}`);
// 其它样式
GM_addStyle(`
.helper-ellip-link {
display: inline-block;
color: #004e83 !important;
overflow: hidden;
max-width: calc(min(70%, 30rem));
text-overflow: ellipsis;
vertical-align: top;
}`);
// ========================================================================================================
// 获取页面信息
// ========================================================================================================
function hasReadPermission(doc = document) {
return !Boolean(qS('#messagetext', doc));
}
function isLogged() {
return document.cookie.split(';').some(e => /_lastcheckfeed=\d+/.test(e));
}
function isFirstPage(URL_params) {
const page = URL_params.page;
return !Boolean(page) || page == 1;
}
function getPostId(post) { return post.id.slice(3); }
function getPostsInPage(page_doc = document) { return qSA('[id^=pid]', page_doc); }
function getPostInfo(post) {
const post_id = getPostId(post);
const post_URL = qS(`#postnum${post_id}`, post).href;
const post_URL_params = post_URL.parseURL();
const thread_id = post_URL_params.ptid;
const post_auth = qS('#favatar' + post_id + ' > div.pi > div > a', post).text;
const post_auth_id = qS('#favatar' + post_id + ' > div.pi > div > a', post).href.parseURL().uid;
const sub_time = qS('[id^=authorposton]', post).textContent;
return { post_id, thread_id, post_auth, post_auth_id, sub_time, post_URL };
}
function getFirstFloorAuthorInfo(page_doc = document) {
const first_post_info = getPostInfo(qS('#postlist > div > table'), page_doc);
return { name: first_post_info.post_auth, uid: first_post_info.post_auth_id };
}
function theOnlyAuthorInfo(page_doc = document) {
const specific_authorid = page_doc.original_url.parseURL().authorid;
const first_floor_author = getFirstFloorAuthorInfo(page_doc);
return specific_authorid == first_floor_author.uid ? first_floor_author : null;
}
async function getThreadAuthorInfo(page_doc = document) {
const URL_params = page_doc.original_url.parseURL();
if (isFirstPage(URL_params)) {
const the_only_author = theOnlyAuthorInfo(page_doc);
if (!the_only_author) {
return getFirstFloorAuthorInfo();
}
else {
delete URL_params.authorid;
const real_first_page = await getPageDocInDomain(URL_params);
return getThreadAuthorInfo(real_first_page);
}
}
else {
const thread_auth_name = qS('#tath > a:nth-child(1)').title;
const thread_auth_id = qS('#tath > a:nth-child(1)').href.parseURL().uid;
return { name: thread_auth_name, uid: thread_auth_id };
}
}
function getSpaceAuthor(page_doc = document) {
const URL_params = page_doc.original_url.parseURL();
if (typeof URL_params.do === 'undefined') {
return qS('meta[name="keywords"]', page_doc).content.slice(0, -3);
}
else {
const author_name = qS('#pcd > div > div > h2 > a', page_doc);
return author_name ? author_name.textContent : '';
}
};
async function getThreadPopularity(tid) {
const page_doc = await getPageDocInDomain({ loc: 'forum', mod: 'viewthread', tid, mobile: '2' }, mobileUA);
const views = Number(qS('i.dm-eye', qS('ul.authi', page_doc)).nextSibling.textContent);
const replies = Number(qS('i.dm-chat-s', qS('ul.authi', page_doc)).nextSibling.textContent);
const favs = Number(qS('#forum > div.foot.foot_reply.flex-box.cl > a:nth-child(2)', page_doc).textContent.slice(0, -2));
const shares = Number(qS('#forum > div.foot.foot_reply.flex-box.cl > a:nth-child(3)', page_doc).textContent.slice(0, -2));
return { views, replies, favs, shares };
}
// ========================================================================================================
// 获取页面内容
// ========================================================================================================
function createURLInDomain(params) {
if (!'loc' in params) {
return;
}
let url = `https://${location.host}/main/${params.loc}.php?`;
delete params.loc;
for (let [key, value] of Object.entries(params)) {
url += `${key}=${value}&`;
}
return url;
}
async function getPageDocInDomain(params, UA = null) {
const url = createURLInDomain(params);
if (UA === null) {
const response = await fetch(url);
let page_doc = new DOMParser().parseFromString(await response.text(), 'text/html');
page_doc.original_url = url;
return page_doc;
}
else {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: { 'User-Agent': UA },
onload: response => {
let page_doc = new DOMParser().parseFromString(response.responseText, 'text/html');
page_doc.original_url = url;
resolve(page_doc);
}
});
});
}
}
function formatPostNodeText(node) {
let text = '';
switch (node.tagName + '.' + node.className) {
case 'STYLE.':
case 'SCRIPT.':
case 'TABLE.op':
case 'IGNORE_JS_OP.':
break;
case 'DIV.quote':
text += '<<<\n';
let quote_href = qS('td > div > blockquote > font > a', node);
if (quote_href) {
let origin_quote = quote_href.innerText;
quote_href.innerText += ` PID:${quote_href.href.parseURL().pid}`;
text += node.textContent + '\n';
quote_href.innerText = origin_quote;
}
else {
text += node.textContent + '\n'
}
text += '>>>\n';
break;
case 'HR.l':
text += '++++++++\n';
break;
default:
text += node.textContent;
}
return text;
}
function getPostContent(pid, page_doc = document) {
const post = qS('#post_' + pid, page_doc);
const tf = qS('#postmessage_' + pid, post);
let children_nodes = tf.childNodes;
let text = '';
for (let child of children_nodes) {
text += formatPostNodeText(child);
}
let post_file = [];
qSA('ignore_js_op', tf).forEach(node => {
for (let a of qSA('a', node)) {
if (a.href.includes('mod=attachment&aid=')) {
let title, ext;
if (a.innerText == '下载附件') {
[title, ext] = extractFileAndExt(qS('strong', node).innerText);
}
else {
[title, ext] = extractFileAndExt(a.innerText);
}
if (!startWithChinese(title)) {
title = '';
}
post_file.push({ url: a.href, title, ext });
break;
}
}
});
let attach = [];
qSA('a[id^=aid]', post).forEach(a => {
let [title, ext] = extractFileAndExt(a.innerText);
if (!startWithChinese(title)) {
title = '';
}
attach.push({ url: a.href, title, ext });
});
// let image_list = qS('#imagelist_' + pid, post) // 多图
// if (image_list) {
// image_list = qS('div.pattl', post); // 单图
// }
// if (image_list) {
// image_list = qSA('img', image_list);
// for (let i = 0; i < image_list.length; i++) {
// const img = image_list[i];
// const img_url = img.getAttribute('zoomfile');
// let img_title = img.title
// if (!startWithChinese(img_title)) {
// img_title = '';
// }
// attach.push({ url: img_url, title: img_title });
// }
// }
let op_body = qS('[id^="op-"][id$="-body"]', post);
let op = [];
if (op_body) {
const url_list = qSA('a', op_body);
if (url_list.length > 0) {
for (let url of url_list) {
let [title, ext] = extractFileAndExt(url.innerText);
op.push({ url: url.href, title, ext });
}
}
}
return { text, post_file, attach, op };
}
async function getPageContent(page_doc, type = 'main') { // type: main, checked, all
if (!page_doc.original_url) {
page_doc.original_url = page_doc.URL;
}
const tid = page_doc.original_url.parseURL().tid;
const title = qS('meta[name="keywords"]', page_doc).content;
let page_id = page_doc.original_url.parseURL().page;
if (!page_id) {
page_id = 1;
}
const checked_posts = await GM.getValue(tid + '_checked_posts', []);
const posts_in_page = getPostsInPage(page_doc);
let text = '';
let post_file = [];
let attach = [];
let op = [];
for (let post of posts_in_page) {
if (type == 'checked') {
const post_id = getPostId(post);
if (!checked_posts.includes(post_id)) {
continue;
}
}
const post_info = getPostInfo(post);
const post_content = getPostContent(post_info.post_id, page_doc);
post_file.push(...post_content.post_file);
attach.push(...post_content.attach);
op.push(...post_content.op);
if (type != 'main') {
text += '<----------------\n';
}
text += `//${post_info.post_auth}(UID: ${post_info.post_auth_id}) ${post_info.sub_time}\n`;
text += `//PID:${post_info.post_id}\n`;
text += post_content.text;
if (type != 'main') {
text += '\n---------------->\n';
}
if (type == 'main') {
break;
}
}
return { tid, title, page_id, text, post_file, attach, op };
}
async function getAllPageContent(tid, authorid = '', type = 'all', progress = null, dt = null) { // type: main, all, checked
const first_page = await getPageDocInDomain({ loc: 'forum', mod: 'viewthread', tid, authorid });
const title = qS('meta[name="keywords"]', first_page).content;
if (!hasReadPermission(first_page)) {
updateProgressbar(progress, dt);
return { tid, title, text: '没有阅读权限', attach: [], op: [] };
}
if (type == 'main') {
updateProgressbar(progress, decimalCeil(0.8 * dt));
const content = await getPageContent(first_page, type);
updateProgressbar(progress, decimalCeil(0.2 * dt));
return content;
}
const page_num = (qS('#pgt > div > div > label > span', first_page) || { title: '共 1 页' }).title.match(/共 (\d+) 页/)[1];
const ddt = decimalCeil(dt / page_num);
updateProgressbar(progress, ddt);
const promises = [getPageContent(first_page, type)];
promises.push(...Array.from({ length: page_num - 1 }, async (_, i) => {
const page = await getPageDocInDomain({ loc: 'forum', mod: 'viewthread', tid, page: i + 2, authorid });
updateProgressbar(progress, 0.8 * ddt);
const content = await getPageContent(page, type);
updateProgressbar(progress, 0.2 * ddt);
return content;
}));
let content_list = await Promise.all(promises);
content_list.sort((a, b) => a.page_id - b.page_id);
let text = '';
let post_file = [];
let attach = [];
let op = [];
content_list.forEach(content => {
text += content.text + '\n';
post_file.push(...content.post_file);
attach.push(...content.attach);
op.push(...content.op);
});
return { tid, title, text, post_file, attach, op };
}
// ========================================================================================================
// 保存与下载
// ========================================================================================================
function downloadFromURL(target, zip = null, progress = null, dt = null) {
// 直接下载或者保存到zip
// progress={value} 当前进度条百分比
// dt 完成后增加的进度条百分比
let { url, title, ext, is_blob } = target;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
onload: response => {
if (!ext) {
const content_type = response.responseHeaders.match(/Content-Type: (.+)/i);
if (content_type && content_type[1]) {
ext = MIME_type_map[content_type[1]] || '';
}
}
if (ext != '') {
const blob = response.response;
if (zip !== null && response.status == 200) {
zip.file(`${title}.${ext}`, blob, { binary: true });
updateProgressbar(progress, dt);
}
else {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = () => {
const a = docre('a');
a.download = `${title}.${ext}`;
a.href = reader.result;
a.click();
updateProgressbar(progress, dt);
}
const revokeURL = () => is_blob ? URL.revokeObjectURL(url) : null;
reader.onloadend = revokeURL;
}
resolve();
}
}
});
});
}
async function insertZip(target_list, zip, progress = null, dt = null) {
const calcZipTagrgetFileNum = target_list => {
if (!target_list.hasOwnProperty('files_num')) {
target_list.files_num = target_list.reduce((acc, cur) => acc + (cur?.is_dir ? calcZipTagrgetFileNum(cur.files) : 1), 0);
}
return target_list.files_num;
};
calcZipTagrgetFileNum(target_list);
const ddt = dt ? dt / target_list.files_num : null;
for (let target of target_list) {
if (target?.is_dir) {
const dir = zip.folder(target.title);
insertZip(target.files, dir, progress, ddt * target.files_num);
}
else {
await downloadFromURL(target, zip, progress, ddt);
}
}
}
async function createZipAndDownloadFromURLs(zip_name, target_list, progress = null, dt = null) {
if (target_list.length == 0) {
return Promise.resolve();
}
if (target_list.length == 1) {
return downloadFromURL(target_list[0], null, progress, dt);
}
const zip = new JSZip();
await insertZip(target_list, zip, progress, decimalCeil(0.75 * dt));
return await new Promise(async (resolve, reject) => {
const content = await zip.generateAsync({ type: 'blob' });
const a = docre('a');
a.download = zip_name + '.zip';
a.href = URL.createObjectURL(content);
a.click();
updateProgressbar(progress, decimalCeil(0.25 * dt));
URL.revokeObjectURL(a.href);
resolve();
});
}
async function saveFile(filename, text, post_file = [], attach = [], op = []) {
let download_list = []
if (hs.enable_text_download) {
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
download_list.push({ list: [{ url, title: filename, is_blob: true }], name: '正文' });
}
if (hs.enable_postfile_download) {
post_file.forEach((e, i) => {
if (!e.url.startsWith(location.origin)) {
e.url = location.origin + '/main/' + e.url;
}
e.title = e.title || `${i + 1}`;
e.title = `${filename}_${e.title}`;
});
download_list.push({ list: post_file, name: '帖内资源' });
}
if (hs.enable_attach_download) {
attach.forEach((e, i) => {
if (!e.url.startsWith(location.origin)) {
e.url = location.origin + '/main/' + e.url;
}
e.title = e.title || `${i + 1}`;
e.title = `${filename}_附${e.title}`;
});
download_list.push({ list: attach, name: '附件' });
}
if (hs.enable_op_download) {
download_list.push({ list: op, name: '原创资源保护' });
}
download_list = download_list.filter(e => e.list.length > 0);
if (download_list.length == 0) {
alert('没有需要保存的内容, 请检查设置.');
return;
}
const top_progressbar = createTopProgressbar();
let progress = { value: 0, bar: top_progressbar };
let promises = [];
switch (hs.files_pack_mode) {
case 'no': {
const files_num = download_list.reduce((acc, cur) => acc + cur.list.length, 0);
const dt = decimalCeil(100 / files_num);
download_list.forEach(target =>
target.list.forEach(e =>
promises.push(downloadFromURL(e, null, progress, dt)
)));
break;
}
case 'single': {
const dt = decimalCeil(100 / download_list.length);
download_list.forEach(target =>
promises.push(createZipAndDownloadFromURLs(`${filename}_${target.name}`, target.list, progress, dt)
));
break;
}
case 'all': {
promises.push(createZipAndDownloadFromURLs(filename, download_list.flatMap(e => e.list), progress, 100));
break;
}
}
}
async function saveThread(type = 'main') {
const thread_id = qS('#postlist > table:nth-child(1) > tbody > tr > td.plc.ptm.pbn.vwthd > span > a').href.parseURL().tid;
let title_name = qS('#thread_subject').parentNode.textContent.replaceAll('\n', '').replaceAll('[', '【').replaceAll(']', '】');
let file_info = `Link: ${document.URL}\n****************\n`;
if (type == 'main') {
let text = file_info;
let content = await getPageContent(document, 'main');
text += content.text;
await saveFile(title_name, text, content.post_file, content.attach, content.op);
}
else {
if (type == 'checked') {
const checked_posts = await GM.getValue(thread_id + '_checked_posts', []);
if (checked_posts.length == 0) {
alert('没有需要保存的内容, 请检查设置.');
return;
}
}
let filename = title_name;
let text = file_info;
const the_only_author = theOnlyAuthorInfo();
let filename_suffix = '';
if (the_only_author) {
filename_suffix = `${the_only_author.name}`;
if (type == 'checked') {
filename_suffix += '节选';
}
}
else {
if (type == 'all') {
filename_suffix = '全帖';
}
else if (type == 'checked') {
filename_suffix = '节选';
}
}
filename += '(' + filename_suffix + ')';
const content_list = await getAllPageContent(thread_id, the_only_author ? the_only_author.uid : '', type);
await saveFile(filename, text + content_list.text, content_list.post_file, content_list.attach, content_list.op);
if (type == 'checked') {
GM.deleteValue(thread_id + '_checked_posts');
}
}
}
async function saveMergedThreads(type = 'main') {
const uid = location_params.uid;
let checked_threads = GM_getValue(uid + '_checked_threads', []);
if (checked_threads.length == 0) {
alert('没有需要保存的内容, 请检查设置.');
return;
}
const bar = createTopProgressbar();
const progress = { value: 0, bar };
if (type == 'main') {
const dt = decimalCeil(90 / checked_threads.length);
const promises = checked_threads.map(tid => getAllPageContent(tid, uid, 'main', progress, dt));
let content_list = await Promise.all(promises);
content_list = content_list.sort((a, b) => a.tid - b.tid);
const content = content_list.map(e => e.text).join('\n\n=================\n\n');
let filename = content_list.reduce((acc, cur) => commonPrefix(acc, cur.title), content_list[0].title);
filename += '(合集)';
await downloadFromURL({
url: URL.createObjectURL(new Blob([content], { type: 'text/plain' })),
title: filename,
is_blob: true
},
null, progress, 10);
}
else {
const dt = decimalCeil(85 / checked_threads.length);
const promises = checked_threads.map(tid => getAllPageContent(tid, type == 'author' ? uid : '', type, progress, dt));
let content_list = await Promise.all(promises);
let filename = content_list.reduce((acc, cur) => commonPrefix(acc, cur.title), content_list[0].title);
filename += type == 'author' ? '(合集仅作者)' : '(合集)'
content_list = content_list.map(e => {
return {
title: e.title,
url: URL.createObjectURL(new Blob([e.text], { type: 'text/plain' }))
}
});
createZipAndDownloadFromURLs(filename, content_list, progress, 15);
}
await GM.deleteValue(uid + '_checked_threads');
updatePageDoc();
}
// ========================================================================================================
// 获取用户最新动态
// ========================================================================================================
async function getUserNewestPostOrThread(uid, tid, last_tpid = 0) {
// 返回结构:
// { new: [{ tid, title, pids: [], fid }], found, last_tpid }
// 其中对于对于tid=0的情况,pids为undefined; 对于tid!=0的情况,fid为undefined
if (tid == 0) {
return getUserNewestThread(uid, last_tpid);
}
else if (tid > 0) {
return getUserNewestPostInThread(uid, tid, last_tpid);
}
else if (tid == -1) {
return getUserNewestReply(uid, last_tpid);
}
}
async function getUserNewestReply(uid, last_pid = 0) {
// 返回用户空间回复页首页新于last_pid的回复,通过能否在首页查询到不晚于last_pid的回复判断是否可能有更多回复
const URL_params = { loc: 'home', mod: 'space', uid, do: 'thread', view: 'me', type: 'reply', from: 'space', mobile: '2' };
const followed_threads = GM_getValue(uid + '_followed_threads', []);
const follow_tids = followed_threads.map(e => e.tid).filter(e => e > 0);
const page_doc = await getPageDocInDomain(URL_params, mobileUA);
const threads_in_page = qSA('#home > div.threadlist.cl > ul > li', page_doc);
let new_replyed_threads = [];
let found = false;
if (threads_in_page.length > 0) {
for (let thread of threads_in_page) {
const reply_in_thread = qSA('a', thread);
const tid = reply_in_thread[0].href.parseURL().ptid;
const title = qS('em', reply_in_thread[0]).textContent.trim()
let pids = []
for (let i = 1; i < reply_in_thread.length; i++) { // index 0 是主题链接
const pid = reply_in_thread[i].href.parseURL().pid;
if (pid <= last_pid) {
found = true;
break;
}
pids.push(pid);
}
if (pids.length > 0 && !follow_tids.includes(Number(tid))) {
new_replyed_threads.push({ tid, title, pids });
}
}
}
last_pid = new_replyed_threads.length == 0 ? 1 : new_replyed_threads[0].pids[0]; // last_pid==0代表第一次查询新回复状态,所以完全没有回复的状态只能设为1
return { 'new': new_replyed_threads, found, last_tpid: last_pid };
}
async function getUserNewestThread(uid, last_tid = 0) {
// 返回用户空间主题页首页新于last_tid的主题,通过能否在首页查询到不晚于last_tid的主题判断是否可能有更多主题
const URL_params = { loc: 'home', mod: 'space', uid, do: 'thread', view: 'me', from: 'space', mobile: '2' };
const page_doc = await getPageDocInDomain(URL_params, mobileUA);
const threads_in_page = qSA('li.list', page_doc);
let new_threads = [];
let found = false;
if (threads_in_page.length > 0) {
for (let thread of threads_in_page) {
const thread_title_node = qS('.threadlist_tit', thread);
const tid = thread_title_node.parentNode.href.parseURL().tid;
if (tid <= last_tid) {
found = true;
break;
}
const title = qS('em', thread_title_node).textContent;
const fid = qS('li.mr > a', thread).href.parseURL().fid;
new_threads.push({ tid, title, fid });
if (last_tid == 0) {
break;
}
}
}
last_tid = new_threads.length == 0 ? 1 : new_threads[0].tid;
return { 'new': new_threads, found, 'last_tpid': last_tid }
}
async function getUserNewestPostInThread(uid, tid, last_pid = 0) {
// 返回关注主题只看该作者末页(large_page_num)新于last_pid的回复,通过能否在末页查询到不晚于last_pid的回复判断是否可能有更多回复
const URL_params = { loc: 'forum', mod: 'viewthread', tid, authorid: uid, page: large_page_num, mobile: '2' };
const page_doc = await getPageDocInDomain(URL_params, mobileUA);
const posts_in_page = getPostsInPage(page_doc);
const thread_title = qS('meta[name="keywords"]', page_doc).content;
let new_posts = [];
let found = false;
let pids = [];
for (let i = posts_in_page.length - 1; i >= 0; i--) {
const post = posts_in_page[i];
const pid = getPostId(post);
if (pid <= last_pid) {
found = true;
break;
}
pids.push(pid);
if (last_pid == 0) {
break;
}
}
if (pids.length > 0) {
new_posts.push({ tid, title: thread_title, pids });
}
last_pid = new_posts.length == 0 ? 1 : new_posts[0].pids[0]; // last_pid==0代表第一次查询新回复状态,所以完全没有回复的状态只能设为1
return { new: new_posts, found, last_tpid: last_pid };
}
// ========================================================================================================
// 关注、屏蔽与自动回复
// ========================================================================================================
// 根据checkbox的状态更新value对应的数组
// value/id: tid/pid, uid/tid
function recordCheckbox(value, id, checked) {
let checked_list = GM_getValue(value, []);
id = id.split('_check_')[1];
updateListElements(checked_list, id, checked);
updateGMList(value, checked_list);
}
// 关注某个用户在某个Thread下的回复
// 若tid==0,则关注用户的所有主题
// 若tid==-1, 则关注用户的所有回复
function recordFollow(info, followed) {
let followed_threads = GM_getValue(info.uid + '_followed_threads', []);
updateListElements(followed_threads, { tid: info.tid, title: info.title, last_tpid: 0 }, followed, (a, b) => a.tid == b.tid); // last_tpid==0 表示这是新关注的用户
updateGMList(info.uid + '_followed_threads', followed_threads);
let followed_users = GM_getValue('followed_users', []);
updateListElements(followed_users, { uid: info.uid, name: info.name }, followed_threads.length > 0, (a, b) => a.uid == b.uid);
updateGMList('followed_users', followed_users);
let followed_num = GM_getValue('followed_num', 0);
followed_num += followed ? 1 : -1;
followed_num = followed_num < 0 ? 0 : followed_num;
GM.setValue('followed_num', followed_num);
}
function blockUser(uid, name) {
hs.blacklist = updateListElements(hs.blacklist, { uid, name }, true, (a, b) => a.uid == b.uid);
GM.setValue('helper_setting', hs);
updateGMList(`${uid}_followed_threads`, []);
const followed_users = GM_getValue('followed_users', []);
updateListElements(followed_users, { uid, name }, false, (a, b) => a.uid == b.uid);
updateGMList('followed_users', followed_users);
}
function autoReply(timeout = 2000) {
const reply_text = hs.auto_reply_message;
const reply_textarea = qS('#fastpostmessage');
if (reply_textarea) {
reply_textarea.value = reply_text;
}
const reply_btn = qS('#fastpostsubmit');
if (reply_btn) {
setTimeout(() => reply_btn.click(), timeout);
}
}
// ========================================================================================================
// 修改页面的工具
// ========================================================================================================
function setHidden(elem, hidden = true) {
if (!elem) {
return;
}
if (hidden) {
elem.style.display = 'none';
}
else {
elem.style.display = '';
}
}
function insertElement(elem, pos, type = 'insertBefore') {
switch (type) {
case 'append':
pos.appendChild(elem);
break;
case 'insertBefore':
pos.parentNode.insertBefore(elem, pos);
break;
case 'insertAfter':
if (pos.nextSibling) {
pos.parentNode.insertBefore(elem, pos.nextSibling);
}
else {
pos.parentNode.appendChild(elem);
}
break;
}
}
function insertInteractiveLink(text, func, pos, type = 'append') {
const a = docre('a');
a.href = 'javascript:void(0)';
a.textContent = text;
if (func instanceof Function) {
a.addEventListener('click', func);
}
insertElement(a, pos, type);
return a;
}
function insertLink(text, URL_params, pos, max_text_length = 0, type = 'append') {
const a = docre('a');
if (max_text_length > 0 && text.length > max_text_length) {
a.text = text.slice(0, max_text_length) + '...';
a.title = text;
}
else {
a.textContent = text;
}
a.href = createURLInDomain(URL_params);
a.target = '_blank';
insertElement(a, pos, type);
return a;
}
function createFollowButton(info) {
// info: { uid, name, tid, title }
const follow_btn = docre('button');
const follow_status = GM_getValue(info.uid + '_followed_threads', []);
const followed = follow_status.some(e => e.tid == info.tid);
follow_btn.type = 'button';
follow_btn.className = 'helper-f-button';
follow_btn.setAttribute('data-hfb-uid', info.uid);
if (followed) {
follow_btn.setAttribute('data-hfb-followed', '');
}
let follow_type = '';
switch (info.tid) {
case -1:
follow_type = 'hfb-special';
info.title = '所有回复';
break;
case 0:
follow_type = 'hfb-normal';
info.title = '所有主题';
break;
default:
follow_type = 'hfb-thread';
}
follow_btn.classList.add(follow_type);
follow_btn.addEventListener('click', async () => {
const followed_num = GM_getValue('followed_num', 0);
if (followed_num >= magic_num) {
alert('关注数已达上限,请清理关注列表.');
return;
}
const follow_status = GM_getValue(info.uid + '_followed_threads', []);
const followed = follow_status.some(e => e.tid == info.tid);
qSA(`.${follow_type}[data-hfb-uid='${info.uid}']`).forEach(e => {
if (followed) {
e.removeAttribute('data-hfb-followed');
}
else {
e.setAttribute('data-hfb-followed', '')
}
});
recordFollow(info, !followed);
if (info.tid == -1 && !followed) { // 特关同时也关注主题
recordFollow({ uid: info.uid, name: info.name, tid: 0, title: '所有主题' }, true);
}
});
return follow_btn;
}
function addWrapInNode(root, min_wrap_length = hs.min_wrap_length, wrap_length = hs.typical_wrap_length, max_wrap_length = hs.max_wrap_length, dot_char = hs.wrap_dot, comma_char = hs.wrap_comma) {
const findBreak = text => {
for (let i = wrap_length; i < Math.min(text.length, max_wrap_length); i++) {
if (dot_char.includes(text[i])) {
return i;
}
}
if (text.length > max_wrap_length) {
for (let i = max_wrap_length; i < text.length; i++) {
if (dot_char.includes(text[i]) || comma_char.includes(text[i])) {
return i;
}
}
}
return -1;
};
let iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, null, false);
let node = iter.nextNode();
while (node) {
let text = node.nodeValue;
let break_index;
if (text.length > wrap_length) {
break_index = findBreak(text);
}
if (break_index > 0) {
let text1 = text.slice(0, break_index + 1);
let text2 = text.slice(break_index + 1);
if (text2.trim().length > min_wrap_length) {
node.nodeValue = '';
let new_node1 = document.createTextNode(text1);
let br = docre('br');
br.setAttribute('data-hbr', 'auto-wrap');
let new_node2 = document.createTextNode(text2);
insertElement(new_node2, node, 'insertAfter');
insertElement(br, new_node2);
insertElement(new_node1, br);
let current_node = node;
node = iter.nextNode();
node = iter.nextNode();
node.parentNode.removeChild(current_node);
continue;
}
}
node = iter.nextNode();
}
iter = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, { acceptNode: node => node.tagName == 'BR' }, false);
node = iter.nextNode();
while (node) {
let in_multi_br = false;
let last_in_multi_br = false;
while (node) {
if (node.nextSibling) {
const next = node.nextSibling;
const next_is_br = next.tagName == 'BR';
const next_is_space = next.nodeType == Node.TEXT_NODE && next.nodeValue.trim() == '';
const next_is_the_end = !next.nextSibling;
const nnext_is_br = next?.nextSibling?.tagName == 'BR';
const nnext_is_newline = next?.nextSibling?.nodeType == Node.TEXT_NODE && next?.nextSibling?.nodeValue.trim() != '';
const nnext_is_newblock = next?.nextSibling?.tagName == 'DIV';
in_multi_br = next_is_br || next_is_the_end || next_is_space && (nnext_is_br || nnext_is_newline || nnext_is_newblock);
if (in_multi_br) {
node = iter.nextNode();
last_in_multi_br = true;
continue;
}
}
break;
}
if (!in_multi_br && !last_in_multi_br) {
const br = docre('br');
br.setAttribute('data-hbr', 'before-single-br');
insertElement(br, node);
}
node = iter.nextNode();
}
// 删掉引用中的自动<br>和引用后可能的第一个自动<br>
qSA('div.quote', root).forEach(e => {
const brs = qSA('br[data-hbr]', e);
for (let br of brs) {
br.parentNode.removeChild(br);
}
const next = e.nextElementSibling;
if (next.getAttribute('data-hbr') == 'before-single-br') {
next.parentNode.removeChild(next);
}
});
}
function removeWrapInNode(root) {
let iter = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, {
acceptNode: node => {
if (node.tagName == 'BR' && node.hasAttribute('data-hbr') && node.getAttribute('data-hbr') == 'before-single-br') {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_REJECT;
}
}, false);
let node = iter.nextNode();
while (node) {
const current = node;
node = iter.nextNode();
current.parentNode.removeChild(current);
}
iter = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, {
acceptNode: node => {
if (node.tagName == 'BR' && node.hasAttribute('data-hbr') && node.getAttribute('data-hbr') == 'auto-wrap') {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_REJECT;
}
}, false);
node = iter.nextNode();
while (node) {
const next = node.nextSibling;
const previous = node.previousSibling;
const current = node;
node = iter.nextNode();
previous.nodeValue += next.nodeValue;
current.parentNode.removeChild(next);
current.parentNode.removeChild(current);
}
}
// ========================================================================================================
// 修改页面内容
// ========================================================================================================
function insertHelperLink() {
let target_menu = qS('#myitem')
if (target_menu) {
const helper_setting_link = insertInteractiveLink('助手', () => { if (!qS('#helper-popup')) { createHelperPopup() } }, target_menu, 'insertBefore');
helper_setting_link.id = 'helper_setting';
const span = docre('span');
span.textContent = ' | ';
span.className = 'pipe';
insertElement(span, target_menu);
return;
}
target_menu = qS('#myspace');
if (target_menu) {
target_menu = qS('#myspace')
insertInteractiveLink('助手', () => { if (!qS('#helper-popup')) { createHelperPopup() } }, target_menu, 'insertBefore');
return;
}
}
function modifyPostInPage() {
const tid = location_params.tid;
const posts_in_page = getPostsInPage();
const thread_title = qS('#thread_subject').textContent;
let all_checked = true;
for (let post of posts_in_page) {
const post_info = getPostInfo(post);
const pid = post_info.post_id;
const uid = post_info.post_auth_id;
const name = post_info.post_auth;
const label = docre('label');
const checkbox = docre('input');
checkbox.id = 'post_check_' + pid;
checkbox.className = 'helper-checkbox';
checkbox.type = 'checkbox';
checkbox.addEventListener('change', () => { recordCheckbox(`${tid}_checked_posts`, checkbox.id, checkbox.checked) });// 每个Thread设置一个数组,存入被选中的Post的ID
label.appendChild(checkbox);
const label_text = document.createTextNode('保存本层');
label.className = 'helper-checkbox-label o';
label.appendChild(label_text);
all_checked = all_checked && checkbox.checked;
const user_card = qS('tbody > tr:nth-child(1) > td.pls > div', post)
const profile_card = qS('[id^=userinfo] > div.i.y ', post);
insertInteractiveLink('代表作', () => createMasterpiecePopup(uid, name), qS('div:first-child', profile_card));
const post_follow_btn = createFollowButton({ uid, name, tid, title: thread_title });
post_follow_btn.classList.add('o');
user_card.appendChild(post_follow_btn);
user_card.appendChild(label);
const profile_icon = qS('div.imicn', profile_card);
profile_icon.appendChild(createFollowButton({ uid, name, tid: 0 }));
insertInteractiveLink(' 屏蔽用户', () => {
blockUser(uid, name);
updatePageDoc();
}, profile_icon);
}
const label = docre('label');
const label_text = document.createTextNode(all_checked ? '清空全选' : '全选本页');
label.appendChild(label_text);
qS('#postlist > table:nth-child(1) > tbody > tr > td.plc.ptm.pbn.vwthd > div').appendChild(label);
const checkbox = docre('input');
checkbox.id = 'page_checked_all';
checkbox.type = 'checkbox';
checkbox.className = 'helper-checkbox';
checkbox.style.verticalAlign = 'middle';
checkbox.checked = all_checked;
checkbox.addEventListener('change', () => {
qSA('[id^=post_check_]').forEach(e => e.checked = checkbox.checked);
label_text.textContent = checkbox.checked ? '清空全选' : '全选本页';
});
label.appendChild(checkbox);
}
function modifyPostPage() {
const the_only_author = theOnlyAuthorInfo();
const saveFunc = (type = 'main') => () => {
saveThread(type).then(() => {
if (hs.enable_auto_reply) {
autoReply();
}
});
};
const down_first_link_pos = qS('#postlist > div > table > tbody > tr:nth-child(1) > td.plc > div.pi > strong');
if (isFirstPage(location_params)) {
insertInteractiveLink('保存主楼 ', saveFunc(), down_first_link_pos);
}
const down_thread_link_pos = qS('#postlist > table:nth-child(1) > tbody > tr > td.plc.ptm.pbn.vwthd > div');
if (the_only_author) {
insertInteractiveLink('保存作者 ', saveFunc('all'), down_thread_link_pos);
}
else {
insertInteractiveLink('保存全帖 ', saveFunc('all'), down_thread_link_pos);
}
insertInteractiveLink('保存选中 ', saveFunc('checked'), down_thread_link_pos);
modifyPostInPage();
}
function modifySpacePage() {
const uid = location_params.uid;
const toptb = qS('#toptb > div.z');
if (toptb) {
const name = getSpaceAuthor();
insertLink(`${name}的主题`, { loc: 'home', mod: 'space', uid, do: 'thread', view: 'me', from: 'space' }, toptb);
toptb.appendChild(createFollowButton({ uid, name, tid: location_params?.type == 'reply' ? -1 : 0 }));
// updatePageDoc();
}
const addMergedownComponent = () => {
const pos = qS('#delform > table > tbody > tr.th > th');
const save_select = docre('select');
const save_types = ['main', 'all', 'author'];
const save_types_text = ['主楼(合并)', '全帖(打包)', '作者(打包)'];
for (let i = 0; i < save_types.length; i++) {
const option = docre('option');
option.value = save_types[i];
option.textContent = save_types_text[i];
save_select.appendChild(option);
if (save_types[i] == hs.default_merge_mode) {
option.selected = true;
}
}
const save_link = insertInteractiveLink(' 下载 ', () => saveMergedThreads(save_select.value), pos);
save_select.addEventListener('change', () => {
const new_save_link = save_link.cloneNode(true);
new_save_link.addEventListener('click', () => {
saveMergedThreads(save_select.value);
});
save_link.replaceWith(new_save_link);
});
save_select.style.display = 'none';
pos.appendChild(save_select);
const thread_table = qS('#delform > table > tbody');
if (qS('.emp', thread_table)) {
return;
}
const thread_in_page = qSA('tr:not(.th)', thread_table);
for (let thread of thread_in_page) {
const link = qS('th > a', thread)
const tid = link.href.parseURL().tid;
const checkbox = docre('input');
checkbox.id = 'thread_check_' + tid;
checkbox.type = 'checkbox';
checkbox.className = 'pc';
insertElement(checkbox, link);
if (qS('td:nth-child(3) > a', thread).textContent == '保密存档') {
checkbox.disabled = true;
continue;
}
checkbox.addEventListener('change', () => { recordCheckbox(`${uid}_checked_threads`, checkbox.id, checkbox.checked) });// 每个用户设置一个数组,存入被选中的thread的ID
}
};
const addMasterpieceComponent = () => {
const user_name = getSpaceAuthor();
const header = document.querySelector('#ct > div.mn > div > div.bm_h > h1');
const masterpiece = docre('span');
masterpiece.className = 'xs1 xw0';
const pipe = docre('span');
pipe.className = 'pipe';
pipe.textContent = '|';
masterpiece.appendChild(pipe);
insertInteractiveLink('代表作', () => createMasterpiecePopup(uid, user_name), masterpiece);
header.appendChild(masterpiece);
};
const addDebugModeComponent = () => {
const pos = qS('#pcd > div > ul');
const label = docre('label');
const checkbox = docre('input');
checkbox.type = 'checkbox';
checkbox.checked = hs.enable_debug_mode;
checkbox.addEventListener('change', () => {
hs.enable_debug_mode = checkbox.checked;
hs.enable_debug_mode ? log.setLevel('debug') : log.resetLevel();
log.info('调试模式已' + (hs.enable_debug_mode ? '开启' : '关闭'));
GM.setValue('helper_setting', hs);
});
const text = document.createTextNode('调试模式');
label.appendChild(checkbox);
label.appendChild(text);
pos.appendChild(label);
};
executeIfLoctionMatch(addMergedownComponent, { do: 'thread', type: ['thread', undefined] });
executeIfLoctionMatch(addMasterpieceComponent, { do: 'thread', view: 'me', from: 'space' });
executeIfLoctionMatch(addDebugModeComponent, { do: 'wall', uid: GM_info.script.author });
}
function modifyIndexPage() {
setHidden(qS('#logo'));
setHidden(qS('#desktop'));
setHidden(qS('#mobile'));
insertElement(createOverlay('redirect', () => { location.href = 'main' }), qS('#info'), 'insertBefore');
log.log('Index page modified.');
}
function modifyPageDoc() {
if (hasReadPermission() && isLogged()) {
executeIfLoctionMatch(modifyIndexPage, { pathname: '/' },);
executeIfLoctionMatch(modifyPostPage, { loc: 'forum', mod: 'viewthread', 'mobile': ['no', undefined] });
executeIfLoctionMatch(modifySpacePage, { loc: 'home', mod: 'space', 'mobile': ['no', undefined] });
updatePageDoc();
if (hs.enable_notification) {
updateNotificationPopup();
}
insertHelperLink();
}
}
// ========================================================================================================
// 更新页面内容
// ========================================================================================================
function updatePostsStaus() {
const posts_in_page = getPostsInPage();
const tid = location_params.tid;
// 更新自动换行
for (let post of posts_in_page) {
if (hs.enable_auto_wrap) {
const post_content = qS('[id^=postmessage]', post);
addWrapInNode(post_content);
}
else {
const post_content = qS('[id^=postmessage]', post);
removeWrapInNode(post_content);
}
}
// 更新“保存本层”复选框
const checked_posts = GM_getValue(`${tid}_checked_posts`, []);
qSA('[id^=post_check_]').forEach(e => {
e.checked = checked_posts.includes(e.id.slice(11));
});
}
function updateForumStaus() {
qSA('[id^=normalthread]').forEach(thread => {
const title = qS('a.s.xst', thread).innerText.trim().toLowerCase();
const uid = qS('td.by cite a', thread).href.parseURL().uid;
// 屏蔽关键词
if (hs.enable_block_keyword && hs.block_keywords.some(keyword => title.includes(keyword))) {
thread.style.display = 'none';
}
// 屏蔽用户
else if (hs.enable_blacklist && hs.blacklist.some(e => e.uid == uid)) {
thread.style.display = 'none';
}
else {
thread.style.display = '';
}
});
}
async function updateMergedownStaus() {
// 更新多选下载复选框
const checked_threads = await GM.getValue(location_params.uid + '_checked_threads', []);
qSA('[id^=thread_check_]').forEach(e => {
e.checked = checked_threads.includes(e.id.slice(13));
});
}
function updateFollowOrBlockButtonsStatus() {
for (let { uid } of hs.blacklist) {
qSA(`.helper-f-button[data-hfb-uid='${uid}']`).forEach(e => {
e.removeAttribute('data-hfb-followed');
e.classList.remove('helper-f-button');
e.classList.add('helper-b-button');
e.replaceWith(e.cloneNode(true));
});
}
qSA('.helper-b-button').forEach(e => {
if (!hs.blacklist.some(p => p.uid == e.getAttribute('data-hfb-uid'))) {
e.classList.remove('helper-b-button');
e.classList.add('helper-f-button');
e.addEventListener('click', async () => {
const followed_num = GM_getValue('followed_num', 0);
if (followed_num >= magic_num) {
alert('关注数已达上限,请清理关注列表.');
return;
}
const follow_status = GM_getValue(info.uid + '_followed_threads', []);
const followed = follow_status.some(e => e.tid == info.tid);
qSA(`.${follow_type}[data-hfb-uid='${info.uid}']`).forEach(e => {
if (followed) {
e.removeAttribute('data-hfb-followed');
}
else {
e.setAttribute('data-hfb-followed', '')
}
});
recordFollow(info, !followed);
if (info.tid == -1 && !followed) { // 特关同时也关注主题
recordFollow({ uid: info.uid, name: info.name, tid: 0, title: '所有主题' }, true);
}
});
}
});
}
function updatePageDoc() {
executeIfLoctionMatch(updatePostsStaus, { loc: 'forum', mod: 'viewthread', 'mobile': ['no', undefined] });
executeIfLoctionMatch(updateForumStaus, { loc: 'forum', mod: 'forumdisplay', 'mobile': ['no', undefined] });
executeIfLoctionMatch(updateMergedownStaus, { loc: 'home', mod: 'space', do: 'thread', type: ['thread', undefined], 'mobile': ['no', undefined] });
updateFollowOrBlockButtonsStatus();
}
// ========================================================================================================
// 浮动弹窗通用工具
// ========================================================================================================
function removeHelperPopup() {
const popup = qS('#helper-popup');
if (popup) {
document.body.removeChild(popup);
}
const overlay = qS('#helper-overlay');
if (overlay) {
document.body.removeChild(overlay);
}
}
function createCloseButton(onclick) {
const close_btn = docre('button');
close_btn.className = 'helper-close-btn';
close_btn.type = 'button';
close_btn.addEventListener('click', onclick);
return close_btn;
}
function createOverlay(type = 'shield', onclick = removeHelperPopup) {
const overlay = docre('div');
overlay.id = 'helper-overlay';
overlay.className = 'helper-' + type + '-layer';
overlay.addEventListener('click', onclick);
return overlay;
}
function createLoadingOverlay() {
const overlay = docre('div');
overlay.id = 'helper-loading-overlay';
const spiner = docre('div');
spiner.className = 'helper-spinner';
overlay.appendChild(spiner);
return overlay;
}
function createPopupWithTitle(title) {
const popup = docre('div');
const overlay = createOverlay();
popup.id = 'helper-popup';
const helper_title_container = docre('div');
helper_title_container.id = 'helper-title-container';
popup.appendChild(helper_title_container);
const helper_title = docre('div');
helper_title.id = 'helper-title';
helper_title.textContent = title;
helper_title_container.appendChild(helper_title);
const close_btn = createCloseButton(removeHelperPopup);
helper_title_container.appendChild(close_btn);
const hr = docre('hr');
hr.className = 'helper-hr';
popup.appendChild(hr);
const content_container = docre('div');
content_container.id = 'helper-content-container';
popup.appendChild(content_container);
document.body.appendChild(overlay);
return popup;
}
function createCenterMessageDiv(message) {
const div = docre('div');
div.className = 'helper-center-message';
div.textContent = message;
return div;
}
function createTopProgressbar() {
const container = docre('div');
container.id = 'helper-top-progressbar-container';
const progressbar = docre('div');
progressbar.id = 'helper-top-progressbar';
container.appendChild(progressbar);
document.body.appendChild(container);
return progressbar;
}
function updateProgressbar(progress, dt, remove_timeout = 1500) {
if (progress && dt && dt > 0) {
progress.value += dt;
progress.bar.style.width = progress.value + '%';
if (progress.value >= 100) {
setTimeout(() => {
document.body.removeChild(progress.bar.parentNode);
}, remove_timeout);
}
}
}
// ========================================================================================================
// 助手设置组件
// ========================================================================================================
function createHelperSettingSelect(attr, options = [], texts = []) {
const status = hs[attr];
if (options.length == 0) {
options = [status];
}
const select = docre('select');
select.className = 'helper-select helper-active-component';
options.forEach(option => {
const opt = docre('option');
opt.value = option;
opt.textContent = texts[options.indexOf(option)] || option;
if (option == status) {
opt.selected = true;
}
select.appendChild(opt);
});
select.addEventListener('change', (e) => {
hs[attr] = e.target.value;
GM.setValue('helper_setting', hs);
});
return select;
}
function createHelperSettingSwitch(attr, onchange = null) {
const label = docre('label');
const checkbox = docre('input');
checkbox.type = 'checkbox';
checkbox.checked = hs[attr];
checkbox.addEventListener('change', (e) => {
hs[attr] = e.target.checked;
GM.setValue('helper_setting', hs);
if (onchange) {
onchange();
}
});
label.appendChild(checkbox);
const span = docre('span');
span.className = 'helper-toggle-switch helper-halfheight-active-component';
label.appendChild(span);
return label;
}
function createHelperSettingMultiCheck(multichecks) {
const container = docre('div');
container.className = 'helper-multicheck-container helper-active-component';
multichecks.forEach(option => {
const item = docre('div');
item.className = 'helper-multicheck-item';
const checkbox = docre('input');
checkbox.type = 'checkbox';
checkbox.checked = hs[option.attr];
checkbox.addEventListener('change', (e) => {
hs[option.attr] = e.target.checked;
GM.setValue('helper_setting', hs);
});
item.appendChild(checkbox);
const item_text = docre('div');
item_text.textContent = option.text;
item_text.className = 'helper-multicheck-text';
item_text.addEventListener('click', () => {
checkbox.checked = !checkbox.checked;
checkbox.dispatchEvent(new Event('change'));
});
item.appendChild(item_text);
container.appendChild(item);
});
return container;
}
function createHelperSettingButton(btn_text, onclick) {
const btn = docre('button');
btn.type = 'button';
btn.className = 'helper-setting-button helper-active-component';
btn.textContent = btn_text;
btn.addEventListener('click', onclick);
return btn;
}
function createHelperActiveComponent(type, args) {
switch (type) {
case 'switch':
return createHelperSettingSwitch(...args);
case 'multicheck':
return createHelperSettingMultiCheck(...args);
case 'select':
return createHelperSettingSelect(...args);
case 'button':
return createHelperSettingButton(...args);
default:
return docre('div');
}
}
function createHeperTagsContainer(tag_list, onremove = null, value_attr = null) {
const tag_container = docre('div');
tag_container.className = 'helper-tag-container';
const renderTags = () => {
tag_container.innerHTML = '';
tag_list.forEach((tag_info, index) => {
const tag = docre('div');
tag.className = 'helper-tag';
if (value_attr && (value_attr in tag_info)) {
tag.textContent = tag_info[value_attr];
}
else {
tag.textContent = tag_info;
}
const remove_btn = docre('button');
remove_btn.type = 'button';
remove_btn.className = 'helper-tag-remove-btn';
remove_btn.textContent = '×';
remove_btn.addEventListener('click', () => {
tag_list.splice(index, 1);
renderTags();
updatePageDoc();
if (onremove) {
onremove();
}
});
tag.appendChild(remove_btn);
tag_container.appendChild(tag);
});
};
renderTags();
return tag_container;
}
// ========================================================================================================
// 助手设置弹窗
// ========================================================================================================
function createFollowListTab() {
const followed_users = GM_getValue('followed_users', []);
if (followed_users.length == 0) {
return createCenterMessageDiv('暂无关注');
}
const table = docre('table');
table.className = 'helper-popup-table';
const table_head = docre('thead');
table.appendChild(table_head);
const title_row = table_head.insertRow();
const user_title = docre('th');
const thread_title = docre('th');
const follow_title = docre('th');
user_title.textContent = '用户';
thread_title.textContent = '关注内容';
follow_title.textContent = '操作';
[user_title, thread_title, follow_title].forEach(e => {
title_row.appendChild(e);
e.className = 'helper-sticky-header';
});
const table_body = docre('tbody');
table.appendChild(table_body);
for (let user of followed_users) {
const followed_threads = GM_getValue(user.uid + '_followed_threads', []);
const user_URL_params = { loc: 'home', mod: 'space', uid: user.uid };
if (followed_threads.some(e => e.tid == -1)) {
const row = table_body.insertRow();
const [user_cell, thread_cell, follow_cell] = [0, 1, 2].map(i => row.insertCell(i));
insertLink(user.name, user_URL_params, user_cell);
const thread_URL_params = { loc: 'home', mod: 'space', uid: user.uid, do: 'thread', view: 'me', type: 'reply', from: 'space' };
insertLink('所有回复', thread_URL_params, thread_cell);
follow_cell.appendChild(createFollowButton({ 'uid': user.uid, name: user.name, tid: -1, title: '所有回复' }));
continue;
}
for (let thread of followed_threads) {
const row = table_body.insertRow();
const [user_cell, thread_cell, follow_cell] = [0, 1, 2].map(i => row.insertCell(i));
insertLink(user.name, user_URL_params, user_cell);
let thread_URL_params;
if (thread.tid > 0) {
thread_URL_params = { loc: 'forum', mod: 'viewthread', tid: thread.tid };
}
else if (thread.tid == 0) {
thread_URL_params = { loc: 'home', mod: 'space', uid: user.uid, do: 'thread', view: 'me', from: 'space' };
}
insertLink(thread.title, thread_URL_params, thread_cell);
follow_cell.appendChild(createFollowButton({ uid: user.uid, name: user.name, tid: thread.tid, title: thread.title }));
}
}
return table;
}
function createHistoryNotificationTab() {
const notification_messages = GM_getValue('notification_messages', []);
if (notification_messages.length == 0) {
return createCenterMessageDiv('暂无历史通知');
}
const div = docre('div');
notification_messages.forEach(message => { div.innerHTML += message; });
return div;
}
function createBlockKeywordTab() {
const div = docre('div');
const title_div = docre('div');
title_div.className = 'helper-sticky-header';
const input = docre('input');
input.type = 'text';
input.placeholder = '输入屏蔽词并回车提交';
input.className = 'helper-input';
let tag_container = createHeperTagsContainer(hs.block_keywords, () => GM.setValue('helper_setting', hs));
input.addEventListener('keyup', e => {
if (e.key == 'Enter') {
const keyword = input.value.trim().toLowerCase();
if (keyword.length > 0 && !hs.block_keywords.includes(keyword)) {
hs.block_keywords.push(keyword);
GM.setValue('helper_setting', hs);
div.removeChild(tag_container);
tag_container = createHeperTagsContainer(hs.block_keywords, () => GM.setValue('helper_setting', hs));
div.appendChild(tag_container);
updatePageDoc();
}
input.value = '';
}
});
div.appendChild(input);
div.appendChild(tag_container);
return div;
}
function createBlacklistTab() {
return createHeperTagsContainer(hs.blacklist, () => GM.setValue('helper_setting', hs), 'name');
}
function createDebugTab() {
const div = docre('div');
const all_value = GM_listValues();
all_value.forEach(element => {
const p = docre('p');
p.textContent = element;
p.addEventListener('click', () => {
div.innerHTML = '';
div.textContent = element + ':' + JSON.stringify(GM_getValue(element));
});
div.appendChild(p);
});
return div;
}
function createHelperSettingTab(setting_type) {
const div = docre('div');
let components = [];
switch (setting_type) {
case 'read':
components = [
// 开启更新通知
{ title: '订阅更新通知', type: 'switch', args: ['enable_notification', () => setHidden(qS('#htb-follow'), !hs.enable_notification)] },
// 开启历史消息
{ title: '保存历史通知', type: 'switch', args: ['enable_history', () => setHidden(qS('#htb-history'), !hs.enable_history)] },
// 开启辅助换行
{ title: '自动换行', type: 'switch', args: ['enable_auto_wrap', updatePageDoc] },
// 开启屏蔽词
{
title: '标题关键词屏蔽', type: 'switch', args: ['enable_block_keyword', () => {
updatePageDoc();
setHidden(qS('#htb-block'), !hs.enable_block_keyword);
}
]
},
// 开启黑名单
{
title: '黑名单', type: 'switch', args: ['enable_blacklist', () => {
updatePageDoc();
setHidden(qS('#htb-blacklist'), !hs.enable_blacklist);
}]
}];
break;
case 'save':
components = [
// 选择下载内容
{
title: '保存帖子内容', type: 'multicheck', args: [[
{ attr: 'enable_text_download', text: '文本' },
{ attr: 'enable_postfile_download', text: '帖内资源' },
{ attr: 'enable_attach_download', text: '附件' },
{ attr: 'enable_op_download', text: '原创资源' }]]
},
// 选择文件打包模式
{
title: '帖内打包保存方式', type: 'select', args: [
'files_pack_mode',
['no', 'single', 'all'],
['不打包', '分类打包', '全部打包']]
},
// 开启自动回复
{
title: '保存后自动回复', type: 'switch', args: ['enable_auto_reply']
},
// 选择默认合并下载模式
{
title: '空间合并保存方式', type: 'select', args: [
'default_merge_mode',
['main', 'author', 'all'],
['主楼(合并)', '作者(打包)', '全帖(打包)']]
}
];
break;
case 'data':
components = [
// 清除历史消息
{
title: '清空历史通知', type: 'button', args: ['全部清空', () => {
const confirm = window.confirm('确定清空所有历史通知?');
if (confirm) {
GM.deleteValue('notification_messages');
location.reload();
}
}]
, hidden: !hs.enable_history
},
// 清除脚本数据
{
title: '清空脚本数据', type: 'button', args: ['全部清空', () => {
const confirm = window.confirm('确定清空脚本所有数据?');
if (confirm) {
GM_listValues().forEach(e => GM.deleteValue(e));
location.reload();
}
}]
}];
break;
}
components.forEach(component => {
const container = docre('div');
container.className = 'helper-setting-container';
const text_node = docre('div');
text_node.textContent = component.title;
container.appendChild(text_node);
const active_component = createHelperActiveComponent(component.type, component.args);
container.appendChild(active_component);
if (!component.hasOwnProperty('hidden')) {
component.hidden = false;
}
setHidden(container, component.hidden);
div.appendChild(container);
});
return div;
}
function createHelperPopup() {
const popup = createPopupWithTitle('湿热助手');
const content_container = qS('#helper-content-container', popup);
const tab_btn_container = docre('div');
tab_btn_container.id = 'helper-tab-btn-container';
tab_btn_container.className = 'helper-scroll-component';
content_container.appendChild(tab_btn_container);
const tab_content_container = docre('div');
tab_content_container.id = 'helper-tab-content-container';
tab_content_container.className = 'helper-scroll-component';
content_container.appendChild(tab_content_container);
const tabs = [
{ id: 'view', name: '浏览设置', func: () => createHelperSettingTab('read') },
{ id: 'save', name: '下载设置', func: () => createHelperSettingTab('save') },
{ id: 'data', name: '数据设置', func: () => createHelperSettingTab('data') },
{ id: 'follow', name: '关注列表', func: createFollowListTab, 'hidden': !hs.enable_notification },
{ id: 'history', name: '历史提醒', func: createHistoryNotificationTab, 'hidden': !hs.enable_history },
{ id: 'block', name: '标题屏蔽词', func: createBlockKeywordTab, 'hidden': !hs.enable_block_keyword },
{ id: 'blacklist', name: '黑名单', func: createBlacklistTab, 'hidden': !hs.enable_blacklist },
{ id: 'debug', name: '调试', func: createDebugTab, hidden: !hs.enable_debug_mode }];
const show_tab = content => {
tab_content_container.innerHTML = '';
tab_content_container.appendChild(content)
};
tabs.forEach((tab, index) => {
const btn = docre('button');
btn.type = 'button';
btn.id = 'htb-' + tab.id;
btn.className = 'helper-tab-btn';
btn.textContent = tab.name;
btn.addEventListener('click', () => {
qSA('button', tab_btn_container).forEach(e => e.classList.remove('helper-tab-selected'));
btn.classList.add('helper-tab-selected');
show_tab(tab.func());
});
if (index == 0) {
btn.classList.add('helper-tab-selected');
show_tab(tab.func());
}
if (!tab.hasOwnProperty('hidden')) {
tab.hidden = false;
}
setHidden(btn, tab.hidden);
tab_btn_container.appendChild(btn);
});
document.body.appendChild(popup);
}
// ========================================================================================================
// 消息提醒弹窗
// ========================================================================================================
function createNotificationPopup() {
const popup = docre('div');
popup.id = 'helper-notification-popup';
document.body.appendChild(popup);
const close_btn = createCloseButton(() => { popup.style.display = 'none' });
close_btn.style.position = 'absolute';
close_btn.style.top = '10px';
close_btn.style.right = '10px';
close_btn.classList.add('helper-redx');
popup.appendChild(close_btn);
}
async function updateNotificationPopup() {
const followed_users = await GM.getValue('followed_users', []);
if (followed_users.length > 0) {
let popup = qS('#helper-notification-popup');
let notification_messages = [];
let promises = [];
const createParaAndInsertUserNameLink = (user, parent) => {
const messageElement = docre('div');
messageElement.className = 'helper-noti-message';
parent.appendChild(messageElement);
const user_URL_params = { loc: 'home', mod: 'space', uid: user.uid };
const user_link = insertLink(user.name, user_URL_params, messageElement);
user_link.className = 'helper-ellip-link';
user_link.style.maxWidth = '30%';
user_link.style.color = 'inherit !important';
return messageElement;
}
const processNewInfos = (user, thread, new_infos) => {
let followed_threads = GM_getValue(user.uid + '_followed_threads', []);
let new_threads = new_infos.new;
const found_last = new_infos.found;
const last_tpid = new_infos.last_tpid;
if (new_threads.length > 0) {
updateListElements(followed_threads, { tid: thread.tid, last_tpid, title: thread.title }, true, (a, b) => a.tid == b.tid);
updateGMList(user.uid + '_followed_threads', followed_threads);
}
if (thread.last_tpid == 0 || new_threads.length == 0) { // 如果没有更新,或者是首次关注,则不发送消息
return notification_messages;
}
if (!popup) {
createNotificationPopup();
popup = qS('#helper-notification-popup');
}
const div = docre('div');
if (thread.tid != 0) {
for (let new_thread of new_threads) {
const thread_title = new_thread.title;
const messageElement = createParaAndInsertUserNameLink(user, div);
let message = ` 有`;
if (!found_last && thread.tid != -1) { // 在特定关注主题末页未找到不晚于last_pid的
message += '至少';
}
message += `${new_thread.pids.length}条新回复在 `;
const text_element = document.createTextNode(message);
messageElement.appendChild(text_element);
const thread_URL_params = { loc: 'forum', mod: 'redirect', goto: 'findpost', ptid: new_thread.tid, pid: new_thread.pids.at(-1) };
const thread_message = insertLink(thread_title, thread_URL_params, messageElement);
thread_message.className = 'helper-ellip-link';
}
if (!found_last && thread.tid == -1) { // 在空间回复页首页未找到不晚于last_pid的
const messageElement = createParaAndInsertUserNameLink(user, div);
const text_element2 = document.createTextNode(' 或有 ');
messageElement.appendChild(text_element2);
const reply_URL_params = { loc: 'home', mod: 'space', 'uid': user.uid, do: 'thread', view: 'me', type: 'reply', from: 'space' };
insertLink('更多新回复', reply_URL_params, messageElement);
}
}
else if (thread.tid == 0) {
const noti_threads = new_threads.filter(e => hs.important_fids.includes(e.fid));
hs.important_fids.forEach(fid => { new_threads = new_threads.filter(e => e.fid != fid) });
const notif_num = new_threads.length > hs.max_noti_threads ? hs.max_noti_threads : new_threads.length;
noti_threads.push(...new_threads.slice(0, notif_num));
for (let new_thread of noti_threads) {
const messageElement = createParaAndInsertUserNameLink(user, div);
const text_element = document.createTextNode(' 有新帖 ');
messageElement.appendChild(text_element);
const thread_URL_params = { loc: 'forum', mod: 'viewthread', tid: new_thread.tid };
const thread_message = insertLink(new_thread.title, thread_URL_params, messageElement);
thread_message.className = 'helper-ellip-link';
if (hs.important_fids.includes(new_thread.fid)) {
thread_message.style = 'color: red !important';
}
}
if (new_threads.length > 3) {
const messageElement = createParaAndInsertUserNameLink(user, div);
let message = ` 有另外 `;
if (!found_last) {
message += '至少';
}
const text_element = document.createTextNode(message);
messageElement.appendChild(text_element);
const thread_URL_params = { loc: 'home', mod: 'space', 'uid': user.uid, do: 'thread', view: 'me', from: 'space' };
insertLink(`${new_threads.length - 3}条新帖`, thread_URL_params, messageElement);
}
}
popup.appendChild(div);
notification_messages.push(div.innerHTML);
return notification_messages;
};
for (let user of followed_users) {
let followed_threads = GM_getValue(user.uid + '_followed_threads', []);
for (let thread of followed_threads) {
promises.push(
getUserNewestPostOrThread(user.uid, thread.tid, thread.last_tpid).then(
new_infos => processNewInfos(user, thread, new_infos)
)
);
}
}
if (hs.enable_history) {
await Promise.all(promises);
const old_notification_messages = GM_getValue('notification_messages', []);
notification_messages.push(...old_notification_messages);
updateGMList('notification_messages', notification_messages);
}
}
}
// ========================================================================================================
// 代表作弹窗
// ========================================================================================================
function createMasterpieceTable(masterpiece_info, sortby = 'view') {
const div = docre('div');
div.className = 'helper-scroll-component';
div.style.width = '100%';
div.style.paddingBottom = '10px';
const has_thread = ['view', 'reply'].some(e => masterpiece_info['max_' + e + '_threads'].length > 0);
if (!has_thread) {
div.appendChild(createCenterMessageDiv('暂无作品'));
return div;
}
let masterpiece_list = [];
const updateTable = sort_by => {
const table = docre('table');
table.className = 'helper-popup-table';
const table_head = docre('thead');
table.appendChild(table_head);
const title_row = table_head.insertRow();
const thread_title = docre('th');
const view_title = docre('th');
const reply_title = docre('th');
thread_title.textContent = '主题';
view_title.textContent = '浏览';
reply_title.textContent = '回复';
switch (sort_by) {
case 'reply':
reply_title.className = 'helper-sortby';
masterpiece_list = masterpiece_info.max_reply_threads;
view_title.addEventListener('click', () => {
div.innerHTML = '';
updateTable('view');
});
view_title.style.cursor = 'pointer';
break;
case 'view':
default:
view_title.className = 'helper-sortby';
masterpiece_list = masterpiece_info.max_view_threads;
reply_title.addEventListener('click', () => {
div.innerHTML = '';
updateTable('reply');
});
reply_title.style.cursor = 'pointer';
}
[thread_title, view_title, reply_title].forEach(e => title_row.appendChild(e));
const table_body = docre('tbody');
table.appendChild(table_body);
for (let thread of masterpiece_list) {
const row = table_body.insertRow();
const [thread_cell, view_cell, reply_cell] = [0, 1, 2].map(i => row.insertCell(i));
const thread_URL_params = { loc: 'forum', mod: 'viewthread', tid: thread.tid };
insertLink(thread.title, thread_URL_params, thread_cell);
thread_cell.innerHTML = (thread.spanHTML ?? '') + thread_cell.innerHTML;
view_cell.textContent = thread.views;
reply_cell.textContent = thread.replies;
}
div.appendChild(table);
};
updateTable();
return div;
}
async function createMasterpiecePopup(uid, user_name) {
const popup = createPopupWithTitle(`${user_name} 的代表作`);
document.body.appendChild(popup);
const content_container = qS('#helper-content-container', popup);
let masterpiece_info = GM_getValue(uid + '_masterpiece', { update_time: 0, max_view_threads: [], max_reply_threads: [] });
const loadContent = async () => {
content_container.appendChild(createLoadingOverlay());
const top_progressbar = createTopProgressbar();
let progress = { value: 0, bar: top_progressbar };
masterpiece_info = await updateMasterpiece(uid, progress);
content_container.innerHTML = '';
};
if (Date.now() - masterpiece_info.update_time > hs.data_cache_time) {
await loadContent();
}
content_container.appendChild(createMasterpieceTable(masterpiece_info, hs.default_masterpiece_sort));
const updateFootnote = () => {
const footnote = docre('div');
content_container.appendChild(footnote);
footnote.className = 'helper-footnote';
const update_time_ago = timeAgo(masterpiece_info.update_time);
footnote.textContent = `缓存时间:${update_time_ago}`;
if (update_time_ago != '今天内' || hs.enable_debug_mode) {
footnote.textContent += ' | ';
const reload_link = insertInteractiveLink('立即刷新', async () => {
await loadContent();
content_container.appendChild(createMasterpieceTable(masterpiece_info, hs.default_masterpiece_sort));
updateFootnote();
}, footnote);
reload_link.style.color = 'inherit !important';
}
};
updateFootnote();
}
async function updateMasterpiece(uid, progress = null) {
const getUserThreadNum = async (uid) => {
const URL_params = { loc: 'home', mod: 'space', uid: uid, do: 'profile' };
const page_doc = await getPageDocInDomain(URL_params);
const thread_info = qS('#ct > div.mn > div > div.bm_c > div > div:nth-child(1) > ul.cl.bbda.pbm.mbm > li > a:nth-child(12)', page_doc);
const thread_num = Number(thread_info.textContent.slice(4));
return thread_num > 0 ? thread_num : 1;
};
const max_page = Math.ceil(await getUserThreadNum(uid) / 20);
updateProgressbar(progress, 10);
const dt = decimalCeil(90 / max_page);
let threads = [];
let promises = Array.from({ length: max_page }, (v, k) => k + 1).map(page_num => new Promise(async (resolve, reject) => {
const URL_params = { loc: 'home', mod: 'space', 'uid': uid, do: 'thread', view: 'me', page: page_num, mobile: '2' };
const page_doc = await getPageDocInDomain(URL_params, mobileUA);
const threads_in_page = qSA('li.list', page_doc);
for (let thread of threads_in_page) {
const title_node = qS('.threadlist_tit', thread);
const spanHTML = qS('span', title_node)?.outerHTML;
const title = qS('em', title_node).textContent;
const tid = title_node.parentNode.href.parseURL().tid;
const views = Number(qS('.dm-eye-fill', thread).nextSibling.textContent);
const replies = Number(qS('.dm-chat-s-fill', thread).nextSibling.textContent);
threads.push({ tid, spanHTML, title, views, replies });
}
updateProgressbar(progress, dt);
resolve();
}));
await Promise.all(promises);
const max_view_threads = threads.sort((a, b) => b.views - a.views).slice(0, hs.masterpiece_num);
const max_reply_threads = threads.sort((a, b) => b.replies - a.replies).slice(0, hs.masterpiece_num);
const masterpiece_info = { update_time: Date.now(), max_view_threads, max_reply_threads };
GM.setValue(uid + '_masterpiece', masterpiece_info);
return masterpiece_info;
}
// ========================================================================================================
// 插入表情
// ========================================================================================================
async function modifySmiliesArray(new_smilies) {
await checkVariableDefined('smilies_array');
for (let smilies of new_smilies) {
smilies_type['_' + smilies.type] = [smilies.name, smilies.path];
smilies_array[smilies.type] = new Array();
smilies_array[smilies.type][1] = smilies.info;
}
}
async function modifySmiliesSwitch(original_smilies_types, mode = 'img') {
await checkVariableDefined('smilies_switch');
let smilies_switch_str = unsafeWindow['smilies_switch'].toString();
smilies_switch_str = smilies_switch_str.replace("STATICURL+'image/smiley/'+smilies_type['_'+type][1]+'/'", `('${original_smilies_types}'.split(',').includes(type.toString())?(STATICURL+'image/smiley/'+smilies_type['_'+type][1]+'/'):smilies_type['_'+type][1])`);
if (mode == 'img') {
// TODO fastpost时有问题
smilies_switch_str = smilies_switch_str.replace("'insertSmiley('+s[0]+')'", `"insertText('[img]"+smilieimg+"[/img]',strlen('[img]"+smilieimg+"[/img]'),0)"`);
}
smilies_switch = new Function('return ' + smilies_switch_str)();
}
async function insertExtraSmilies(id, seditorkey, original_smilies_types, new_smilies) {
await modifySmiliesArray(new_smilies);
await modifySmiliesSwitch(original_smilies_types, 'img');
smilies_show(id, 8, seditorkey);
}
async function modifyBBCode2Html(original_smilies_types) {
// 可以正常使用,但由于modifyPostOnSubmit的缘故,同步弃用
await checkVariableDefined('bbcode2html');
let bbcode2html_str = unsafeWindow['bbcode2html'].toString();
bbcode2html_str = bbcode2html_str.replace("STATICURL+'image/smiley/'+smilies_type['_'+typeid][1]+'/'", `('${original_smilies_types}'.split(',').includes(typeid.toString())?(STATICURL+'image/smiley/'+smilies_type['_'+typeid][1]+'/'):smilies_type['_'+typeid][1])`);
bbcode2html_str = bbcode2html_str.replace("}if(!fetchCheckbox('bbcodeoff')&&allowbbcode){", "}if(!fetchCheckbox('bbcodeoff')&&allowbbcode){")
bbcode2html = new Function('return ' + bbcode2html_str)();
}
async function modifyPostOnSubmit(submit_id, original_smilies_types) {
// TODO 不知道为什么,不这么做的话自定义表情在提交时会被转义成bbcode
// TODO 但是对于fastpost的情况还是没法处理,所以暂时弃用
const post = qS('#' + submit_id);
submit_id = submit_id.replace('form', '');
// const original_onsubmit_str = post.getAttribute('onsubmit').toString();
post.setAttribute('onsubmit', `if(typeof smilies_type == 'object'){for (var typeid in smilies_array){for (var page in smilies_array[typeid]){for(var i in smilies_array[typeid][page]){re=new RegExp(preg_quote(smilies_array[typeid][page][i][1]),"g");this.message.value=this.message.value.replace(re,'[img]'+('${original_smilies_types}'.split(',').includes(typeid.toString())?(STATICURL+'image/smiley/'+ smilies_type['_' + typeid][1] + '/'):smilies_type['_' + typeid][1])+smilies_array[typeid][page][i][2]+"[/img]");}}}}`);
}
// ========================================================================================================
// 窗口事件和DOM属性
// ========================================================================================================
window.addEventListener('keydown', e => {
if (e.key == 'Escape') {
const noti_popup = qS('#helper-notification-popup');
if (noti_popup && noti_popup.style.display != 'none') {
noti_popup.style.display = 'none';
}
else {
removeHelperPopup();
}
}
});
document.original_url = document.URL;
// ========================================================================================================
// 主体运行
// ========================================================================================================
modifyPageDoc();
})();
// 功能优化
// TODO 使用倒序浏览替代large_page_num
// TODO 保存文本链接处理
// TODO 版面浮动名片、好友浮动名片添加代表作、关注、拉黑
// TODO 进度条优化:saveThread中的getAllPageContent
// TODO 自动切换全帖/选中:显示已选
// TODO 滚动条悬停显示
// TODO 设置按钮hover
// TODO 支持firefox
// TODO 保存的文件名是否要带小分区名
// TODO 考虑op未加载的情况
// 设置优化
// TODO 换行参数
// TODO 提醒参数
// TODO 获取帖子信息参数
// TODO 显示按钮和订阅更新分开设置
// TODO 代表作数量
// TODO 历史消息上限
// TODO 插入TAB设置
// TODO 恢复默认设置
// TODO 是否重命名附件
// 调试模式
// TODO 导出关注
// TODO 删除键值
// 代码优化
// TODO 添加debug log
// TODO css classname data清理
// TODO 使用?. ?? 运算符替代if判断
// TODO 使用hasOwnProperty替代in判断
// FIXME getSpaceAuthor
// TODO 使用nodename替代tagname
// TODO 避免getAllPageContent中first page重复获取
// 搁置: 麻烦
// FIXME 置顶重复
// FIXME 历史消息重复
// TODO md格式
// TODO 图片不区分楼层
// NOTE 可能会用到 @require https://scriptcat.org/lib/513/2.0.0/ElementGetter.js