Greasy Fork

Greasy Fork is available in English.

Google Search Suggestions Collector

Collect Google search suggestions

目前为 2024-12-06 提交的版本,查看 最新版本

// ==UserScript==
// @name         Google Search Suggestions Collector
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Collect Google search suggestions
// @author       WWW
// @include      *://www.google.*/*
// @include      *://google.*/*
// @grant        GM_setClipboard
// @license      MIT
// ==/UserScript==


(function() {
    'use strict';

    // 在全局作用域内添加状态变量
    let isCollecting = false;
    let shouldStop = false;

    function addStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .suggest-collector-btn {
                position: fixed;
                right: 200px;
                top: 20px;
                width: 50px;
                height: 50px;
                border-radius: 25px;
                background: var(--collector-bg, #ffffff);
                border: 2px solid var(--collector-border, #e0e0e0);
                box-shadow: 0 2px 12px rgba(0,0,0,0.15);
                cursor: move;
                z-index: 10000;
                display: flex;
                align-items: center;
                justify-content: center;
                user-select: none;
            }

            .suggest-collector-panel {
                position: fixed;
                width: 300px;
                background: var(--collector-bg, #ffffff);
                border: 1px solid var(--collector-border, #e0e0e0);
                border-radius: 8px;
                padding: 15px;
                box-shadow: 0 2px 12px rgba(0,0,0,0.15);
                z-index: 9999;
                display: none;
            }

            .suggest-collector-panel input {
                width: 100%;
                padding: 8px;
                border: 1px solid var(--collector-border, #e0e0e0);
                border-radius: 4px;
                margin-bottom: 10px;
                background: var(--collector-input-bg, #ffffff);
                color: var(--collector-text, #333333);
            }

            .suggest-collector-panel button {
                padding: 8px 16px;
                border: none;
                border-radius: 4px;
                background: #4CAF50;
                color: white;
                cursor: pointer;
                transition: background 0.3s;
            }

            .suggest-collector-panel button:hover {
                background: #45a049;
            }

            .suggest-collector-panel textarea {
                background: var(--collector-input-bg, #ffffff);
                color: var(--collector-text, #333333);
                border: 1px solid var(--collector-border, #e0e0e0);
                border-radius: 4px;
            }

            @media (prefers-color-scheme: dark) {
                :root {
                    --collector-bg: #2d2d2d;
                    --collector-border: #404040;
                    --collector-text: #e0e0e0;
                    --collector-input-bg: #3d3d3d;
                }
            }

            .input-mode-selector {
                display: flex;
                gap: 20px;
                margin-bottom: 15px;
                padding: 0 10px;
            }

            .input-mode-selector label {
                display: flex;
                align-items: center;
                gap: 5px;
                cursor: pointer;
                color: var(--collector-text, #333333);
                min-width: 70px;
            }

            .input-mode-selector input[type="radio"],
            .filter-options input[type="checkbox"] {
                margin: 0;
                cursor: pointer;
                width: 16px;
                height: 16px;
            }

            .filter-options {
                margin-bottom: 15px;
                padding: 0 10px;
            }

            .filter-options label {
                display: flex;
                align-items: center;
                gap: 5px;
                cursor: pointer;
                color: var(--collector-text, #333333);
                justify-content: flex-end;
            }

            #singleInput {
                padding: 0 10px;
            }

            .depth-selector {
                margin-bottom: 15px;
                padding: 0 10px;
                display: flex;
                align-items: center;
                gap: 10px;
            }

            .depth-selector label {
                color: var(--collector-text, #333333);
            }

            .depth-selector select {
                padding: 5px;
                border-radius: 4px;
                border: 1px solid var(--collector-border, #e0e0e0);
                background: var(--collector-input-bg, #ffffff);
                color: var(--collector-text, #333333);
                cursor: pointer;
            }
        `;
        document.head.appendChild(style);
    }

    function createUI() {
        const btn = document.createElement('div');
        btn.className = 'suggest-collector-btn';
        btn.innerHTML = '🔍';
        document.body.appendChild(btn);

        const panel = document.createElement('div');
        panel.className = 'suggest-collector-panel';
        panel.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
                <div class="input-mode-selector">
                    <label><input type="radio" name="inputMode" value="single" checked> single</label>
                    <label><input type="radio" name="inputMode" value="batch"> batch</label>
                </div>
                <div class="filter-options">
                    <label><input type="checkbox" id="onlyEnglish"> Only English</label>
                </div>
            </div>
            <div class="depth-selector">
                <label>Search Depth:</label>
                <select id="searchDepth">
                    <option value="1">1 letter</option>
                    <option value="2">2 letters</option>
                    <option value="3">3 letters</option>
                    <option value="4">4 letters</option>
                    <option value="5">5 letters</option>
                </select>
            </div>
            <div id="singleInput">
                <input type="text" id="baseKeyword" placeholder="type keyword">
            </div>
            <div id="batchInput" style="display: none;">
                <textarea id="batchKeywords" placeholder="type keyword in each line" style="width: 100%; height: 100px; margin-bottom: 10px;"></textarea>
            </div>
            <button id="startCollect">start collect</button>
            <div id="progress" style="display: none; margin-top: 10px;">
                <div style="margin-bottom: 8px;">
                    total progress: <span id="totalProgress">0/0</span>
                    <div style="background: var(--collector-border); height: 20px; border-radius: 10px;">
                        <div id="totalProgressBar" style="width: 0%; height: 100%; background: #4CAF50; border-radius: 10px; transition: width 0.3s;"></div>
                    </div>
                </div>
                <div style="margin-bottom: 8px;">
                    current keyword progress: <span id="progressText">0/26</span>
                    <div style="background: var(--collector-border); height: 20px; border-radius: 10px;">
                        <div id="progressBar" style="width: 0%; height: 100%; background: #4CAF50; border-radius: 10px; transition: width 0.3s;"></div>
                    </div>
                </div>
                <div>collected: <span id="collectedCount">0</span> items</div>
            </div>
            <div id="result" style="max-height: 300px; overflow-y: auto; margin-top: 10px;"></div>
        `;
        document.body.appendChild(panel);

        let isDragging = false;
        let currentX;
        let currentY;
        let initialX;
        let initialY;
        let xOffset = 0;
        let yOffset = 0;

        // 更新面板位置的函数
        function updatePanelPosition() {
            const btnRect = btn.getBoundingClientRect();
            panel.style.right = `${window.innerWidth - (btnRect.right + 20)}px`;
            panel.style.top = `${btnRect.bottom + 20}px`;
        }

        btn.addEventListener('mousedown', dragStart);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', dragEnd);

        function dragStart(e) {
            initialX = e.clientX - xOffset;
            initialY = e.clientY - yOffset;
            if (e.target === btn) {
                isDragging = true;
            }
        }

        function drag(e) {
            if (isDragging) {
                e.preventDefault();
                currentX = e.clientX - initialX;
                currentY = e.clientY - initialY;
                xOffset = currentX;
                yOffset = currentY;
                btn.style.transform = `translate(${currentX}px, ${currentY}px)`;
                // 拖动时更新面板位置
                updatePanelPosition();
            }
        }

        function dragEnd() {
            isDragging = false;
        }

        btn.addEventListener('click', (e) => {
            if (!isDragging) {
                panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
                if (panel.style.display === 'block') {
                    updatePanelPosition();
                }
            }
        });

        const radioButtons = panel.querySelectorAll('input[name="inputMode"]');
        radioButtons.forEach(radio => {
            radio.addEventListener('change', (e) => {
                document.getElementById('singleInput').style.display = e.target.value === 'single' ? 'block' : 'none';
                document.getElementById('batchInput').style.display = e.target.value === 'batch' ? 'block' : 'none';
            });
        });
    }

    async function getSuggestions(keyword) {
        const response = await fetch(`https://suggestqueries.google.com/complete/search?client=chrome&q=${encodeURIComponent(keyword)}`);
        const data = await response.json();
        return data[1];
    }

    function updateProgress(current, total, collectedItems) {
        const progressBar = document.getElementById('progressBar');
        const progressText = document.getElementById('progressText');
        const collectedCount = document.getElementById('collectedCount');
        const progress = document.getElementById('progress');

        progress.style.display = 'block';
        const percentage = (current / total) * 100;
        progressBar.style.width = percentage + '%';
        progressText.textContent = `${current}/${total}`;
        collectedCount.textContent = collectedItems.size;
    }

    function generateCombinations(letters, depth) {
        if (depth === 1) return letters.map(letter => [letter]);

        const combinations = [];
        for (let i = 0; i < letters.length; i++) {
            const subCombinations = generateCombinations(letters.slice(i + 1), depth - 1);
            subCombinations.forEach(subComb => {
                combinations.push([letters[i], ...subComb]);
            });
        }
        return combinations;
    }

    async function collectSuggestions(baseKeyword) {
        const result = new Set();
        const letters = 'abcdefghijklmnopqrstuvwxyz'.split('');
        const resultDiv = document.getElementById('result');
        const onlyEnglish = document.getElementById('onlyEnglish').checked;
        const searchDepth = parseInt(document.getElementById('searchDepth').value);

        const isEnglishOnly = (text) => /^[A-Za-z0-9\s.,!?-]+$/.test(text);

        if (shouldStop) {
            return Array.from(result);
        }

        // 收集基础关键词的建议
        const baseSuggestions = await getSuggestions(baseKeyword);
        baseSuggestions.forEach(s => {
            if (!onlyEnglish || isEnglishOnly(s)) {
                result.add(s);
            }
        });

        // 生成所有可能的字母组合
        const allCombinations = [];
        for (let depth = 1; depth <= searchDepth; depth++) {
            const depthCombinations = generateCombinations(letters, depth);
            allCombinations.push(...depthCombinations);
        }

        // 更新进度条的总数
        const totalCombinations = allCombinations.length;
        updateProgress(0, totalCombinations, result);

        // 对每个组合进行查询
        for (let i = 0; i < allCombinations.length; i++) {
            if (shouldStop) {
                break;
            }

            const combination = allCombinations[i];
            const letterCombination = combination.join('');
            const suggestions = await getSuggestions(`${baseKeyword} ${letterCombination}`);

            suggestions.forEach(s => {
                if (!onlyEnglish || isEnglishOnly(s)) {
                    result.add(s);
                }
            });

            updateProgress(i + 1, totalCombinations, result);
            resultDiv.innerHTML = `<textarea style="width: 100%; height: 200px;">${Array.from(result).join('\n')}</textarea>`;

            await new Promise(resolve => setTimeout(resolve, 200));
        }

        return Array.from(result);
    }

    function init() {
        addStyles();
        createUI();

        const startCollectBtn = document.getElementById('startCollect');

        startCollectBtn.addEventListener('click', async () => {
            if (isCollecting) {
                // 如果正在收集,点击按钮则停止
                shouldStop = true;
                startCollectBtn.textContent = 'start collect';
                startCollectBtn.style.background = '#4CAF50';
                isCollecting = false;
                return;
            }

            const isBatchMode = document.querySelector('input[name="inputMode"]:checked').value === 'batch';
            let keywords = [];

            if (isBatchMode) {
                const batchText = document.getElementById('batchKeywords').value.trim();
                keywords = batchText.split('\n').filter(k => k.trim());
            } else {
                const singleKeyword = document.getElementById('baseKeyword').value.trim();
                if (singleKeyword) {
                    keywords = [singleKeyword];
                }
            }

            if (keywords.length === 0) {
                alert('Please enter a keyword');
                return;
            }

            // 开始收集
            isCollecting = true;
            shouldStop = false;
            startCollectBtn.textContent = 'stop collect';
            startCollectBtn.style.background = '#ff4444';

            const resultDiv = document.getElementById('result');
            resultDiv.innerHTML = 'Collecting...';
            document.getElementById('progress').style.display = 'block';

            try {
                const allSuggestions = new Set();
                const totalKeywords = keywords.length;

                for (let i = 0; i < keywords.length; i++) {
                    if (shouldStop) {
                        break;
                    }

                    const keyword = keywords[i];
                    document.getElementById('totalProgress').textContent = `${i + 1}/${totalKeywords}`;
                    document.getElementById('totalProgressBar').style.width = `${((i + 1) / totalKeywords) * 100}%`;

                    const suggestions = await collectSuggestions(keyword);
                    suggestions.forEach(s => allSuggestions.add(s));
                }

                const resultText = Array.from(allSuggestions).join('\n');
                resultDiv.innerHTML = `
                    <textarea style="width: 100%; height: 200px;">${resultText}</textarea>
                    <button id="copyBtn">Copy to Clipboard</button>
                `;

                document.getElementById('copyBtn').addEventListener('click', () => {
                    GM_setClipboard(resultText);
                    alert('Copied to clipboard!');
                });
            } catch (error) {
                resultDiv.innerHTML = 'Error occurred while collecting: ' + error.message;
            } finally {
                // 恢复按钮状态
                isCollecting = false;
                shouldStop = false;
                startCollectBtn.textContent = 'start collect';
                startCollectBtn.style.background = '#4CAF50';
            }
        });
    }

    init();
})();