// ==UserScript==
// @name 知乎首页信息流过滤
// @namespace https://zhaoji.wang/
// @version 0.2.2
// @description 可以按照标签过滤知乎首页信息流,隐藏含不喜欢标签的回答、文章及视频。并且可以在信息流标题下显示发布时间与编辑时间。
// @author Zhaoji Wang
// @license Apache-2.0
// @match https://www.zhihu.com/
// @match https://www.zhihu.com/follow
// @icon https://www.google.com/s2/favicons?domain=zhihu.com
// @require https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js
// @require https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.min.js
// @require https://cdn.bootcdn.net/ajax/libs/localforage/1.9.0/localforage.min.js
// @require https://cdn.bootcdn.net/ajax/libs/dayjs/1.10.6/dayjs.min.js
// @require https://cdn.bootcdn.net/ajax/libs/dayjs/1.10.6/locale/zh-cn.min.js
// @require https://cdn.bootcdn.net/ajax/libs/dayjs/1.10.6/plugin/duration.min.js
// @require https://cdn.bootcdn.net/ajax/libs/dayjs/1.10.6/plugin/relativeTime.min.js
// @grant GM_xmlhttpRequest
// @connect www.zhihu.com
// @connect zhuanlan.zhihu.com
// ==/UserScript==
"use strict";
$(() => {
// 加载 Day.js
dayjs.locale("zh-cn");
dayjs.extend(dayjs_plugin_duration);
dayjs.extend(dayjs_plugin_relativeTime); // 将 GM_xmlhttpRequest 函数 Promise 化
const get = (url) =>
new Promise((resolve, reject) =>
GM_xmlhttpRequest({
method: "GET",
url,
onload(response) {
resolve(response.responseText);
},
onerror(error) {
reject(error);
}
})
);
$(".Topstory-mainColumn").prepend('<div id="filter-rules"></div>');
const app = new Vue({
el: "#filter-rules",
template: `
<div
id="filter-rules"
style="background: #fff; box-shadow: 0 1px 3px rgb(18 18 18 / 10%); padding: 20px; border-bottom: 1px solid #f0f2f7; margin-bottom: 10px; border-radius: 2px;"
v-if="isLoadConfigDone"
>
<div class="header" style="position: relative;">
<div style="font-size: 18px; font-weight: 600; line-height: 1.6;">
过滤规则
</div>
<div
class="action-bar"
style="position: absolute; right: 0; top: 2px;"
>
<button
@click="addRule()"
style="line-height: 2; padding: 0 12px; color: #06f; text-align: center; border: 1px solid; border-radius: 3px; cursor: pointer; font-size: 12px;"
>
添加标签
</button>
<button
@click="toggleBarDisplayStatus()"
style="line-height: 2; padding: 0 12px; color: #06f; text-align: center; border: 1px solid; border-radius: 3px; cursor: pointer; font-size: 12px;"
>
{{ barIsShown ? '折叠' : '展开' }}
</button>
</div>
</div>
<div
v-show="barIsShown"
style="line-height: 1.67; margin-top: 9px;"
>
<div
class="rule-tag"
title="点击可删除该标签"
style="position: relative; display: inline-block; height: 30px; padding: 0 12px; font-size: 14px; line-height: 30px; color: #06f; border-radius: 100px; background: rgba(0,102,255,.1); margin: 3px 5px 3px 0; vertical-align: middle; cursor: pointer;"
v-if="rules.length"
v-for="(v, i) in rules"
:key="v"
@click="removeRule(i)"
>
{{ v }}
</div>
<p v-if="!rules.length">
当前尚未设置规则
</p>
</div>
</div>
`,
data() {
return {
isLoadConfigDone: false,
rules: [],
titles: [],
tags: {},
times: {},
barIsShown: true
};
},
methods: {
async loadConfig() {
let config = await localforage.getItem("zhihu-filter-config");
if (!config) {
config = await localforage.setItem("zhihu-filter-config", {
barIsShown: true,
rules: []
});
}
this.barIsShown = config.barIsShown;
this.rules = config.rules;
this.isLoadConfigDone = true;
},
async saveConfig() {
await localforage.setItem("zhihu-filter-config", {
barIsShown: this.barIsShown,
rules: this.rules
});
},
async toggleBarDisplayStatus() {
this.barIsShown = !this.barIsShown;
await this.saveConfig();
},
async addRule() {
const newTag = prompt("请输入需要被过滤的标签");
if (newTag) {
this.rules = Array.from(new Set([...this.rules, newTag]));
await this.saveConfig();
}
},
async removeRule(index) {
this.rules.splice(index, 1);
await this.saveConfig();
},
updateTitles() {
this.titles = Array.from($(".ContentItem-title a")).map((v) => ({
title: $(v).text(),
href: $(v).attr("href")
}));
setTimeout(this.updateTitles, 100);
},
updateTagsAndTimes() {
this.titles.forEach(async (v) => {
if (!this.tags[v.title]) {
if (v.href.includes("question") && !v.href.includes("answer")) {
// 知乎问题
const html = await get(v.href);
const tags = Array.from($(".QuestionTopic", html)).map((e) =>
$(e).text()
);
const { created: createdTime, updatedTime } = Object.values(
JSON.parse(
Array.from($(html)).filter(
(v) => v.id === "js-initialData"
)[0].innerHTML
).initialState.entities.questions
)[0];
this.tags[v.title] = tags;
this.times[v.title] = {
createdTime,
updatedTime
};
} else if (v.href.includes("question") && v.href.includes("answer")) {
// 知乎问题的回答
const html = await get(v.href);
const tags = Array.from($(".QuestionTopic", html)).map((e) =>
$(e).text()
);
const { createdTime, updatedTime } = Object.values(
JSON.parse(
Array.from($(html)).filter(
(v) => v.id === "js-initialData"
)[0].innerHTML
).initialState.entities.answers
)[0];
this.tags[v.title] = tags;
this.times[v.title] = {
createdTime,
updatedTime
};
} else if (v.href.includes("zhuanlan")) {
// 知乎专栏的文章
const html = await get(v.href);
const tags = Array.from($(".Tag.Topic", html)).map((e) =>
$(e).text()
);
const { created: createdTime, updated: updatedTime } =
Object.values(
JSON.parse(
Array.from($(html)).filter(
(v) => v.id === "js-initialData"
)[0].innerHTML
).initialState.entities.articles
)[0];
this.tags[v.title] = tags;
this.times[v.title] = {
createdTime,
updatedTime
};
} else if (v.href.includes("zvideo")) {
// 知乎视频
const html = await get(v.href);
const tags = Array.from($(".ZVideoTag", html)).map((e) =>
$(e).text()
);
const { publishedAt: createdTime, updatedAt: updatedTime } =
Object.values(
JSON.parse(
Array.from($(html)).filter(
(v) => v.id === "js-initialData"
)[0].innerHTML
).initialState.entities.zvideos
)[0];
this.tags[v.title] = tags;
this.times[v.title] = {
createdTime,
updatedTime
};
} else {
this.tags[v.title] = true;
}
}
});
setTimeout(this.updateTagsAndTimes, 1000);
},
updateQuestionsDisplayStatus() {
Array.from($(".TopstoryItem")).forEach((v, i) => {
const title = $(v).find(".ContentItem-title a").text();
if (
!$(v).is(":hidden") &&
this.tags[title] &&
this.tags[title] !== true &&
this.tags[title].some((tag) => this.rules.includes(tag))
) {
$(v).hide();
console.log("已过滤问题:", title);
}
});
setTimeout(this.updateQuestionsDisplayStatus, 100);
},
updateQuestionsTimeMark() {
Array.from($(".TopstoryItem")).forEach((v, i) => {
const $title = $(v).find(".ContentItem-title a");
const title = $title.text();
if (
!$(v).is(":hidden") &&
!$(v).find(".time-mark").length &&
this.times[title] &&
this.times[title] !== true
) {
const createdTime = this.times[title].createdTime;
const updatedTime = this.times[title].updatedTime;
const createdTimeStr = `${dayjs
.duration(dayjs().diff(this.times[title].createdTime * 1000))
.humanize()}前`;
const updatedTimeStr = `${dayjs
.duration(dayjs().diff(this.times[title].updatedTime * 1000))
.humanize()}前`;
if (createdTime === updatedTime) {
$title.parent().after(
`<div class="time-mark" style="font-size: 14px; color: #8590a6; line-height: 1.67; margin-top: 5px; font-weight: 400;">发布于 ${createdTimeStr}</div>`
);
} else {
if (createdTimeStr === updatedTimeStr) {
$title.parent().after(
`<div class="time-mark" style="font-size: 14px; color: #8590a6; line-height: 1.67; margin-top: 5px; font-weight: 400;">编辑于 ${updatedTimeStr}</div>`
);
} else {
$title.parent().after(
`<div class="time-mark" style="font-size: 14px; color: #8590a6; line-height: 1.67; margin-top: 5px; font-weight: 400;">发布于 ${createdTimeStr} → 编辑于 ${updatedTimeStr}</div>`
);
}
}
}
});
setTimeout(this.updateQuestionsTimeMark, 100);
}
},
async created() {
await this.loadConfig();
this.updateTitles();
this.updateTagsAndTimes();
this.updateQuestionsDisplayStatus();
this.updateQuestionsTimeMark();
}
});
});