知乎问题标记已读

在知乎问题标题旁添加标记为已读的按钮,并允许导出和导入已读数据

// ==UserScript==
// @name         知乎问题标记已读
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  在知乎问题标题旁添加标记为已读的按钮,并允许导出和导入已读数据
// @author       shaoz
// @match        *://*.zhihu.com/*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    function addButton(element, questionId) {
        if (element.querySelector('.mark-as-read')) {
            return; // 避免重复添加按钮
        }

        const isRead = localStorage.getItem('read_' + questionId) === 'true';

        const readButton = document.createElement('button');
        readButton.className = 'mark-as-read';
        readButton.innerText = isRead ? '已读' : '标记为已读';
        readButton.style.marginLeft = '10px';
        readButton.style.color = '#000000';
        readButton.style.backgroundColor = isRead ? '#4CAF50' : '#e0e0e0';
        readButton.style.border = '1px solid #dcdcdc';
        readButton.style.padding = '1px 4px';
        readButton.style.fontSize = '12px';
        readButton.style.fontWeight = 'normal';
        readButton.style.cursor = 'pointer';

        readButton.onclick = function() {
            const currentState = localStorage.getItem('read_' + questionId) === 'true';
            localStorage.setItem('read_' + questionId, !currentState);
            this.style.backgroundColor = currentState ? '#e0e0e0' : '#4CAF50';
            this.innerText = currentState ? '标记为已读' : '已读';
        };

        // 使按钮在标题的同一行显示
        element.style.display = 'inline-flex';
        element.style.alignItems = 'center';
        element.appendChild(readButton);
    }

    function processPage() {
        // 尝试从详情页获取问题ID
        const dataElement = document.getElementById('js-initialData');
        if (dataElement) {
            try {
                const initialData = JSON.parse(dataElement.textContent);
                const questionId = initialData.initialState.entities.questions[Object.keys(initialData.initialState.entities.questions)[0]].id;
                const titleElement = document.querySelector('.QuestionHeader-title');
                if (titleElement) {
                    addButton(titleElement, questionId);
                }
            } catch (e) {
                console.error('Error parsing initial data:', e);
            }
        }

        // 主页或搜索结果页
        document.querySelectorAll('.ContentItem-title, .SearchResult-Card .ContentItem-title').forEach(title => {
            const linkElement = title.querySelector('a[href*="/question/"]');
            if (linkElement) {
                const href = linkElement.getAttribute('href');
                const match = href.match(/question\/(\d+)/);
                if (match) {
                    addButton(title, match[1]);
                }
            }
        });
    }

    function exportReadData() {
        const data = {};
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (key.startsWith('read_')) {
                data[key] = localStorage.getItem(key);
            }
        }
        const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = 'read_data.json';
        a.click();
        URL.revokeObjectURL(url);
    }

    function importReadData() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = 'application/json';
        input.onchange = function(event) {
            const file = event.target.files[0];
            const reader = new FileReader();
            reader.onload = function() {
                try {
                    const importedData = JSON.parse(reader.result);
                    for (const [key, value] of Object.entries(importedData)) {
                        if (key.startsWith('read_') && localStorage.getItem(key) === null) {
                            localStorage.setItem(key, value);
                        }
                    }
                    alert('数据导入成功!');
                } catch (e) {
                    alert('导入失败,文件格式不正确!');
                }
            };
            reader.readAsText(file);
        };
        input.click();
    }

    function addExportImportButtons() {
        const container = document.createElement('div');
        container.style.position = 'fixed';
        container.style.bottom = '10px';
        container.style.right = '10px';
        container.style.zIndex = '1000';
        container.style.display = 'flex';
        container.style.gap = '10px';

        const exportButton = document.createElement('button');
        exportButton.innerText = '导出已读数据';
        exportButton.style.padding = '5px 10px';
        exportButton.style.cursor = 'pointer';
        exportButton.onclick = exportReadData;

        const importButton = document.createElement('button');
        importButton.innerText = '导入已读数据';
        importButton.style.padding = '5px 10px';
        importButton.style.cursor = 'pointer';
        importButton.onclick = importReadData;

        container.appendChild(exportButton);
        container.appendChild(importButton);
        document.body.appendChild(container);
    }

    const observer = new MutationObserver(mutations => {
        processPage();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    processPage();
    addExportImportButtons();
})();