// ==UserScript==
// @name RSS Viewer
// @name:zh-CN RSS 预览
// @name:zh-TW RSS 預覽
// @name:ja RSSビューア
// @name:ko RSS 뷰어
// @namespace https://github.com/houoop/rss-viewer
// @version 1.0.0
// @description Convert XML RSS feed to a human-readable RSS interface with dual-panel layout, theme switching, and responsive design
// @description:zh-CN 将XML RSS源转换为人类可读的RSS界面,支持双栏布局、主题切换和响应式设计
// @description:zh-TW 將XML RSS源轉換為人類可讀的RSS界面,支持雙欄佈局、主題切換和響應式設計
// @description:ja XML RSSフィードを人間が読めるRSSインターフェースに変換、デュアルパネルレイアウト、テーマ切り替え、レスポンシブデザインをサポート
// @description:ko XML RSS 피드를 인간이 읽을 수 있는 RSS 인터페이스로 변환, 듀얼 패널 레이아웃, 테마 전환, 반응형 디자인 지원
// @author houoop
// @license MIT
// @homepage https://github.com/houoop/rss-viewer
// @supportURL https://github.com/houoop/rss-viewer/issues
// @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbyB3dy5zdmdyZXBvLmNvbSwgR2VuZXJhdG9yOiBTVkcgUmVwbyBNaXhlciBUb29scyAtLT4KPHN2ZyBmaWxsPSIjMDAwMDAwIiBoZWlnaHQ9IjgwMHB4IiB3aWR0aD0iODAwcHgiIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCA0OTAgNDkwIiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+CjxwYXRoIGQ9Ik0xNDQuODU2LDM0Ni42MjVoMjAuNjA2YzcuNjU2LDAsMTEuNDg0LDMuODg4LDExLjQ4NCwxMS42NjR2MTMuNzQyaDE0LjI5NnYtMTYuNjEzYzAtOS4yNDEtNC4xNzItMTQuMTMxLTEyLjUzMS0xNC42NAoJCXYtMC40OTNjNy42NTYtMS4wNzcsOC45ODctMy4wMDYsMTAuNzM3LTUuODAyYzEuNzUtMi43OTYsMi42MTctOCwyLjYxNy0xNS42MjZjMC04LjM4OS0xLjczNS0xNC4xOTEtNS4yMzQtMTcuMzkxCgkJYy0zLjQ5OS0zLjItOS44NjktNC44LTE5LjExMS00LjhoLTM3LjE2djc1LjM2NmgxNC4yOTZWMzQ2LjYyNXogTTE0NC44NTYsMzA4LjcwMmgyMS41MzNjNC42OCwwLDcuNzE2LDAuNzc4LDkuMDkyLDIuMzMzCgkJYzEuMzc2LDEuNTcsMi4wNjQsNS4wMzksMi4wNjQsMTAuNDIzYzAsNS4yNjQtMC43OTMsOC43NjMtMi4zOTMsMTAuNTEyYy0xLjYsMS43NS00Ljg2LDIuNjE3LTkuNzUsMi42MTdoLTIwLjU0NlYzMDguNzAyeiIvPgo8cGF0aCBkPSJNMjMyLjE3LDM2MS4yMDRjLTguMjEsMC0xMy4yMzQtMC41ODMtMTUuMDg4LTEuNzY0Yy0xLjgzOS0xLjE4MS0yLjc1Mi00LjM5Ni0yLjc1Mi05LjY2bC0wLjA2LTEuNmgtMTMuOTA3bDAuMDQ1LDIuNzUxCgkJYzAsOC43NjMsMS45NDQsMTQuNTgsNS44MzIsMTcuNDUxYzMuODg4LDIuODcxLDExLjc1NCw0LjMwNywyMy41OTcsNC4zMDdjMTMuMTg5LDAsMjEuNzEzLTEuNDY2LDI1LjU3MS00LjM4MQoJCWMzLjg3My0yLjkzMSw1LjgwMi05LjQwNiw1LjgwMi0xOS40MWMwLTguMTM1LTEuNjQ1LTEzLjQ4OC00Ljk1LTE2LjA3NWMtMy4yOS0yLjU3Mi0xMC41NTctNC4xNzItMjEuNzczLTQuOAoJCWMtOS40OTUtMC41MDgtMTUuMTAzLTEuMjU2LTE2Ljc5My0yLjIxM2MtMS42OS0wLjk1Ny0yLjU0Mi0zLjgxMy0yLjU0Mi04LjU1M2MwLTQuMDA4LDEuMDQ3LTYuNjM5LDMuMTI1LTcuODk1CgkJYzIuMDc5LTEuMjU2LDYuNTA1LTEuODg0LDEzLjI3OS0xLjg4NGM1Ljc0MiwwLDkuMzkxLDAuNTUzLDEwLjkzMSwxLjYzYzEuNTQsMS4wOTIsMi40ODIsMy43NTMsMi44MTEsNy45ODUKCQljMCwwLjMyOSwwLjA0NSwwLjgzNywwLjEyLDEuNTRoMTMuOTY3di0yLjg3MWMwLTcuODA2LTEuOTc0LTEzLjA0LTUuOTA3LTE1LjczMWMtMy45NDgtMi42OTItMTEuNTc0LTQuMDM4LTIyLjkyNC00LjAzOAoJCWMtMTEuOTYzLDAtMTkuOTMzLDEuNDY2LTIzLjkyNiw0LjM5NmMtMy45OTMsMi45MTYtNS45OTYsOC43OTMtNS45OTYsMTcuNTg1YzAsOC42MTMsMS42NiwxNC4yMjEsNC45NjUsMTYuODM4CgkJYzMuMzIsMi42MTcsMTAuOTQ2LDQuMjc3LDIyLjg2NCw0Ljk2NWw3Ljg5NSwwLjUwOGM0LjQ1NiwwLjI1NCw3LjM1NywwLjk4Nyw4LjcwMywyLjE4M2MxLjM0NiwxLjE5NiwyLjAxOSwzLjYwNCwyLjAxOSw3LjI1MwoJCWMwLDQuOTM1LTAuODk3LDguMDktMi42NjIsOS40NTFDMjQyLjY1MiwzNjAuNTMyLDIzOC41NywzNjEuMjA0LDIzMi4xNywzNjEuMjA0eiIvPgo8cGF0aCBkPSJNMjk5Ljg1LDM2MS4yMDRjLTguMTk1LDAtMTMuMjE5LTAuNTgzLTE1LjA3My0xLjc2NGMtMS44MzktMS4xODEtMi43NTItNC4zOTYtMi43NTItOS42NmwtMC4wNi0xLjZoLTEzLjkwN2wwLjA0NSwyLjc1MQoJCWMwLDguNzYzLDEuOTQ0LDE0LjU4LDUuODMyLDE3LjQ1MWMzLjg4OCwyLjg3MSwxMS43NTQsNC4zMDcsMjMuNjEyLDQuMzA3YzEzLjE1OSwwLDIxLjY4My0xLjQ2NiwyNS41NzEtNC4zODEKCQljMy44NTgtMi45MzEsNS44MDItOS40MDYsNS44MDItMTkuNDFjMC04LjEzNS0xLjY3NS0xMy40ODgtNC45NjUtMTYuMDc1Yy0zLjI5LTIuNTcyLTEwLjU1Ny00LjE3Mi0yMS43NzMtNC44CgkJYy05LjQ5NS0wLjUwOC0xNS4xMDMtMS4yNTYtMTYuNzkzLTIuMjEzYy0xLjY5LTAuOTU3LTIuNTQyLTMuODEzLTIuNTQyLTguNTUzYzAtNC4wMDgsMS4wNDctNi42MzksMy4xMjUtNy44OTUKCQljMi4wNzktMS4yNTYsNi41MDUtMS44ODQsMTMuMjc5LTEuODg0YzUuNzQyLDAsOS4zOTEsMC41NTMsMTAuOTQ2LDEuNjNjMS41MjUsMS4wOTIsMi40ODIsMy43NTMsMi44MTEsNy45ODUKCQljMCwwLjMyOSwwLjAzLDAuODM3LDAuMTIsMS41NGgxMy45Njd2LTIuODcxYzAtNy44MDYtMS45NzQtMTMuMDQtNS45MjItMTUuNzMxYy0zLjk0OC0yLjY5Mi0xMS41NzQtNC4wMzgtMjIuOTA5LTQuMDM4CgkJYy0xMS45NzgsMC0xOS45NDgsMS40NjYtMjMuOTQxLDQuMzk2Yy0zLjk5MywyLjkxNi01Ljk5Niw4Ljc5My01Ljk5NiwxNy41ODVjMCw4LjYxMywxLjY2LDE0LjIyMSw0Ljk2NSwxNi44MzgKCQljMy4zMiwyLjYxNywxMC45NDYsNC4yNzcsMjIuODc5LDQuOTY1bDcuODk2LDAuNTA4YzQuNDU2LDAuMjU0LDcuMzU3LDAuOTg3LDguNzAzLDIuMTgzYzEuMzE2LDEuMTk2LDIuMDA0LDMuNjA0LDIuMDA0LDcuMjUzCgkJYzAsNC45MzUtMC44OTcsOC4wOS0yLjY2Miw5LjQ1MUMzMTAuMzQ3LDM2MC41MzIsMzA2LjI4LDM2MS4yMDQsMjk5Ljg1LDM2MS4yMDR6Ii8+CjxwYXRoIGQ9Ik03Ny43ODgsMHYyNjUuMTExSDQyLjE4OXYxMzkuNjE1aDAuMDAxbDM1LjU5LDM1LjU5MUw3Ny43ODgsNDkwaDM3MC4wMjNWMTAyLjQyMkwzNDUuMzg4LDBINzcuNzg4eiBNMzk1Ljc5MywzODkuNDEzCgkJSDU3LjUwMXYtMTA4Ljk5aDMzOC4yOTJWMzg5LjQxM3ogTTM1My4wMjIsMzYuOTYybDU3LjgxNiw1Ny44MDRoLTU3LjgxNlYzNi45NjJ6Ii8+CjwvZz4KPC9zdmc+
// @match *://*/*
// @grant GM_xmlhttpRequest
// @connect cdn.jsdelivr.net
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/rss-parser.min.js
// @run-at document-end
// ==/UserScript==
(function () {
"use strict";
/*
* RSS Viewer - RSS 预览
* Version: 1.0.0
* Author: houoop
* License: MIT
*
* 将XML RSS源转换为人类可读的RSS界面
* Convert XML RSS feed to a human-readable RSS interface
*
* ## 更新日志 | Changelog
*
* ### v1.0.0 (2024-08-23)
* - 初始版本发布
* - 支持RSS/Atom格式解析
* - 双栏布局界面
* - 明暗主题切换
* - 返回顶部按钮
* - 响应式设计
* - 多语言支持 (中文简体/繁体、英文、日文、韩文)
* - 文章预览和分类标签
* - 平滑动画效果
*
* ## 功能特性 | Features
* - 自动检测RSS源并转换为可读界面
* - 支持多种RSS格式 (RSS, Atom, XML)
* - 双栏布局:左侧文章列表,右侧内容展示
* - 明暗主题切换,支持主题记忆
* - 响应式设计,适配移动设备
* - 多语言界面支持
* - 文章预览和分类标签
* - 平滑滚动和动画效果
* - 自定义滚动条样式
*/
// 多语言支持
const i18n = {
en: {
themeToggle: "Toggle Theme",
backToTop: "Back to Top",
lastUpdate: "Last Update",
author: "Author",
publishTime: "Published",
categories: "Categories",
noDescription: "No description available",
articleCount: "Articles",
},
"zh-CN": {
themeToggle: "切换主题",
backToTop: "返回顶部",
lastUpdate: "最后更新",
author: "作者",
publishTime: "发布时间",
categories: "分类",
noDescription: "暂无描述",
articleCount: "篇文章",
},
"zh-TW": {
themeToggle: "切換主題",
backToTop: "返回頂部",
lastUpdate: "最後更新",
author: "作者",
publishTime: "發布時間",
categories: "分類",
noDescription: "暫無描述",
articleCount: "篇文章",
},
ja: {
themeToggle: "テーマ切り替え",
backToTop: "トップに戻る",
lastUpdate: "最終更新",
author: "著者",
publishTime: "公開日時",
categories: "カテゴリ",
noDescription: "説明なし",
articleCount: "記事",
},
ko: {
themeToggle: "테마 전환",
backToTop: "맨 위로",
lastUpdate: "마지막 업데이트",
author: "작성자",
publishTime: "게시 시간",
categories: "카테고리",
noDescription: "설명 없음",
articleCount: "개의 글",
},
};
// 获取浏览器语言
function getBrowserLanguage() {
const lang = navigator.language.toLowerCase();
if (lang.startsWith("zh")) {
return lang.includes("tw") || lang.includes("hk") ? "zh-TW" : "zh-CN";
} else if (lang.startsWith("ja")) {
return "ja";
} else if (lang.startsWith("ko")) {
return "ko";
}
return "en";
}
// 获取翻译文本
function t(key) {
const lang =
localStorage.getItem("rss-reader-language") || getBrowserLanguage();
return i18n[lang]?.[key] || i18n["en"][key];
}
// 检查页面是否是RSS源
function isRSSFeed() {
// 检查Content-Type
const contentType = document.contentType?.toLowerCase() || "";
const validContentTypes = [
"application/rss+xml",
"application/atom+xml",
"application/xml",
"text/xml",
"text/plain", // 有些RSS使用text/plain
];
if (validContentTypes.includes(contentType)) {
return true;
}
return false;
}
// 提取和解析RSS内容
async function extractRSS() {
try {
// 检查RSSParser是否可用
if (typeof RSSParser === "undefined") {
console.error("RSSParser 未加载,请检查网络连接或脚本管理器设置");
return null;
}
// 获取XML内容
let xmlContent;
let prettyPrint = document.querySelector("#webkit-xml-viewer-source-xml");
if (prettyPrint) {
xmlContent = prettyPrint.innerHTML;
} else {
switch (document.contentType) {
// case "application/rss+xml":
// xmlContent = document.querySelector(
// "#webkit-xml-viewer-source-xml"
// ).innerHTML;
// break;
// case "application/atom+xml":
// xmlContent = document.querySelector(
// "#webkit-xml-viewer-source-xml"
// ).innerHTML;
// break;
// case "application/xml":
// xmlContent = document.querySelector(
// "#webkit-xml-viewer-source-xml"
// ).innerHTML;
// break;
case "text/xml":
xmlContent = new XMLSerializer().serializeToString(document);
break;
case "text/plain":
xmlContent = document.body.textContent;
break;
default:
console.warn("未知的Content-Type,尝试提取页面内容");
xmlContent = document.body.textContent;
}
}
// 使用RSSParser解析XML
const parser = new RSSParser();
const parsedFeed = await parser.parseString(xmlContent);
console.log("Feed解析结果:", parsedFeed);
parsedFeed.items.forEach((item) => {
Object.keys(item).forEach((key) => {
if (key !== "content:encoded") {
if (typeof item[key] === "string") {
item[key] = item[key].replace(/&/g, "&");
item[key] = item[key].replace(/</g, "<");
item[key] = item[key].replace(/>/g, ">");
item[key] = item[key].replace(/"/g, '"');
item[key] = item[key].replace(/'/g, "'");
item[key] = item[key].replace(/'/g, "'");
item[key] = item[key].replace(/ /g, " ");
item[key] = item[key].replace(/—/g, "—");
item[key] = item[key].replace(/–/g, "–");
item[key] = item[key].replace(/…/g, "…");
item[key] = item[key].replace(/“/g, "“");
item[key] = item[key].replace(/”/g, "”");
item[key] = item[key].replace(/‘/g, "‘");
item[key] = item[key].replace(/’/g, "’");
item[key] = item[key].replace(/«/g, "«");
item[key] = item[key].replace(/»/g, "»");
item[key] = item[key].replace(/•/g, "•");
}
}
});
});
//遍历parsedFeed的每个字符属性,如果属性是字符串,则取消转义
Object.keys(parsedFeed).forEach((key) => {
if (typeof parsedFeed[key] === "string") {
parsedFeed[key] = parsedFeed[key].replace(/&/g, "&");
parsedFeed[key] = parsedFeed[key].replace(/</g, "<");
parsedFeed[key] = parsedFeed[key].replace(/>/g, ">");
parsedFeed[key] = parsedFeed[key].replace(/"/g, '"');
parsedFeed[key] = parsedFeed[key].replace(/'/g, "'");
parsedFeed[key] = parsedFeed[key].replace(/'/g, "'");
parsedFeed[key] = parsedFeed[key].replace(/ /g, " ");
parsedFeed[key] = parsedFeed[key].replace(/—/g, "—");
parsedFeed[key] = parsedFeed[key].replace(/–/g, "–");
parsedFeed[key] = parsedFeed[key].replace(/…/g, "…");
parsedFeed[key] = parsedFeed[key].replace(/“/g, "“");
parsedFeed[key] = parsedFeed[key].replace(/”/g, "”");
parsedFeed[key] = parsedFeed[key].replace(/‘/g, "‘");
parsedFeed[key] = parsedFeed[key].replace(/’/g, "’");
parsedFeed[key] = parsedFeed[key].replace(/«/g, "«");
parsedFeed[key] = parsedFeed[key].replace(/»/g, "»");
parsedFeed[key] = parsedFeed[key].replace(/•/g, "•");
}
});
return {
title: parsedFeed.title || "RSS Feed",
description: parsedFeed.description || "",
items: parsedFeed.items.map((item) => {
return {
title: item.title || "",
link: item.link || "",
content:
item["content:encoded"] ||
item["content:encodedSnippet"] ||
item.content ||
"",
description: item.description || "",
pubDate: item.pubDate || item.isoDate || "",
author: item.creator || item.author || "",
categories: item.categories || [],
};
}),
};
} catch (e) {
console.error("这不是rss xml", e);
return null;
}
}
// 安全地清理和解析HTML内容
function cleanHTMLContent(html) {
if (!html) return "";
// 移除不安全的标签
let cleanedHTML = html
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "") // 移除script标签
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "") // 移除style标签
.replace(/<meta[^>]*>/gi, "") // 移除meta标签
.replace(/<link[^>]*>/gi, "") // 移除link标签
.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, "") // 移除iframe标签
.replace(/on\w+="[^"]*"/g, "") // 移除事件处理器
.replace(/on\w+='[^']*'/g, "") // 移除事件处理器
.replace(/javascript:[^"']*/gi, ""); // 移除javascript协议
return cleanedHTML;
}
// 渲染文章内容
function renderArticleContent(article, container, doc) {
container.textContent = ""; // 清空容器
// 创建文章头部
const header = doc.createElement("div");
header.className = "article-header";
// 创建标题
const title = doc.createElement("h1");
title.className = "article-title";
title.textContent = article.title;
// 添加点击事件打开文章链接
if (article.link) {
title.style.cursor = "pointer";
title.addEventListener("click", () => {
window.open(article.link, "_blank");
});
}
header.appendChild(title);
// 创建元信息
const meta = doc.createElement("div");
meta.className = "article-meta";
if (article.author) {
const authorSpan = doc.createElement("span");
authorSpan.textContent = `${t("author")}: ${article.author}`;
meta.appendChild(authorSpan);
}
if (article.pubDate) {
const dateSpan = doc.createElement("span");
dateSpan.textContent = `${t("publishTime")}: ${new Date(
article.pubDate
).toLocaleString()}`;
meta.appendChild(dateSpan);
}
if (article.categories && article.categories.length) {
const categorySpan = doc.createElement("span");
categorySpan.textContent = `${t("categories")}: ${article.categories.join(
", "
)}`;
meta.appendChild(categorySpan);
}
header.appendChild(meta);
// 创建文章内容
const body = doc.createElement("div");
body.className = "article-body";
const contentToParse = cleanHTMLContent(
article.content || article.description || ""
);
// 使用沙箱环境来解析HTML内容,避免TrustedHTML错误
let contentNodes;
try {
// 尝试使用沙箱文档
const sandbox = document.implementation.createHTMLDocument("sandbox");
const sandboxDiv = sandbox.createElement("div");
sandboxDiv.innerHTML = contentToParse;
contentNodes = sandboxDiv.childNodes;
} catch (error) {
// console.warn("沙箱解析失败,使用纯文本:", error);
// 如果沙箱也失败,创建纯文本节点
const textNode = doc.createTextNode(
contentToParse.replace(/<[^>]*>/g, "")
);
contentNodes = [textNode];
}
// 将节点导入到目标文档
contentNodes.forEach((node) => {
body.appendChild(doc.importNode(node, true));
});
// 添加所有元素到容器
container.appendChild(header);
container.appendChild(body);
}
// 创建阅读器界面
async function createReaderInterface(rssData) {
// 创建新的HTML文档
const newDoc = document.implementation.createHTMLDocument();
const container = newDoc.createElement("div");
container.className = "rss-reader-container";
// 创建顶部header
const header = newDoc.createElement("div");
header.className = "reader-header";
// 创建header信息容器
const headerInfo = newDoc.createElement("div");
headerInfo.className = "header-info";
// RSS标题
const feedTitle = newDoc.createElement("h1");
feedTitle.className = "feed-title";
feedTitle.textContent = rssData.title;
headerInfo.appendChild(feedTitle);
// RSS元信息
const feedMeta = newDoc.createElement("div");
feedMeta.className = "feed-meta";
// 获取最新的文章日期
const latestDate = rssData.items
.map((item) => new Date(item.pubDate))
.reduce(
(latest, current) => (current > latest ? current : latest),
new Date(0)
);
feedMeta.textContent = `${t("lastUpdate")}: ${latestDate.toLocaleString()}`;
headerInfo.appendChild(feedMeta);
// RSS描述
if (rssData.description) {
const feedDesc = newDoc.createElement("div");
feedDesc.className = "feed-description";
feedDesc.textContent = rssData.description;
headerInfo.appendChild(feedDesc);
}
// 将header信息容器添加到header
header.appendChild(headerInfo);
// 创建头部操作按钮容器
const headerActions = newDoc.createElement("div");
headerActions.className = "header-actions";
// 创建主题切换按钮
const themeToggleBtn = newDoc.createElement("button");
themeToggleBtn.className = "theme-toggle";
themeToggleBtn.textContent = "🌙";
themeToggleBtn.title = t("themeToggle");
themeToggleBtn.addEventListener("click", () => {
const currentTheme = document.documentElement.getAttribute("data-theme");
const newTheme = currentTheme === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("rss-reader-theme", newTheme);
themeToggleBtn.textContent = newTheme === "dark" ? "☀️" : "🌙";
});
headerActions.appendChild(themeToggleBtn);
header.appendChild(headerActions);
container.appendChild(header);
// 创建主内容容器
const readerContent = newDoc.createElement("div");
readerContent.className = "reader-content";
// 创建左侧文章列表
const articleList = newDoc.createElement("div");
articleList.className = "article-list";
// 创建右侧内容区
const articleContent = newDoc.createElement("div");
articleContent.className = "article-content";
// 渲染文章列表
rssData.items.forEach((item, index) => {
const articleItem = newDoc.createElement("div");
articleItem.className = "article-item";
const title = newDoc.createElement("h3");
title.textContent = item.title;
articleItem.appendChild(title);
// 添加发布日期
const date = newDoc.createElement("div");
date.style.fontSize = "12px";
date.style.color = "#5f6368";
date.style.marginTop = "4px";
date.textContent = new Date(item.pubDate).toLocaleDateString();
articleItem.appendChild(date);
// 添加文章描述预览
if (item.description) {
const preview = newDoc.createElement("div");
preview.className = "article-preview";
const cleanDesc = cleanHTMLContent(item.description)
.replace(/<[^>]*>/g, "")
.trim();
if (cleanDesc.length > 0) {
preview.textContent =
cleanDesc.length > 100
? cleanDesc.substring(0, 100) + "..."
: cleanDesc;
articleItem.appendChild(preview);
}
}
// 添加文章分类标签
if (item.categories && item.categories.length > 0) {
const categories = newDoc.createElement("div");
categories.className = "article-categories";
item.categories.forEach((category) => {
const tag = newDoc.createElement("span");
tag.className = "category-tag";
tag.textContent = category;
categories.appendChild(tag);
});
articleItem.appendChild(categories);
}
// 添加双击事件监听器
articleItem.addEventListener("dblclick", () => {
if (item.link) {
window.open(item.link, "_blank");
}
});
articleItem.addEventListener("click", () => {
// 移除其他文章的active状态
newDoc.querySelectorAll(".article-item").forEach((item) => {
item.classList.remove("active");
});
// 添加当前文章的active状态
articleItem.classList.add("active");
// 更新右侧内容
renderArticleContent(item, articleContent, newDoc);
// 滚动到顶部
articleContent.scrollTop = 0;
});
articleList.appendChild(articleItem);
// 默认显示第一篇文章
if (index === 0) {
articleItem.classList.add("active");
renderArticleContent(item, articleContent, newDoc);
}
});
readerContent.appendChild(articleList);
readerContent.appendChild(articleContent);
container.appendChild(readerContent);
// 创建返回顶部按钮
const backToTop = newDoc.createElement("button");
backToTop.className = "back-to-top";
backToTop.textContent = "↑";
backToTop.title = t("backToTop");
backToTop.addEventListener("click", () => {
articleContent.scrollTo({ top: 0, behavior: "smooth" });
});
container.appendChild(backToTop);
// 清空原有内容并添加阅读器界面
while (document.documentElement.firstChild) {
document.documentElement.removeChild(document.documentElement.firstChild);
}
// 添加样式到新文档
const styleElement = newDoc.createElement("style");
styleElement.textContent = `
/* 主题变量 */
:root {
/* 明亮主题 */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #202124;
--text-secondary: #5f6368;
--border-color: #e0e0e0;
--hover-bg: #e8f0fe;
--active-border: #1a73e8;
--header-bg: #ffffff;
--button-primary-bg: #2ecc71;
--button-primary-hover: #27ae60;
--button-secondary-bg: #ecf0f1;
--button-secondary-hover: #bdc3c7;
--shadow-color: rgba(0, 0, 0, 0.1);
}
/* 暗黑主题 */
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #e0e0e0;
--text-secondary: #9e9e9e;
--border-color: #404040;
--hover-bg: #3d3d3d;
--active-border: #64b5f6;
--header-bg: #2d2d2d;
--button-primary-bg: #2ecc71;
--button-primary-hover: #27ae60;
--button-secondary-bg: #424242;
--button-secondary-hover: #616161;
--shadow-color: rgba(0, 0, 0, 0.3);
}
body {
margin: 0;
padding: 0;
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
.rss-reader-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: var(--bg-primary);
}
.reader-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid var(--border-color);
background-color: var(--header-bg);
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 4px var(--shadow-color);
}
.header-info {
flex: 1;
min-width: 0;
margin-right: 20px;
}
.feed-title {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 8px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
letter-spacing: -0.5px;
}
.feed-meta {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.feed-description {
font-size: 14px;
color: var(--text-secondary);
margin: 8px 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.5;
}
.reader-content {
display: flex;
flex: 1;
overflow: hidden;
background-color: var(--bg-primary);
}
.article-list {
width: 320px;
border-right: 1px solid var(--border-color);
overflow-y: auto;
background: var(--bg-secondary);
}
.article-item {
padding: 16px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.2s ease;
}
.article-item h3 {
font-size: 15px;
margin: 0;
line-height: 1.5;
color: var(--text-primary);
font-weight: 500;
}
.article-item:hover {
background: var(--hover-bg);
}
.article-item.active {
background: var(--hover-bg);
border-left: 4px solid var(--active-border);
}
.article-content {
flex: 1;
padding: 30px 40px;
overflow-y: auto;
background: var(--bg-primary);
line-height: 1.6;
}
.article-header {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.article-title {
font-size: 28px;
color: var(--text-primary);
margin-bottom: 16px;
line-height: 1.4;
font-weight: 600;
letter-spacing: -0.5px;
}
.article-meta {
color: var(--text-secondary);
font-size: 14px;
margin: 12px 0;
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.article-body {
color: var(--text-primary);
font-size: 16px;
line-height: 1.8;
max-width: 800px;
margin: 0 auto;
}
.article-body img {
max-width: 100%;
height: auto;
display: block;
margin: 20px auto;
border-radius: 8px;
box-shadow: 0 4px 12px var(--shadow-color);
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
position: relative;
}
.subscribe-button-group {
display: flex;
align-items: stretch;
box-shadow: 0 2px 4px var(--shadow-color);
border-radius: 6px;
overflow: hidden;
}
.main-subscribe-button {
background-color: var(--button-primary-bg);
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
white-space: nowrap;
}
.main-subscribe-button:hover {
background-color: var(--button-primary-hover);
}
.dropdown-button {
background-color: var(--button-primary-bg);
color: white;
border: none;
border-left: 1px solid rgba(255, 255, 255, 0.2);
padding: 10px 12px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.dropdown-button:hover {
background-color: var(--button-primary-hover);
}
.theme-toggle {
background-color: var(--button-secondary-bg);
color: var(--text-primary);
border: none;
border-radius: 6px;
padding: 10px;
cursor: pointer;
font-size: 18px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
.theme-toggle:hover {
background-color: var(--button-secondary-hover);
}
.dropdown-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
background-color: var(--bg-primary);
border-radius: 8px;
box-shadow: 0 4px 12px var(--shadow-color);
min-width: 200px;
z-index: 1000;
border: 1px solid var(--border-color);
overflow: hidden;
}
.dropdown-item {
padding: 12px 16px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-primary);
}
.dropdown-item:hover {
background-color: var(--hover-bg);
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--text-secondary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-primary);
}
/* 文章内容排版美化 */
.article-body p {
margin: 1.5em 0;
}
.article-body a {
color: var(--active-border);
text-decoration: none;
}
.article-body a:hover {
text-decoration: underline;
}
.article-body blockquote {
margin: 1.5em 0;
padding: 1em 2em;
border-left: 4px solid var(--active-border);
background: var(--bg-secondary);
border-radius: 4px;
}
.article-body code {
background: var(--bg-secondary);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Fira Code', monospace;
font-size: 0.9em;
}
.article-body pre {
background: var(--bg-secondary);
padding: 1em;
border-radius: 8px;
overflow-x: auto;
}
.article-body h1, .article-body h2, .article-body h3,
.article-body h4, .article-body h5, .article-body h6 {
color: var(--text-primary);
margin: 1.5em 0 1em;
line-height: 1.3;
font-weight: 600;
}
/* 返回顶部按钮 */
.back-to-top {
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--button-primary-bg);
color: white;
border: none;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px var(--shadow-color);
transition: all 0.3s ease;
opacity: 0;
visibility: hidden;
transform: translateY(20px);
z-index: 1000;
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.back-to-top:hover {
background-color: var(--button-primary-hover);
transform: translateY(-2px);
box-shadow: 0 6px 16px var(--shadow-color);
}
/* 文章列表项更多样式 */
.article-item {
position: relative;
padding: 16px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
}
.article-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 3px;
height: 0;
background-color: var(--active-border);
transition: height 0.3s ease;
}
.article-item:hover::before {
height: 100%;
}
.article-item.active {
background: var(--hover-bg);
border-left: 4px solid var(--active-border);
}
/* 添加文章描述预览 */
.article-preview {
font-size: 13px;
color: var(--text-secondary);
margin-top: 8px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 添加文章分类标签 */
.article-categories {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.category-tag {
background-color: var(--bg-secondary);
color: var(--text-secondary);
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
border: 1px solid var(--border-color);
}
/* 添加文章阅读进度指示器 */
.article-list::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, var(--active-border) 0%, transparent 100%);
transform-origin: left;
transform: scaleX(0);
transition: transform 0.3s ease;
}
/* 添加加载动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.article-item {
animation: fadeIn 0.5s ease forwards;
}
.article-item:nth-child(1) { animation-delay: 0.1s; }
.article-item:nth-child(2) { animation-delay: 0.2s; }
.article-item:nth-child(3) { animation-delay: 0.3s; }
.article-item:nth-child(4) { animation-delay: 0.4s; }
.article-item:nth-child(5) { animation-delay: 0.5s; }
/* 响应式设计优化 */
@media (max-width: 768px) {
.article-list {
width: 280px;
}
.article-content {
padding: 20px;
}
.back-to-top {
bottom: 20px;
right: 20px;
width: 40px;
height: 40px;
font-size: 16px;
}
.reader-header {
padding: 15px;
}
.feed-title {
font-size: 20px;
}
}
@media (max-width: 480px) {
.reader-content {
flex-direction: column;
}
.article-list {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--border-color);
max-height: 300px;
}
.article-content {
padding: 15px;
}
}
`;
newDoc.head.appendChild(styleElement);
const newBody = newDoc.createElement("body");
newBody.appendChild(container);
newDoc.documentElement.appendChild(newBody);
document.documentElement.appendChild(newDoc.documentElement);
// 初始化主题
const savedTheme = localStorage.getItem("rss-reader-theme") || "light";
document.documentElement.setAttribute("data-theme", savedTheme);
const themeToggleElement = document.querySelector(".theme-toggle");
if (themeToggleElement) {
themeToggleElement.textContent = savedTheme === "dark" ? "☀️" : "🌙";
}
// 返回顶部按钮显示/隐藏逻辑
const backToTopButton = document.querySelector(".back-to-top");
const articleContentElement = document.querySelector(".article-content");
if (backToTopButton && articleContentElement) {
articleContentElement.addEventListener("scroll", () => {
if (articleContentElement.scrollTop > 300) {
backToTopButton.classList.add("visible");
} else {
backToTopButton.classList.remove("visible");
}
});
}
}
// 主函数
async function init() {
// 检查是否是RSS源
if (!isRSSFeed()) {
return;
}
// 提取和解析RSS
const feed = await extractRSS();
if (!feed) {
console.log("这不是RSS XML页面");
return;
}
// 创建阅读器界面
createReaderInterface(feed);
}
// 启动程序
init();
})();