Greasy Fork

Greasy Fork is available in English.

发送到 Memos

将选中的链接或文本发送到 Memos,支持配置、添加源页面信息和快速发送按钮

当前为 2025-04-20 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name            发送到 Memos
// @name:en         Send to Memos
// @namespace       http://tampermonkey.net/
// @version         2.4
// @description     将选中的链接或文本发送到 Memos,支持配置、添加源页面信息和快速发送按钮
// @description:en  Send selected links or text to Memos, supports configuration, adding source page information, and quick send buttons
// @author          Hu3rror
// @match           *://*/*
// @grant           GM_xmlhttpRequest
// @grant           GM_registerMenuCommand
// @grant           GM_getValue
// @grant           GM_setValue
// @grant           GM_addStyle
// @license         MIT
// ==/UserScript==
(function() {
    'use strict';

    // 获取保存的配置或使用默认值
    const MEMOS_API_URL = GM_getValue('MEMOS_API_URL', '');
    const API_TOKEN = GM_getValue('API_TOKEN', '');
    let isConfigured = MEMOS_API_URL && API_TOKEN;

    // 检测系统颜色主题
    function isDarkMode() {
        return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
    }

    // 添加样式(包含优化后的暗黑模式)
    GM_addStyle(`
        #memos-float-button {
            position: fixed;
            width: 36px;
            height: 36px;
            background-color: rgba(76, 175, 80, 0.7);
            color: white;
            border-radius: 50%;
            text-align: center;
            line-height: 36px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            box-shadow: 1px 1px 3px rgba(0,0,0,0.2);
            z-index: 9999;
            user-select: none;
            display: none; /* 初始状态隐藏 */
            align-items: center;
            justify-content: center;
            right: 20px;
            bottom: 20px;
            transition: all 0.3s ease;
        }
        #memos-float-button:hover {
            background-color: rgba(69, 160, 73, 0.9);
            opacity: 1;
        }
        .memos-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.5);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10000;
        }
        .memos-modal-content {
            padding: 20px;
            border-radius: 8px;
            width: 400px;
            max-width: 90%;
            transition: all 0.3s ease;
        }
        .memos-modal-content.light-mode {
            background-color: white;
            color: #333;
        }
        .memos-modal-content.dark-mode {
            background-color: #1e1e1e;
            color: #e0e0e0;
        }
        .memos-modal h2 {
            margin-top: 0;
        }
        .memos-form-group {
            margin-bottom: 15px;
        }
        .memos-form-group label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        .memos-form-group input {
            width: 100%;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            transition: all 0.3s ease;
        }
        .memos-modal-content.light-mode .memos-form-group input {
            background-color: white;
            border-color: #ddd;
            color: #333;
        }
        .memos-modal-content.dark-mode .memos-form-group input {
            background-color: #2d2d2d;
            border-color: #3d3d3d;
            color: #e0e0e0;
        }
        .memos-button {
            padding: 8px 16px;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-right: 10px;
            transition: all 0.3s ease;
        }
        .memos-button.save {
            background-color: #4caf50;
        }
        .memos-button.cancel {
            background-color: #f44336;
        }
        .memos-button:hover {
            opacity: 0.9;
        }
        .memos-buttons {
            display: flex;
            justify-content: flex-end;
            margin-top: 20px;
        }

        /* 优化后的暗黑模式按钮样式 */
        .dark-mode #memos-float-button {
            background-color: rgba(60, 70, 60, 0.5); /* 更暗、更柔和的绿色 */
            box-shadow: 0 1px 2px rgba(0,0,0,0.3);
        }
        .dark-mode #memos-float-button:hover {
            background-color: rgba(70, 80, 70, 0.7); /* 悬停时略微变亮 */
        }

        /* 暗黑模式下的模态框按钮 */
        .memos-modal-content.dark-mode .memos-button.save {
            background-color: #3a7d3f; /* 更暗的绿色 */
        }
        .memos-modal-content.dark-mode .memos-button.cancel {
            background-color: #c53929; /* 更暗的红色 */
        }
    `);

    // 创建自动关闭的通知
    function showNotification(message, isError = false) {
        const notification = document.createElement('div');
        notification.style.position = 'fixed';
        notification.style.top = '20px';
        notification.style.right = '20px';
        notification.style.padding = '10px 20px';
        notification.style.backgroundColor = isError ? '#f44336' : '#4caf50';
        notification.style.color = 'white';
        notification.style.borderRadius = '5px';
        notification.style.zIndex = '9999';
        notification.style.opacity = '0.9';
        notification.style.transition = 'opacity 0.2s ease';
        notification.textContent = message;

        document.body.appendChild(notification);

        // 1.5秒后淡出并移除
        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => {
                notification.remove();
            }, 500); // 等待淡出动画完成
        }, 1500);
    }

    // 获取选中文本
    function getSelectedText() {
        return window.getSelection().toString().trim();
    }

    // 获取当前页面信息,以 Markdown 格式
    function getPageInfo() {
        const pageTitle = document.title || '无标题页面';
        const pageUrl = window.location.href;
        return `\n\n---\n**来源**:[${pageTitle}](${pageUrl})`;
    }

    // 发送内容到 Memos 的函数
    function sendToMemos(content) {
        if (!isConfigured) {
            showNotification('请先配置 Memos API URL 和 Token', true);
            showConfigModal();
            return;
        }

        // 添加页面来源信息
        content += getPageInfo();

        GM_xmlhttpRequest({
            method: 'POST',
            url: MEMOS_API_URL,
            headers: {
                'Authorization': `Bearer ${API_TOKEN}`,
                'Content-Type': 'application/json'
            },
            data: JSON.stringify({
                content: content,
                visibility: 'PRIVATE' // 可选:'PUBLIC', 'PROTECTED', 'PRIVATE'
            }),
            onload: function(response) {
                if (response.status === 200 || response.status === 201) {
                    showNotification('成功发送到 Memos!');
                } else {
                    showNotification(`发送到 Memos 失败。状态码: ${response.status}\n可能端点错误,建议尝试 /api/v1/memo 或 /api/memos。`, true);
                }
            },
            onerror: function(error) {
                showNotification('发送到 Memos 时出错: ' + error, true);
            }
        });
    }

    // 存储当前右键点击的链接
    let currentLink = null;

    // 监听右键点击事件,捕获链接
    document.addEventListener('contextmenu', function(event) {
        currentLink = event.target.closest('a');
    }, false);

    // 注册菜单命令
    GM_registerMenuCommand('发送选中内容到 Memos', function() {
        handleSendAction();
    });

    GM_registerMenuCommand('配置 Memos API', function() {
        showConfigModal();
    });

    // 处理发送动作
    function handleSendAction() {
        const selectedText = getSelectedText();
        if (selectedText) {
            // 如果有选中文本,发送选中文本
            sendToMemos(selectedText);
        } else if (currentLink && currentLink.href) {
            // 如果没有选中文本但有链接,发送链接
            const linkText = currentLink.textContent.trim() || '无文本';
            const linkUrl = currentLink.href;
            const content = `${linkText}\n${linkUrl}`;
            sendToMemos(content);
        } else {
            showNotification('请先选择文本或右键点击链接', true);
        }
    }

    // 清理 currentLink,防止重复使用
    document.addEventListener('click', function() {
        currentLink = null;
    }, false);

    // 创建配置模态框
    function showConfigModal() {
        // 创建模态框容器
        const modal = document.createElement('div');
        modal.className = 'memos-modal';

        // 创建模态框内容
        const modalContent = document.createElement('div');
        modalContent.className = `memos-modal-content ${isDarkMode() ? 'dark-mode' : 'light-mode'}`;

        modalContent.innerHTML = `
            <h2>配置 Memos API</h2>
            <div class="memos-form-group">
                <label for="memos-api-url">API URL:</label>
                <input type="text" id="memos-api-url" placeholder="例如:https://example.com/api/v1/memos" value="${MEMOS_API_URL}">
                <small style="color:#666;display:block;margin-top:5px;">注意:保留 /api/v1/memos 路径</small>
            </div>
            <div class="memos-form-group">
                <label for="memos-api-token">API Token:</label>
                <input type="password" id="memos-api-token" placeholder="输入您的 API Token" value="${API_TOKEN}">
            </div>
            <div class="memos-buttons">
                <button class="memos-button cancel">取消</button>
                <button class="memos-button save">保存</button>
            </div>
        `;

        modal.appendChild(modalContent);
        document.body.appendChild(modal);

        // 监听系统颜色模式变化
        const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
        const handleColorSchemeChange = (e) => {
            modalContent.className = `memos-modal-content ${e.matches ? 'dark-mode' : 'light-mode'}`;
        };
        colorSchemeQuery.addEventListener('change', handleColorSchemeChange);

        // 添加事件监听器
        const cancelButton = modalContent.querySelector('.memos-button.cancel');
        const saveButton = modalContent.querySelector('.memos-button.save');

        cancelButton.addEventListener('click', function() {
            colorSchemeQuery.removeEventListener('change', handleColorSchemeChange);
            modal.remove();
        });

        saveButton.addEventListener('click', function() {
            const apiUrl = document.getElementById('memos-api-url').value.trim();
            const apiToken = document.getElementById('memos-api-token').value.trim();

            if (!apiUrl || !apiToken) {
                showNotification('API URL 和 Token 不能为空', true);
                return;
            }

            GM_setValue('MEMOS_API_URL', apiUrl);
            GM_setValue('API_TOKEN', apiToken);
            isConfigured = true;

            showNotification('配置已保存');
            colorSchemeQuery.removeEventListener('change', handleColorSchemeChange);
            modal.remove();

            // 检查并创建浮动按钮(仅当配置完成时)
            updateFloatButton();
        });
    }

    // 创建浮动按钮
    function createFloatButton() {
        // 如果已存在,不再创建
        if (document.getElementById('memos-float-button')) {
            return;
        }
        const floatButton = document.createElement('div');
        floatButton.id = 'memos-float-button';
        floatButton.textContent = 'M';
        floatButton.title = '发送到 Memos';
        document.body.appendChild(floatButton);
        // 应用保存的位置
        const savedPosition = GM_getValue('MEMOS_BUTTON_POSITION', null);
        if (savedPosition) {
            floatButton.style.left = savedPosition.left + 'px';
            floatButton.style.top = savedPosition.top + 'px';
            floatButton.style.right = 'auto';
            floatButton.style.bottom = 'auto';
        }
        // 添加点击事件
        floatButton.addEventListener('click', function(e) {
            if (!floatButton.isDragging) {
                handleSendAction();
            }
        });
        // 添加拖动功能
        let isDragging = false;
        let offsetX, offsetY;
        floatButton.addEventListener('mousedown', function(e) {
            isDragging = true;
            floatButton.isDragging = true; // 添加一个标志
            offsetX = e.clientX - floatButton.getBoundingClientRect().left;
            offsetY = e.clientY - floatButton.getBoundingClientRect().top;
            e.preventDefault(); // 防止选中文本
        });
        document.addEventListener('mousemove', function(e) {
            if (isDragging) {
                const x = e.clientX - offsetX;
                const y = e.clientY - offsetY;
                // 确保按钮不会超出视口
                const maxX = window.innerWidth - floatButton.offsetWidth;
                const maxY = window.innerHeight - floatButton.offsetHeight;
                floatButton.style.left = Math.min(Math.max(0, x), maxX) + 'px';
                floatButton.style.right = 'auto';
                floatButton.style.top = Math.min(Math.max(0, y), maxY) + 'px';
                floatButton.style.bottom = 'auto';
            }
        });
        document.addEventListener('mouseup', function() {
            if (isDragging) {
                // 保存按钮位置
                GM_setValue('MEMOS_BUTTON_POSITION', {
                    left: parseInt(floatButton.style.left, 10) || 0,
                    top: parseInt(floatButton.style.top, 10) || 0
                });
                isDragging = false;
            }
            floatButton.isDragging = false; // 拖动结束,重置标志
        });
        // 监听系统颜色模式变化并更新按钮样式
        const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
        const updateButtonStyle = (e) => {
            if (e.matches) {
                document.body.classList.add('dark-mode');
            } else {
                document.body.classList.remove('dark-mode');
            }
        };

        // 初始设置
        updateButtonStyle(colorSchemeQuery);
        colorSchemeQuery.addEventListener('change', updateButtonStyle);
    }

    // 更新浮动按钮 - 仅当配置完成时显示
    function updateFloatButton() {
        if (isConfigured) {
            createFloatButton();
        } else {
            const existingButton = document.getElementById('memos-float-button');
            if (existingButton) {
                existingButton.remove();
            }
        }
    }

    // 监听选中文本事件
    document.addEventListener('mouseup', function() {
        const selectedText = getSelectedText();
        const floatButton = document.getElementById('memos-float-button');

        if (selectedText && isConfigured) {
            // 如果有选中文本且已配置,显示按钮
            if (!floatButton) {
                createFloatButton(); // 如果按钮不存在则创建
            }
            floatButton.style.display = 'flex';
        } else {
            // 如果没有选中文本或未配置,隐藏按钮
            if (floatButton) {
                floatButton.style.display = 'none';
            }
        }
    });

    // 初始化 - 不自动弹出配置窗口,只检查是否已配置并决定是否显示浮动按钮
    function init() {
        // 首次加载时,检查是否有选中文本,并决定是否显示按钮
        const selectedText = getSelectedText();
        if (selectedText && isConfigured) {
            createFloatButton();
            const floatButton = document.getElementById('memos-float-button');
            if(floatButton){
                 floatButton.style.display = 'flex';
            }

        }
    }

    // 在页面加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();