智能网页书签管理器

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

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 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 智能网页书签管理器
// @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';
    });
})();