PageMemory

带历史记录管理的位置记忆器(支持拖拽)

// ==UserScript==
// @name         PageMemory
// @namespace    scroll-historian.js
// @version      2.2
// @description  带历史记录管理的位置记忆器(支持拖拽)
// @author       QWAS-zx
// @match        *://*/*
// @match        about:srcdoc
// @match        file:///*
// @license      MIT
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';
    
    const STORAGE_KEY = 'Global_Position_History';
    const HELPER_POSITION_KEY = 'Global_Helper_Position';
    let menuVisible = false;
    let historyVisible = false;
    let isDragging = false;
    let dragStartX, dragStartY;
    let initialX, initialY;

    // 添加全局样式
    GM_addStyle(`
        /* 主按钮和菜单样式 */
        #mdn-position-helper {
            position: fixed;
            bottom: 25px;
            right: 25px;
            z-index: 10000;
            cursor: grab;
        }
        
        #mdn-position-helper.dragging {
            cursor: grabbing;
            opacity: 0.9;
            box-shadow: 0 0 15px rgba(38, 139, 210, 0.8);
        }
        
        #mdn-main-btn {
            width: 55px;
            height: 55px;
            border-radius: 50%;
            background: #002b36;
            color: #fdf6e3;
            border: 2px solid #268bd2;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: bold;
            cursor: pointer;
            box-shadow: 0 4px 12px rgba(0,0,0,0.25);
            transition: all 0.3s;
            user-select: none;
        }
        
        #mdn-main-btn:hover {
            transform: scale(1.1);
            background: #073642;
        }
        
        #mdn-action-menu {
            position: absolute;
            bottom: 70px;
            right: 0;
            width: 180px;
            background: #002b36;
            border: 1px solid #268bd2;
            border-radius: 8px;
            padding: 10px 0;
            box-shadow: 0 5px 15px rgba(0,0,0,0.3);
            display: none;
            z-index: 10001;
        }
        
        .mdn-menu-item {
            padding: 10px 15px;
            color: #fdf6e3;
            cursor: pointer;
            transition: background 0.2s;
            display: flex;
            align-items: center;
        }
        
        .mdn-menu-item:hover {
            background: #073642;
        }
        
        /* 历史记录面板样式 */
        #mdn-history-panel {
            position: fixed;
            bottom: 100px;
            right: 30px;
            width: 320px;
            max-height: 60vh;
            background: #002b36;
            border: 1px solid #268bd2;
            border-radius: 8px;
            box-shadow: 0 8px 30px rgba(0,0,0,0.5);
            z-index: 10002;
            display: none;
            overflow: hidden;
            font-family: Arial, sans-serif;
        }
        
        #mdn-history-header {
            padding: 15px;
            background: #073642;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid #268bd2;
        }
        
        #mdn-history-title {
            font-size: 1.2em;
            font-weight: bold;
            color: #268bd2;
        }
        
        #mdn-clear-history {
            background: #dc322f;
            color: white;
            border: none;
            border-radius: 4px;
            padding: 5px 10px;
            cursor: pointer;
            transition: background 0.3s;
        }
        
        #mdn-clear-history:hover {
            background: #ff4136;
        }
        
        #mdn-history-list {
            padding: 10px;
            overflow-y: auto;
            max-height: calc(60vh - 100px);
        }
        
        .mdn-history-item {
            padding: 12px;
            margin-bottom: 10px;
            background: rgba(255,255,255,0.05);
            border-radius: 6px;
            border-left: 3px solid #268bd2;
            cursor: pointer;
            transition: all 0.3s;
        }
        
        .mdn-history-item:hover {
            background: rgba(38, 139, 210, 0.15);
            transform: translateX(-3px);
        }
        
        .mdn-history-title {
            font-weight: bold;
            margin-bottom: 5px;
            color: #fdf6e3;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
            overflow: hidden;
        }
        
        .mdn-history-meta {
            display: flex;
            justify-content: space-between;
            font-size: 0.85em;
            color: #93a1a1;
        }
        
        .mdn-history-actions {
            display: flex;
            justify-content: flex-end;
            gap: 8px;
            margin-top: 8px;
        }
        
        .mdn-restore-btn, .mdn-delete-btn {
            padding: 4px 10px;
            border-radius: 4px;
            font-size: 0.85em;
            cursor: pointer;
        }
        
        .mdn-restore-btn {
            background: rgba(38, 139, 210, 0.3);
            color: #268bd2;
        }
        
        .mdn-delete-btn {
            background: rgba(220, 50, 47, 0.3);
            color: #dc322f;
        }
        
        /* 标记线 */
        #mdn-position-marker {
            position: absolute;
            left: 0;
            right: 0;
            height: 3px;
            background: linear-gradient(90deg, transparent, #ff4136, transparent);
            z-index: 9999;
            pointer-events: none;
            display: none;
        }
        
        /* 通知样式 */
        #mdn-position-notify {
            position: fixed;
            bottom: 100px;
            right: 30px;
            background: rgba(0, 43, 54, 0.9);
            color: #fdf6e3;
            border: 1px solid #268bd2;
            padding: 12px 18px;
            border-radius: 8px;
            z-index: 10001;
            max-width: 300px;
            backdrop-filter: blur(4px);
            animation: fadeIn 0.3s;
        }
        
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }
    `);

    // 创建主容器
    const helperContainer = document.createElement('div');
    helperContainer.id = 'mdn-position-helper';
    document.body.appendChild(helperContainer);
    
    // 创建主按钮
    const mainBtn = document.createElement('div');
    mainBtn.id = 'mdn-main-btn';
    mainBtn.textContent = '📌';
    helperContainer.appendChild(mainBtn);
    
    // 创建菜单
    const actionMenu = document.createElement('div');
    actionMenu.id = 'mdn-action-menu';
    helperContainer.appendChild(actionMenu);
    
    // 创建保存按钮
    const saveBtn = document.createElement('div');
    saveBtn.className = 'mdn-menu-item';
    saveBtn.innerHTML = '<span style="margin-right:8px">💾</span> 保存当前位置';
    actionMenu.appendChild(saveBtn);
    
    // 创建历史记录按钮
    const historyBtn = document.createElement('div');
    historyBtn.className = 'mdn-menu-item';
    historyBtn.innerHTML = '<span style="margin-right:8px">📋</span> 历史记录';
    actionMenu.appendChild(historyBtn);
    
    // 创建历史记录面板
    const historyPanel = document.createElement('div');
    historyPanel.id = 'mdn-history-panel';
    historyPanel.innerHTML = `
        <div id="mdn-history-header">
            <div id="mdn-history-title">保存的位置历史</div>
            <button id="mdn-clear-history">清空记录</button>
        </div>
        <div id="mdn-history-list"></div>
    `;
    document.body.appendChild(historyPanel);
    
    // 创建位置标记线
    const positionMarker = document.createElement('div');
    positionMarker.id = 'mdn-position-marker';
    document.body.appendChild(positionMarker);

    // 获取历史记录
    function getHistory() {
        return GM_getValue(STORAGE_KEY, []);
    }

    // 保存历史记录
    function saveHistory(history) {
        GM_setValue(STORAGE_KEY, history);
    }

    // 添加新记录
    function addNewRecord() {
        const history = getHistory();
        
        const newRecord = {
            id: Date.now(),
            url: window.location.href,
            path: window.location.pathname,
            scrollY: window.scrollY,
            timestamp: Date.now(),
            pageTitle: document.title,
            scrollPercent: getScrollPercentage()
        };
        
        // 添加到历史记录开头
        history.unshift(newRecord);
        
        // 只保留最近的20条记录
        if (history.length > 20) history.pop();
        
        saveHistory(history);
        showNotification(`📍 位置已保存!<br>${newRecord.scrollPercent}%`);
        updateHistoryUI();
    }

    // 删除记录
    function deleteRecord(id) {
        const history = getHistory();
        const newHistory = history.filter(record => record.id !== id);
        saveHistory(newHistory);
        updateHistoryUI();
        showNotification('🗑️ 记录已删除');
    }

    // 清空历史
    function clearHistory() {
        saveHistory([]);
        updateHistoryUI();
        showNotification('🧹 历史记录已清空');
        hideHistoryPanel();
    }

    // 恢复记录
    function restoreRecord(record) {
        // 显示位置标记线
        positionMarker.style.display = 'block';
        positionMarker.style.top = `${record.scrollY}px`;
        setTimeout(() => positionMarker.style.display = 'none', 3000);

        if (window.location.href === record.url) {
            window.scrollTo({ top: record.scrollY, behavior: 'smooth' });
            showNotification(`↩️ 已恢复位置!<br>${record.scrollPercent}%`);
        } else {
            showNotification(`⏳ 正在跳转到保存的页面...`);
            setTimeout(() => {
                window.location.href = record.url;
                // 存储记录以便新页面加载后滚动
                GM_setValue('Global_Pending_Restore', record);
            }, 500);
        }
        
        hideHistoryPanel();
    }

    // 更新历史记录UI
    function updateHistoryUI() {
        const history = getHistory();
        const historyList = document.getElementById('mdn-history-list');
        
        if (history.length === 0) {
            historyList.innerHTML = `<div style="padding:20px; text-align:center; color:#93a1a1;">
                暂无保存的位置记录
            </div>`;
            return;
        }
        
        historyList.innerHTML = '';
        
        history.forEach(record => {
            const item = document.createElement('div');
            item.className = 'mdn-history-item';
            item.innerHTML = `
                <div class="mdn-history-title">${record.pageTitle}</div>
                <div class="mdn-history-meta">
                    <span>${formatTime(record.timestamp)}</span>
                    <span>${record.scrollPercent}%</span>
                </div>
                <div class="mdn-history-actions">
                    <div class="mdn-restore-btn">恢复</div>
                    <div class="mdn-delete-btn">删除</div>
                </div>
            `;
            
            // 添加事件监听
            item.querySelector('.mdn-restore-btn').addEventListener('click', (e) => {
                e.stopPropagation();
                restoreRecord(record);
            });
            
            item.querySelector('.mdn-delete-btn').addEventListener('click', (e) => {
                e.stopPropagation();
                deleteRecord(record.id);
            });
            
            item.addEventListener('click', () => {
                restoreRecord(record);
            });
            
            historyList.appendChild(item);
        });
    }

    // 格式化时间
    function formatTime(timestamp) {
        const date = new Date(timestamp);
        return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
    }

    // 获取滚动百分比
    function getScrollPercentage(scrollY = window.scrollY) {
        const totalHeight = document.documentElement.scrollHeight - window.innerHeight;
        return totalHeight > 0 ? Math.round((scrollY / totalHeight) * 100) : 0;
    }

    // 显示通知
    function showNotification(message) {
        const existingNote = document.getElementById('mdn-position-notify');
        if (existingNote) existingNote.remove();
        
        const notification = document.createElement('div');
        notification.id = 'mdn-position-notify';
        notification.innerHTML = message;
        document.body.appendChild(notification);
        
        setTimeout(() => notification.remove(), 3000);
    }

    // 显示菜单
    function showMenu() {
        menuVisible = true;
        actionMenu.style.display = 'block';
        hideHistoryPanel();
    }

    // 隐藏菜单
    function hideMenu() {
        menuVisible = false;
        actionMenu.style.display = 'none';
    }

    // 显示历史面板
    function showHistoryPanel() {
        historyVisible = true;
        historyPanel.style.display = 'block';
        updateHistoryUI();
        hideMenu();
    }

    // 隐藏历史面板
    function hideHistoryPanel() {
        historyVisible = false;
        historyPanel.style.display = 'none';
    }

    // 切换菜单显示
    function toggleMenu() {
        if (menuVisible) {
            hideMenu();
        } else {
            showMenu();
        }
    }

    // 切换历史面板显示
    function toggleHistory() {
        if (historyVisible) {
            hideHistoryPanel();
        } else {
            showHistoryPanel();
        }
    }

    // 加载保存的位置
    function loadSavedPosition() {
        const savedPos = GM_getValue(HELPER_POSITION_KEY, null);
        if (savedPos) {
            helperContainer.style.left = savedPos.left;
            helperContainer.style.top = savedPos.top;
            helperContainer.style.right = 'auto';
            helperContainer.style.bottom = 'auto';
        }
    }

    // 保存当前位置
    function saveCurrentPosition() {
        const rect = helperContainer.getBoundingClientRect();
        const pos = {
            left: `${rect.left}px`,
            top: `${rect.top}px`
        };
        GM_setValue(HELPER_POSITION_KEY, pos);
    }

    // 主按钮点击事件
    mainBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        toggleMenu();
    });

    // 菜单按钮事件
    saveBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        addNewRecord();
        hideMenu();
    });

    historyBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        toggleHistory();
    });

    // 清空历史按钮
    document.getElementById('mdn-clear-history').addEventListener('click', (e) => {
        e.stopPropagation();
        clearHistory();
    });

    // 点击页面其他区域关闭所有面板
    document.addEventListener('click', (e) => {
        if (!helperContainer.contains(e.target) && !historyPanel.contains(e.target)) {
            hideMenu();
            hideHistoryPanel();
        }
    });

    // 检查是否有待恢复的记录(跨页面恢复)
    const pendingRestore = GM_getValue('Global_Pending_Restore', null);
    if (pendingRestore) {
        setTimeout(() => {
            window.scrollTo({ top: pendingRestore.scrollY, behavior: 'smooth' });
            showNotification(`✅ 位置已恢复!<br>${pendingRestore.pageTitle}`);
            GM_setValue('Global_Pending_Restore', null);
        }, 1000);
    }

    // 初始化历史记录
    updateHistoryUI();
    
    // 初始化位置
    loadSavedPosition();
    
    // ==============================
    // 拖拽功能实现
    // ==============================
    
    mainBtn.addEventListener('mousedown', startDrag);
    
    function startDrag(e) {
        if (e.button !== 0) return; // 只处理左键点击
        
        // 防止拖拽时触发其他事件
        e.preventDefault();
        e.stopPropagation();
        
        // 记录初始位置
        isDragging = true;
        dragStartX = e.clientX;
        dragStartY = e.clientY;
        
        // 获取当前helperContainer的位置
        const rect = helperContainer.getBoundingClientRect();
        initialX = rect.left;
        initialY = rect.top;
        
        // 添加拖拽样式
        helperContainer.classList.add('dragging');
        
        // 添加事件监听
        document.addEventListener('mousemove', doDrag);
        document.addEventListener('mouseup', stopDrag);
        
        // 关闭菜单
        hideMenu();
        hideHistoryPanel();
    }
    
    function doDrag(e) {
        if (!isDragging) return;
        
        // 计算偏移量
        const dx = e.clientX - dragStartX;
        const dy = e.clientY - dragStartY;
        
        // 更新位置
        helperContainer.style.left = `${initialX + dx}px`;
        helperContainer.style.top = `${initialY + dy}px`;
        helperContainer.style.right = 'auto';
        helperContainer.style.bottom = 'auto';
    }
    
    function stopDrag() {
        if (!isDragging) return;
        
        isDragging = false;
        helperContainer.classList.remove('dragging');
        
        // 保存位置
        saveCurrentPosition();
        
        // 移除事件监听
        document.removeEventListener('mousemove', doDrag);
        document.removeEventListener('mouseup', stopDrag);
    }
})();