Greasy Fork

Greasy Fork is available in English.

知乎历史记录

给知乎添加爱历史记录

当前为 2023-02-11 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         知乎历史记录
// @namespace    http://zhangmaimai.com
// @version      0.1
// @description  给知乎添加爱历史记录
// @author       MaxChang3
// @match        https://www.zhihu.com/
// @icon         https://static.zhihu.com/heifetz/favicon.ico
// @grant        GM_addStyle
// @run-at       document-end
// @license      WTFPL
// ==/UserScript==

(function () {
    'use strict'
    const HISTORYS_LIMIT = 20
    const HISTORYS_CACHE = { VALUE: '', CNT: 0, LAST_CNT: -1 }
    /**
     * 给元素绑定添加历史记录的事件
     * @param {Event} e Event
     * @returns
     */
    const bindHistoryEvent = (e) => {
        /** @type {HTMLElement | undefined} */
        const ansterItem = e.target.closest('.ContentItem')
        if (!ansterItem) return
        const zop = ansterItem.dataset.zop
        if (!zop) console.error('无法读取回答或文章信息')
        /**
         * @typedef {{
         *   authorName:string,
         *   itemId:number,
         *   title:string,
         *   type: 'answter' | 'article',
         *   url?: string
         * }} ZHContentData
         */
        /** @type {ZHContentData} */
        const contentData = JSON.parse(zop)
        /** @type {string} */
        const url = ansterItem.querySelector('.ContentItem-title a').href
        contentData.url = url
        const historysData = window.localStorage.getItem('ZH_HISTORY')
        /** @type {ZHContentData[]} */
        const histroys = historysData ?
            JSON.parse(historysData)
                .filter(histroy => histroy.itemId != contentData.itemId)
                .concat(contentData) :
            [contentData]
        if (histroys.length > HISTORYS_LIMIT) histroys.shift()
        window.localStorage.setItem('ZH_HISTORY', JSON.stringify(histroys))
        HISTORYS_CACHE.CNT++
    }
    // 从 localStorage 中取回历史记录
    /** @returns {ZHContentData[]} */
    const getHistoryList = () => JSON.parse(window.localStorage.getItem('ZH_HISTORY'))
    const getHistoryListElements = () => {
        // 做了个简单的缓存机制,如果距离上次点开前进行了若干次点击动作,则重新取回数据
        // 否则就从缓存中拿回来
        if (HISTORYS_CACHE.LAST_CNT !== HISTORYS_CACHE.CNT) {
            const ret = getHistoryList().map(({ title, url, authorName, type }) =>
                `<li>
        <a class="${{
                    'answer': 'zhh-type-answer',
                    'article': 'zhh-type-article'
                }[type]}" href="${url}">${title}</a> - ${authorName}
    </li>`)
                .reverse()
                .join('\n')
            HISTORYS_CACHE.VALUE = ret
            HISTORYS_CACHE.LAST_CNT = HISTORYS_CACHE.CNT
            return ret
        }
        return HISTORYS_CACHE.VALUE
    }
    // 用 Web Component 包装一下,这样渲染是直接出来整个按钮,不过似乎有些麻烦了/
    class ZHHistoryCard extends HTMLElement {
        constructor() {
            super()
            const shadow = this.attachShadow({ mode: 'open' })
            const style = document.createElement('style')
            style.textContent = `
            .Card {
                background: #fff;
                border-radius: 2px;
                -webkit-box-shadow: 0 1px 3px hsl(0deg 0% 7% / 10%);
                box-shadow: 0 1px 3px hsl(0deg 0% 7% / 10%);
                -webkit-box-sizing: border-box;
                box-sizing: border-box;
                margin-bottom: 10px;
                overflow: hidden;
                padding: 5px 0;
            }
            .zhh-button {
                box-sizing: border-box;
                margin: 0px 18px;
                min-width: 0px;
                -webkit-box-pack: center;
                justify-content: center;
                -webkit-box-align: center;
                align-items: center;
                display: flex;
                border: 1px solid rgba(5, 109, 232, 0.5);
                color: rgb(5, 109, 232);
                border-radius: 4px;
                cursor: pointer;
                height: 40px;
                font-size: 14px;
            }
            `
            const wrapper = document.createElement('div')
            wrapper.setAttribute('class', 'Card')
            const button = document.createElement('div')
            button.setAttribute('class', 'zhh-button')
            button.innerText = '查看历史记录'
            wrapper.append(button)
            shadow.appendChild(wrapper)
            shadow.appendChild(style)
        }
    }
    customElements.define('zh-history-card', ZHHistoryCard,)
    // 给现存的卡片添加点击事件
    document.querySelectorAll('.ContentItem')
        .forEach(el => el.addEventListener('click', bindHistoryEvent))
    // 插入历史记录卡片
    document.querySelector('.Topstory-container').children[1].children[1]
        .insertAdjacentHTML('afterbegin', `<zh-history-card id="zhh-card"></zh-history-card>`)
    // 插件 dialog,用这个做弹出层还挺方便的
    document.body.insertAdjacentHTML('beforeend', `
<dialog>
    <div class="Modal Modal--default ChatBoxModal" tabindex="0">
        <div class="Modal-inner">
            <div class="Modal-content Modal-content--spread ChatBoxModal-content">
                <ul style="padding: 20px;" id="zhh-list">
                </ul>
            </div>
        </div>
    </div>
</dialog>
`)
    // 添加样式
    GM_addStyle(`
    dialog {
        padding: 0; 
        border: 0;
    }
    dialog::backdrop {
        background-color: hsla(0,0%,7%,.65);
    }
    #zhh-list li {
        padding-bottom: 5px;
    }
    .zhh-type-answer::before {
        content: '问题';
		color: #2196F3;
		background-color: #2196F333;
		font-weight: bold;
		font-size: 13px;
		padding: 1px 4px 0;
		border-radius: 2px;
		display: inline-block;
		vertical-align: 1.5px;
		margin: 0 4px 0 0;
    }
    .zhh-type-article::before  {
        content: '文章';
        color: #004b87;
        background-color: #2196F333;
        font-weight: bold;
        font-size: 13px;
        padding: 1px 4px 0;
        border-radius: 2px;
        display: inline-block;
        vertical-align: 1.5px;
        margin: 0 4px 0 0;
    }
    `)
    // 点击弹出层周围直接关闭
    const dialog = document.querySelector('dialog')
    dialog.addEventListener('click', (e => {
        if (!e.target.closest('div')) {
            e.target.close()
        }
    }))
    // 给历史记录卡片绑定一个点击事件 实时插入历史记录列表
    document.querySelector('#zhh-card')
        .shadowRoot
        .querySelector('.zhh-button').addEventListener('click', () => {
            dialog.querySelector('#zhh-list').innerHTML = getHistoryListElements()
            dialog.showModal()
        })
    // 监听元素更新,给新添加到内容绑定事件
    const targetNode = document.querySelector('.Topstory-recommend')
    const config = { childList: true, subtree: true }
    /** @type {MutationCallback} */
    const callback = (mutationsList, observer) => {
        for (let mutation of mutationsList) {
            if (mutation.type !== 'childList') continue
            mutation.addedNodes.forEach(node => {
                /** @type {HTMLElement | undefined} */
                const contentItem = node.querySelector('.ContentItem')
                if (contentItem) contentItem.addEventListener('click', bindHistoryEvent)
            })
        }
    }
    const observer = new MutationObserver(callback)
    observer.observe(targetNode, config)
})()