智能网页书签管理器

一键复制网页标题和URL,支持在线管理书签,可自定义隐藏浮动按钮,让您的网络冲浪更加高效!现在支持配置导入导出。

// ==UserScript==
// @name 智能网页书签管理器
// @namespace http://tampermonkey.net/
// @version 2.22
// @description 一键复制网页标题和URL,支持在线管理书签,可自定义隐藏浮动按钮,让您的网络冲浪更加高效!现在支持配置导入导出。
// @match *://*/*
// @grant GM_setClipboard
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @license GPL-3.0
// ==/UserScript==

(function() {
    'use strict';

    // Helper function to generate UUID
    function generateUUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

    // Initialize settings
    let settings = GM_getValue('settings', {
        account: generateUUID().slice(0, 10),
        accessToken: generateUUID(),
        serverUrl: 'http://139.196.228.228:5055/api/save',
        registerUrl: 'http://139.196.228.228:8505',
        debugMode: false,
        showFloatingButton: true,
        hiddenUrls: []
    });

    // Create floating button
    const button = document.createElement('div');
    button.innerHTML = '📋';
    button.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        width: 50px;
        height: 50px;
        background-color: #4CAF50;
        color: white;
        border-radius: 50%;
        text-align: center;
        line-height: 50px;
        font-size: 24px;
        cursor: move;
        z-index: 9999;
        box-shadow: 0 2px 5px rgba(0,0,0,0.3);
        transition: background-color 0.3s;
        display: ${shouldShowButton() ? 'block' : 'none'};
    `;

    // Add button to the page
    document.body.appendChild(button);

    // Function to check if button should be shown
    function shouldShowButton() {
        if (!settings.showFloatingButton) return false;
        const currentUrl = window.location.href;
        return !settings.hiddenUrls.some(url => currentUrl.includes(url));
    }

    // Dragging functionality (unchanged)
    let isDragging = false;
    let startX, startY, startLeft, startTop;

    button.addEventListener('mousedown', function(e) {
        isDragging = true;
        startX = e.clientX;
        startY = e.clientY;
        startLeft = parseInt(window.getComputedStyle(button).left);
        startTop = parseInt(window.getComputedStyle(button).top);
        e.preventDefault();
    });

    document.addEventListener('mousemove', function(e) {
        if (!isDragging) return;
        let newLeft = startLeft + e.clientX - startX;
        let newTop = startTop + e.clientY - startY;

        newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - button.offsetWidth));
        newTop = Math.max(0, Math.min(newTop, window.innerHeight - button.offsetHeight));

        button.style.left = newLeft + 'px';
        button.style.top = newTop + 'px';
        button.style.bottom = 'auto';
        button.style.right = 'auto';
    });

    document.addEventListener('mouseup', function() {
        isDragging = false;
    });

    // Button click event
    button.addEventListener('click', function(e) {
        if (isDragging) return;

        const pageTitle = document.title;
        const pageUrl = window.location.href;
        const copyText = `${pageTitle}\n${pageUrl}`;

        GM_setClipboard(copyText, 'text');

        // Send data to server
        sendDataToServer(pageTitle, pageUrl);

        if (settings.debugMode) {
            alert(`Copied to clipboard and sending to server:\nTitle: ${pageTitle}\nURL: ${pageUrl}`);
        }
    });

    function sendDataToServer(title, url) {
        GM_xmlhttpRequest({
            method: "POST",
            url: settings.serverUrl,
            data: JSON.stringify({
                account: settings.account,
                accessToken: settings.accessToken,
                title: title,
                url: url
            }),
            headers: {
                "Content-Type": "application/json"
            },
            onload: function(response) {
                if (response.status === 200) {
                    button.innerHTML = '✅';
                    button.style.backgroundColor = '#45a049';
                    console.log("Data sent to server successfully:", response.responseText);
                } else {
                    button.innerHTML = '❌';
                    button.style.backgroundColor = '#f44336';
                    console.error("Failed to send data to server:", response.statusText);
                }
            },
            onerror: function(response) {
                button.innerHTML = '❌';
                button.style.backgroundColor = '#f44336';
                console.error("Failed to send data to server:", response.statusText);
            }
        });
    }

    // Settings popup
    function showSettings() {
        const settingsPopup = document.createElement('div');
        settingsPopup.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.2);
            z-index: 10001;
            font-family: Arial, sans-serif;
            width: 600px;
            height: 600px;
            overflow-y: auto;
        `;
        settingsPopup.innerHTML = `
            <h2 style="margin-top: 0; margin-bottom: 20px; color: #333; text-align: center;">设置</h2>
            <div style="display: flex; margin-bottom: 20px;">
                <button id="settingsTab" style="flex: 1; padding: 10px; background-color: #4CAF50; color: white; border: none; cursor: pointer;">常规设置</button>
                <button id="addRecordTab" style="flex: 1; padding: 10px; background-color: #ddd; color: black; border: none; cursor: pointer;">添加记录</button>
                <button id="serverConfigTab" style="flex: 1; padding: 10px; background-color: #ddd; color: black; border: none; cursor: pointer;">服务器配置</button>
                <button id="importExportTab" style="flex: 1; padding: 10px; background-color: #ddd; color: black; border: none; cursor: pointer;">导入/导出</button>
            </div>
            <div id="settingsContent">
                <div style="margin-bottom: 15px;">
                    <label for="account" style="display: inline-block; width: 100px; text-align: left; color: #666;">账户:</label>
                    <input type="text" id="account" value="${settings.account}" style="width: calc(100% - 110px); padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="accessToken" style="display: inline-block; width: 100px; text-align: left; color: #666;">访问令牌:</label>
                    <input type="text" id="accessToken" value="${settings.accessToken}" style="width: calc(100% - 110px); padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
                </div>
                <div style="margin-bottom: 15px;">
                    <label style="display: flex; align-items: center; color: #666;">
                        <input type="checkbox" id="debugMode" ${settings.debugMode ? 'checked' : ''} style="margin-right: 5px;">
                        调试模式
                    </label>
                </div>
                <div style="margin-bottom: 15px;">
                    <label style="display: flex; align-items: center; color: #666;">
                        <input type="checkbox" id="showFloatingButton" ${settings.showFloatingButton ? 'checked' : ''} style="margin-right: 5px;">
                        显示浮动按钮
                    </label>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="hiddenUrls" style="display: block; margin-bottom: 5px; color: #666;">隐藏按钮的 URL (每行一个):</label>
                    <div style="display: flex; align-items: center;">
                        <textarea id="hiddenUrls" style="flex-grow: 1; height: 100px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; resize: vertical;">${settings.hiddenUrls.join('\n')}</textarea>
                        <button id="addCurrentUrl" style="margin-left: 10px; padding: 8px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">添加当前URL</button>
                    </div>
                </div>
            </div>
            <div id="addRecordContent" style="display: none;">
                <div style="margin-bottom: 15px;">
                    <label for="bulkInput" style="display: block; margin-bottom: 5px; color: #666;">粘贴标题和 URL (每行一个):</label>
                    <textarea id="bulkInput" style="width: 100%; height: 150px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; resize: vertical;" placeholder="示例格式:
网页标题1
https://www.example1.com
网页标题2
https://www.example2.com"></textarea>
                </div>
                <button id="addBulkRecords" style="background-color: #4CAF50; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; width: 100%;">添加记录</button>
            </div>
            <div id="serverConfigContent" style="display: none;">
                <div style="margin-bottom: 15px;">
                    <label for="serverUrl" style="display: inline-block; width: 100px; text-align: left; color: #666;">服务器 URL:</label>
                    <textarea id="serverUrl" style="width: calc(100% - 110px); height: 60px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; resize: vertical;">${settings.serverUrl}</textarea>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="registerUrl" style="display: inline-block; width: 100px; text-align: left; color: #666;">注册 URL:</label>
                    <textarea id="registerUrl" style="width: calc(100% - 110px); height: 60px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; resize: vertical;">${settings.registerUrl}</textarea>
                </div>
            </div>
            <div id="importExportContent" style="display: none;">
                <div style="margin-bottom: 15px;">
                    <input type="file" id="importFile" accept=".json" style="display: none;">
                    <button id="importButton" style="background-color: #4CAF50; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; width: 100%; margin-bottom: 10px;">选择文件导入配置</button>
                    <button id="exportButton" style="background-color: #2196F3; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; width: 100%;">导出配置到文件</button>
                </div>
                <div id="importResult" style="margin-top: 15px; color: #666;"></div>
            </div>
            <div style="text-align: right; margin-top: 20px;">
                <button id="saveSettings" style="background-color: #4CAF50; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; margin-right: 10px;">保存</button>
                <button id="closeSettings" style="background-color: #f44336; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer;">关闭</button>
            </div>
        `;
        document.body.appendChild(settingsPopup);

        const settingsTab = document.getElementById('settingsTab');
        const addRecordTab = document.getElementById('addRecordTab');
        const serverConfigTab = document.getElementById('serverConfigTab');
        const importExportTab = document.getElementById('importExportTab');
        const settingsContent = document.getElementById('settingsContent');
        const addRecordContent = document.getElementById('addRecordContent');
        const serverConfigContent = document.getElementById('serverConfigContent');
        const importExportContent = document.getElementById('importExportContent');

        function setActiveTab(tab, content) {
            [settingsTab, addRecordTab, serverConfigTab, importExportTab].forEach(t => {
                t.style.backgroundColor = '#ddd';
                t.style.color = 'black';
            });
            [settingsContent, addRecordContent, serverConfigContent, importExportContent].forEach(c => c.style.display = 'none');
            tab.style.backgroundColor = '#4CAF50';
            tab.style.color = 'white';
            content.style.display = 'block';
        }

        settingsTab.addEventListener('click', () => setActiveTab(settingsTab, settingsContent));
        addRecordTab.addEventListener('click', () => setActiveTab(addRecordTab, addRecordContent));
        serverConfigTab.addEventListener('click', () => setActiveTab(serverConfigTab, serverConfigContent));
        importExportTab.addEventListener('click', () => setActiveTab(importExportTab, importExportContent));


document.getElementById('addCurrentUrl').addEventListener('click', function() {
            const currentUrl = window.location.href;
            const hiddenUrlsTextarea = document.getElementById('hiddenUrls');
            hiddenUrlsTextarea.value += (hiddenUrlsTextarea.value ? '\n' : '') + currentUrl;
        });

        document.getElementById('saveSettings').addEventListener('click', function() {
            settings.account = document.getElementById('account').value;
            settings.accessToken = document.getElementById('accessToken').value;
            settings.serverUrl = document.getElementById('serverUrl').value;
            settings.registerUrl = document.getElementById('registerUrl').value;
            settings.debugMode = document.getElementById('debugMode').checked;
            settings.showFloatingButton = document.getElementById('showFloatingButton').checked;
            settings.hiddenUrls = document.getElementById('hiddenUrls').value.split('\n').map(url => url.trim()).filter(url => url);
            GM_setValue('settings', settings);
            button.style.display = shouldShowButton() ? 'block' : 'none';
            settingsPopup.remove();
        });

        document.getElementById('closeSettings').addEventListener('click', function() {
            settingsPopup.remove();
        });

        document.getElementById('addBulkRecords').addEventListener('click', function() {
            const bulkInput = document.getElementById('bulkInput').value;
            const lines = bulkInput.split('\n');
            for (let i = 0; i < lines.length; i += 2) {
                const title = lines[i].trim();
                const url = lines[i + 1] ? lines[i + 1].trim() : '';
                if (title && url) {
                    sendDataToServer(title, url);
                }
            }
            document.getElementById('bulkInput').value = '';
            alert('记录添加成功!');
        });

        // Updated import/export functionality
        document.getElementById('importButton').addEventListener('click', function() {
            document.getElementById('importFile').click();
        });

        document.getElementById('importFile').addEventListener('change', function(event) {
            const file = event.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = function(e) {
                    try {
                        const importedSettings = JSON.parse(e.target.result);
                        Object.assign(settings, importedSettings);
                        GM_setValue('settings', settings);
                        document.getElementById('importResult').textContent = '配置导入成功!';
                        setTimeout(() => {
                            settingsPopup.remove();
                            showSettings(); // Refresh the settings popup
                        }, 1500);
                    } catch (error) {
                        document.getElementById('importResult').textContent = '导入失败:无效的 JSON 格式';
                    }
                };
                reader.readAsText(file);
            }
        });

        document.getElementById('exportButton').addEventListener('click', function() {
            const configData = JSON.stringify(settings, null, 2);
            const blob = new Blob([configData], {type: 'application/json'});
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'smart-bookmark-manager-config.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        });
    }

    // Right-click menu for settings
    button.addEventListener('contextmenu', function(e) {
        e.preventDefault();
        showSettings();
    });

    // Register menu command for Tampermonkey
    GM_registerMenuCommand("设置", showSettings);

    // Reset button state when the page is about to unload
    window.addEventListener('beforeunload', function() {
        button.innerHTML = '📋';
        button.style.backgroundColor = '#4CAF50';
    });
})();