Copy Google Contact

Copies Contact Name, Email, Phone, and Address in one click!

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Copy Google Contact
// @version      1.8
// @description  Copies Contact Name, Email, Phone, and Address in one click!
// @match        https://contacts.google.com/*
// @namespace    https://greasyfork.org/en/users/922168-mark-zinzow
// @author       Mark Zinzow
// @author       Gemini
// @grant        none
// @license MIT
// ==/UserScript==
/* eslint-disable spaced-comment */
// Educational & Debug Edition
// A heavily commented, high-contrast, fully logged script demonstrating how to navigate Single Page Applications.

(function() {
    'use strict';

    // =========================================================================
    // 1. THE CONTROL CENTER
    // =========================================================================
    // We use levels to control how much "noise" our script makes in the console.
    // Level 1 = Major events only (like finding a new person or copying data).
    // Level 2 = Verbose mode (the "heartbeat" that logs every time the page blinks).
    const DEBUG_LEVEL = 0; // Change this to 1 or 2 to follow along in the Dev Tools Console!

    // These variables act as our script's memory.
    let lastSeenName = null; // Remembers the name of the last contact we looked at.
    let buttonRef = null;    // Keeps a direct link to our custom button so we know if it gets detached.

    // A helper tool to print neat, timestamped messages in the browser's Developer Console.
    function debugLog(level, action, message, data = '') {
        if (DEBUG_LEVEL >= level) {
            // Grab the current time and chop off the date for a clean timestamp
            const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
            console.log(`[${timestamp}] 🔍 ${action.padEnd(15)} | ${message}`, data !== '' ? data : '');
        }
    }

    // =========================================================================
    // 2. THE GHOST BUSTER (Visibility Checker)
    // =========================================================================
    // Modern web pages (Single Page Applications) don't always delete old data when
    // you click to a new page; they just hide it invisibly in the background to save time.
    // Standard code will accidentally grab data from these hidden "ghosts."
    // This function searches the page but strictly ignores anything that isn't physically
    // visible on the user's screen.
    function getVisibleElement(selector) {
        const nodes = document.querySelectorAll(selector);
        for (let node of nodes) {
            // 'offsetParent' checks if the browser is currently drawing the element on the screen.
            if (node.offsetParent !== null || (node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0)) {
                return node; // We found the real, visible one!
            }
        }
        return null; // Nothing visible found yet.
    }

    // Searches specifically inside the current contact's box to see if our button is there.
    function getVisibleButton(parentContainer) {
        if (!parentContainer) return null;
        return parentContainer.querySelector('#gemini-copy-essentials');
    }

    // =========================================================================
    // 3. THE DETECTIVE (State Checker)
    // =========================================================================
    // This function runs every time the webpage twitches. It figures out where we are
    // and decides if it needs to take action.
    function checkDOMState() {
        // Find the visible Name on the screen.
        const nameEl = getVisibleElement('#details-header-name');
        const currentName = nameEl ? nameEl.textContent.trim() : null;

        // If the page is still loading and no name is visible, stop here and wait.
        if (!nameEl) return;

        // Look at the box holding the name, and check if our button is inside it.
        const parentContainer = nameEl.parentElement;
        const existingBtn = getVisibleButton(parentContainer);

        // --- NAVIGATION DETECTION ---
        // If the name we see now is different from the last one we saved, the user must have navigated!
        if (currentName && currentName !== lastSeenName) {
            debugLog(1, "NAV DETECTED", `Name changed from "${lastSeenName}" to "${currentName}"`);
            lastSeenName = currentName; // Update our memory

            // Visual Highlight: Make the active target obvious with high-contrast named colors
            nameEl.style.backgroundColor = 'LightYellow';
            nameEl.style.color = 'Black';
            nameEl.style.padding = '2px 6px';
            nameEl.style.borderRadius = '4px';
            nameEl.style.border = '1px solid Gold';
        }

        // --- THE HEARTBEAT (Verbose Logging) ---
        // These are the detective clues! If DEBUG_LEVEL is 2, this prints a report
        // on the exact status of our button every time the page settles down.
        const buttonReport = {
            foundInVisibleContainer: !!existingBtn,
            isOurSavedRef: existingBtn === buttonRef,
            isConnectedToPage: buttonRef ? buttonRef.isConnected : false
        };

        if (DEBUG_LEVEL >= 2 && currentName) {
           debugLog(2, "DOM SETTLED", `Checking state for ${currentName}...`, buttonReport);
        }

        // --- THE TRIGGER ---
        // If we are looking at a real person, but our button isn't there, it's time to build it!
        if (!existingBtn) {
             debugLog(1, "ACTION", `Button missing for ${currentName}. Injecting!`);
             injectButton(parentContainer);
        }
    }

    // =========================================================================
    // 4. THE BUILDER (Button Injector & Data Scraper)
    // =========================================================================
    function injectButton(parentContainer) {
         // Create a brand new button out of thin air
         const btn = document.createElement('button');
         btn.id = 'gemini-copy-essentials';
         btn.textContent = '📋 Copy Essentials';

         // Style the button using plain English named colors
         Object.assign(btn.style, {
            marginLeft: '20px',
            padding: '6px 14px',
            backgroundColor: 'MediumBlue', // A nice, standard Google-like blue
            color: 'White',                // White text for high contrast
            border: '2px solid Black',     // Sharp border to stand out against light grey backgrounds
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '14px',
            fontWeight: '600'
         });

         // Tell the button what to do when someone clicks it
         btn.onclick = (e) => {
            e.preventDefault();  // Stop the click from doing anything else by accident
            e.stopPropagation();

            debugLog(1, "COPY CLICKED", "Starting data extraction...");

            // Step 1: Grab the visible name
            const currentNameEl = getVisibleElement('#details-header-name');
            const name = currentNameEl ? currentNameEl.textContent.trim() : 'N/A';
            debugLog(2, "DATA FETCH", `Name extracted:`, name);

            // Step 2: Grab the visible email by looking for Google's specific invisible "aria-label"
            const emailBtn = getVisibleElement('button[aria-label^="Copy email"]');
            const email = emailBtn ? emailBtn.getAttribute('data-to-copy') : 'N/A';
            debugLog(2, "DATA FETCH", `Email extracted:`, email);

            // Step 3: Grab the visible phone number and strip out weird invisible formatting characters
            const phoneBtn = getVisibleElement('button[aria-label^="Copy phone"]');
            let phone = phoneBtn ? phoneBtn.getAttribute('data-to-copy') : 'N/A';
            phone = phone.replace(/[\u200B-\u200D\uFEFF\u202A-\u202C]/g, ''); // RegEx magic to clean text
            debugLog(2, "DATA FETCH", `Phone extracted:`, phone);

            // Step 4: Grab the visible address. If it has multiple lines, flatten it into one line with commas.
            const addressBtn = getVisibleElement('button[aria-label^="Copy address"]');
            let address = addressBtn ? addressBtn.getAttribute('data-to-copy') : 'N/A';
            if (address !== 'N/A') {
                address = address.split('\n')
                                 .map(part => part.trim())
                                 .filter(part => part.length > 0)
                                 .join(', ');
            }
            debugLog(2, "DATA FETCH", `Address extracted:`, address);

            // Piece it all together into a clean block of text
            const finalString = `Name: ${name}\nEmail: ${email}\nPhone: ${phone}\nAddress: ${address}`;

            // Send it to the computer's clipboard
            navigator.clipboard.writeText(finalString).then(() => {
                debugLog(1, "SUCCESS", "Data copied to clipboard!");

                // Temporarily change the button to show success
                const originalText = btn.textContent;
                btn.textContent = '✅ Copied!';
                btn.style.backgroundColor = 'ForestGreen'; // Green for success

                // Wait 1.5 seconds, then change it back
                setTimeout(() => {
                    btn.textContent = originalText;
                    btn.style.backgroundColor = 'MediumBlue';
                }, 1500);
            });
        };

         // Physically attach the button to the webpage
         parentContainer.appendChild(btn);
         buttonRef = btn; // Save our leash reference
    }

    // =========================================================================
    // 5. THE WATCHDOG (MutationObserver)
    // =========================================================================
    // This is the engine of the script. It tells the browser, "Let me know the instant
    // anything on the webpage changes."
    let timeoutId;
    const observer = new MutationObserver(() => {
        // Because a page loading fires hundreds of changes a second, we use a "debounce".
        // We cancel the timer if changes are still happening rapidly.
        clearTimeout(timeoutId);
        // We only run our Detective function once the page has been quiet for 1/4 of a second.
        timeoutId = setTimeout(() => {
            checkDOMState();
        }, 250);
    });

    // Start watching the entire body of the webpage for any additions or removals
    observer.observe(document.body, { childList: true, subtree: true });

    // Kick things off when the script first loads
    debugLog(1, "INIT", "Userscript loaded and observing.");
    setTimeout(checkDOMState, 500);

})();