Copy Google Contact

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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);

})();