Greasy Fork is available in English.

知乎首页信息流过滤

可以按照标签过滤知乎首页信息流,隐藏含不喜欢标签的回答、文章及视频。并且可以在信息流标题下显示发布时间与编辑时间。

// ==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();
    }
  });
});