PageMemory

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
    }
})();