Greasy Fork

Greasy Fork is available in English.

知乎历史记录

给知乎添加历史记录

当前为 2024-04-14 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/** 历史记录限制 */
const HISTORYS_LIMIT = 20
/** 自定义组件 - 弹出层 */
class ZHHDialog extends HTMLElement {
    /** @type {HTMLDialogElement} */
    dialog
    /** @type {HTMLUListElement} */
    historyList
    constructor() {
        super()
        const shadow = this.attachShadow({ mode: 'open' })
        const style = document.createElement('style')
        style.textContent = `
            dialog {
                padding: 0; 
                border: 0;
            }
            dialog::backdrop {
                background-color: hsla(0,0%,7%,.65);
            }
            .inner {
                padding: 0 25px;
            }
            .inner:focus{
                outline: none;
            }
            a {
                text-decoration: none;
                color: black;
            }
            ul {
                list-style: none;
                padding-left: 0;
            }
            li {
                padding-bottom: 10px;
            }
            .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.createElement('dialog')
        dialog.innerHTML = `<div class="inner" tabindex="0"></div>`
        // 点击弹出层周围直接关闭
        dialog.addEventListener('click', (e => {
            if (!e.target.closest('div')) e.target.close()
        }))
        const inner = document.createElement('div')
        inner.setAttribute('class', 'inner')
        inner.setAttribute('tabindex', '0')
        const historyList = document.createElement('ul')
        historyList.setAttribute('id', 'zzh-list')
        historyList.innerHTML = `<slot/>`
        this.dialog = dialog
        this.historyList = historyList
        dialog.appendChild(inner)
        inner.appendChild(historyList)
        shadow.appendChild(dialog)
        shadow.appendChild(style)
    }
    showModal() {
        this.dialog.showModal()
    }
}
customElements.define('zh-dialog', ZHHDialog)

/** 自定义组件 - 历史记录卡片 */
class ZHHistoryCard extends HTMLElement {
    /** @type {HTMLElement} */
    button
    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)
        this.button = button
        shadow.appendChild(wrapper)
        shadow.appendChild(style)
    }
}
customElements.define('zh-history-card', ZHHistoryCard)

/**
 * 定义一下后面用到的属性类型
 * @typedef {{
 *   authorName: string,
 *   itemId: number,
 *   title: string,
 *   type: 'answter' | 'article',
 *   url?: string
 * }} ZHContentData
*/
/** 给元素绑定添加历史记录的事件 
 * @param {Event} e Event
 */
const bindHistoryEvent = (e) => {
    /** @type {HTMLElement | undefined} */
    const ansterItem = e.target.closest('.ContentItem')
    if (!ansterItem) return
    const zop = ansterItem.dataset.zop
    if (!zop) console.error('无法读取回答或文章信息')
    /** @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 HISTORYS_CACHE = { VALUE: '', CNT: 0, LAST_CNT: -1 }
/** 获取历史记录元素 */
const getHistoryListElements = () => {
    // 做了个简单的缓存机制,如果距离上次点开前进行了若干次点击动作,则重新取回数据
    // 否则就从直接缓存中拿回来
    if (HISTORYS_CACHE.LAST_CNT === HISTORYS_CACHE.CNT) return HISTORYS_CACHE.VALUE
    const type2Class = { answer: 'zhh-type-answer', article: 'zhh-type-article' }
    /** @type {string} */
    const ret = getHistoryList().map(({ title, url, authorName, type }) =>
        `<li>
            <a class="${type2Class[type]}" href="${url}">${title}</a> - ${authorName}
        </li>`)
        .reverse()
        .join('\n')
    HISTORYS_CACHE.VALUE = ret
    HISTORYS_CACHE.LAST_CNT = HISTORYS_CACHE.CNT
    return ret
}

// 给现存的卡片添加点击事件
document.querySelectorAll('.ContentItem').forEach(el => el.addEventListener('click', bindHistoryEvent))

// 插入历史记录卡片
document.querySelector('.Topstory-container').children[1].children[1]
    .insertAdjacentHTML('afterbegin', `<zh-history-card></zh-history-card>`)

// 插入 dialog
document.body.insertAdjacentHTML('beforeEnd', `<zh-dialog></zh-dialog>`)

/** @type {ZHHDialog} */
const dialog = document.querySelector('zh-dialog')
/** @type {ZHHistoryCard} */
const historyCard = document.querySelector('zh-history-card')

// 给历史记录卡片绑定一个点击事件 实时插入历史记录列表
historyCard.button.addEventListener('click', () => {
    dialog.historyList.innerHTML = getHistoryListElements()
    dialog.showModal()
})

// 监听元素更新,给新添加的内容绑定事件
const targetNode = document.querySelector('.Topstory-recommend')
const config = { childList: true, subtree: true }
/** @type {MutationCallback} */
const callback = (mutationsList) => {
    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)