// ==UserScript==
// @name 知乎历史记录
// @namespace http://zhangmaimai.com
// @version 0.2
// @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
// @downloadURL none
// ==/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 }) =>
`
${title} - ${authorName}
`)
.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', ``)
// 插件 dialog,用这个做弹出层还挺方便的
document.body.insertAdjacentHTML('beforeend', `
`)
// 添加样式
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)
})()