// ==UserScript==
// @name PatreonExpander
// @namespace https://github.com/frosn0w/iOSscripts
// @version 2.25.324
// @description Simplify elements, expand contents and comments
// @author frosn0w
// @match *://*.patreon.com/*
// @run-at document-end
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MzYgNDc2Ij48dGl0bGU+UGF0cmVvbiBsb2dvPC90aXRsZT48cGF0aCBkYXRhLWZpbGw9IjEiIGQ9Ik00MzYgMTQzYy0uMDg0LTYwLjc3OC00Ny41Ny0xMTAuNTkxLTEwMy4yODUtMTI4LjU2NUMyNjMuNTI4LTcuODg0IDE3Mi4yNzktNC42NDkgMTA2LjIxNCAyNi40MjQgMjYuMTQyIDY0LjA4OS45ODggMTQ2LjU5Ni4wNTEgMjI4Ljg4M2MtLjc3IDY3LjY1MyA2LjAwNCAyNDUuODQxIDEwNi44MyAyNDcuMTEgNzQuOTE3Ljk0OCA4Ni4wNzItOTUuMjc5IDEyMC43MzctMTQxLjYyMyAyNC42NjItMzIuOTcyIDU2LjQxNy00Mi4yODUgOTUuNTA3LTUxLjkyOUMzOTAuMzA5IDI2NS44NjUgNDM2LjA5NyAyMTMuMDExIDQzNiAxNDNaIj48L3BhdGg+PC9zdmc+
// @grant none
// @license MIT
// ==/UserScript==
// 常量统一管理
const CONFIG = {
MAX_EXECUTIONS: 50,
INTERVAL_DELAY: 2888,
REMAIN_DAYS: 1,
STYLE_ID: "patreon-expander-styles",
TARGET_SELECTORS: {
POST_TITLE: 'span[data-tag="post-title"]',
COMMENT_ROW: 'div[data-tag="comment-row"]',
// 添加更多常用选择器...
},
};
// 全局样式(合并所有样式,避免重复注入)
const globalStyles = `
p { line-height: 1.4 !important; }
div[data-tag="comment-row"]::before,
div[data-tag="comment-row"]::after {
width: 0 !important;
border-left: 0 !important;
}
.TAI-body-div p {
margin: 8px 0 !important;
}
.TAI-body-div ul {
margin: 6px 0 !important;
}
`;
// 日期处理器(避免重复计算)
const DateFormatter = {
currentDate: new Date(),
refresh() {
this.currentDate = new Date();
},
get formattedDate() {
return `${this.currentMonth}月${this.currentDay}日`;
},
get yesterdayDate() {
const date = new Date(this.currentDate);
date.setDate(date.getDate() - 1);
return `${date.getMonth() + 1}月${date.getDate()}日`;
},
get currentMonth() {
return this.currentDate.getMonth() + 1;
},
get currentDay() {
return this.currentDate.getDate();
},
};
// 安全移除函数
const safeRemove = (() => {
const removedCache = new WeakSet();
return (element, selector = null, levels = 0) => {
if (!element || !element?.parentElement || removedCache.has(element)) {
return false;
}
try {
const target = selector ? element.closest(selector) : element;
if (!target) return false;
let parent = target;
for (let i = 0; i < levels; i++) parent = parent?.parentElement;
if (!parent) return false;
parent.remove();
removedCache.add(element);
return true;
} catch (error) {
console.error("Removal failed:", error);
return false;
}
};
})();
// 移除过期发布
function shouldRemovePost(text) {
const { currentMonth, currentDay } = DateFormatter;
return (
text.includes(" 天前") ||
(text.includes(`${currentMonth}月`) &&
parseInt(text.split(`${currentMonth}月`)[1]) <
currentDay - CONFIG.REMAIN_DAYS) ||
(text.includes(`${currentMonth}月`) &&
parseInt(text.split(`${currentMonth}月`)[0]) === currentMonth - 1)
);
}
// 处理 link
function processLinks(element) {
const { textContent, href, dataset } = element;
switch (true) {
case /小时前|分钟前/.test(textContent):
element.textContent = DateFormatter.formattedDate;
break;
case textContent.includes("昨天"):
element.textContent = DateFormatter.yesterdayDate;
break;
case dataset.tag === "post-published-at" && shouldRemovePost(textContent):
safeRemove(element, 'div[data-tag="post-card"]', 2);
break;
// 删除赠送卡片
case href === "https://www.patreon.com/user/gift?u=80821958":
safeRemove(element, null, 4);
break;
case textContent === "Skip navigation":
element.remove();
break;
case dataset.tag === "comment-avatar-wrapper":
safeRemove(element, null, 1);
break;
}
}
// 处理 button
function processButtons(button) {
const { textContent, ariaExpanded, ariaLabel, dataset } = button;
const BUTTON_INTERVALS = {
展开: 2888,
加载更多留言: 1688,
加载回复: 1888,
};
switch (true) {
case ariaExpanded === "false" && ariaLabel === "打开导航":
safeRemove(button, "header");
break;
case ["收起", "收起回复"].includes(textContent):
safeRemove(button, null, 1);
break;
case textContent === "展开":
case textContent === "加载更多留言":
case textContent === "加载回复":
if (!button.autoClicker) {
button.autoClicker = setInterval(() => {
document.contains(button)
? button.click()
: clearInterval(button.autoClicker);
}, BUTTON_INTERVALS[textContent]);
}
break;
case dataset.tag === "commenter-name" &&
textContent === "贝乐斯 Think Analyze Invest":
safeRemove(button, '[data-tag="commenter-name"]');
break;
}
}
// 处理 Div
function processDivs(element) {
const { dataset, id, ariaExpanded, textContent } = element;
switch (true) {
case id === "main-app-navigation":
safeRemove(element, null, 1);
break;
// 移除导航栏
case ariaExpanded === "false" && textContent.includes("我的会籍"):
safeRemove(element, "nav", 3);
break;
// 移除头图
case dataset.tag === "creation-name" &&
textContent.includes("Love & Peace !"):
safeRemove(element, null, 4);
break;
// 移除搜索框
case dataset.tag === "search-input-box":
safeRemove(element, null, 5);
break;
case dataset.tag === "chip-container":
safeRemove(element, null, 2);
break;
case dataset.tag === "post-details":
safeRemove(element);
break;
// 移除已删除留言区域
case dataset.tag === "comment-body" &&
textContent.includes("此留言已被删除。"):
safeRemove(element, null, 3);
break;
// 移除评论相关功能组件
case dataset.tag === "comment-actions":
safeRemove(element);
break;
case dataset.tag === "comment-field-box":
safeRemove(element, null, 3);
break;
// 缩窄页边距
case dataset.tag === "post-stream-container":
element.parentNode?.style.setProperty("padding-left", "4px");
element.parentNode?.style.setProperty("padding-right", "4px");
break;
}
}
// 通过span插入CSS标签,控制正文部分格式
function processSpans(element) {
if (element.getAttribute("data-tag") === "post-title") {
element.parentNode?.parentNode?.parentNode?.parentNode?.classList.add(
"TAI-body-div"
);
}
}
// 主逻辑
(function init() {
const style = document.createElement("style");
style.id = CONFIG.STYLE_ID;
style.textContent = globalStyles;
document.head.appendChild(style);
let executionCount = 0;
const interval = setInterval(() => {
if (executionCount >= CONFIG.MAX_EXECUTIONS) return clearInterval(interval);
DateFormatter.refresh();
document.querySelectorAll("a").forEach(processLinks);
document.querySelectorAll("button").forEach(processButtons);
document.querySelectorAll("div").forEach(processDivs);
document.querySelectorAll("span").forEach(processSpans);
executionCount++;
}, CONFIG.INTERVAL_DELAY);
})();