LinkedIn Job Info Copier

Adds a button to copy LinkedIn job post information to the clipboard.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         LinkedIn Job Info Copier
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Adds a button to copy LinkedIn job post information to the clipboard.
// @author       Lich_Amnesia
// @match        https://www.linkedin.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=linkedin.com
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const BUTTON_ID = 'copy-job-info-button';
    const BUTTON_TEXT = '📋 Copy Job Info';
    const BUTTON_COPIED_TEXT = '✅ Copied!';

    // --- Helper function to safely get text content ---
    function safeGetText(selector, element = document) {
        try {
            const el = element.querySelector(selector);
            // Prioritize innerText for better representation of rendered text, fallback to textContent
            return el ? (el.innerText || el.textContent || '').trim() : 'N/A';
        } catch (e) {
            console.error(`Error getting text for selector "${selector}":`, e);
            return 'Error';
        }
    }

    // --- Helper function to safely get text from multiple elements ---
     function safeGetAllText(selector, separator = '\n- ', element = document) {
        try {
            const els = element.querySelectorAll(selector);
            if (els.length === 0) return 'N/A';
            return Array.from(els).map(el => (el.innerText || el.textContent || '').trim()).filter(Boolean).join(separator);
        } catch (e) {
            console.error(`Error getting all text for selector "${selector}":`, e);
            return 'Error';
        }
    }

    // --- Function to extract job details ---
    function getJobInfo() {
        const topCardSelector = '.job-details-jobs-unified-top-card__container--two-pane'; // More specific container
        const topCardElement = document.querySelector(topCardSelector);

        if (!topCardElement) {
             console.warn("LinkedIn Job Info Copier: Top card element not found yet.");
             return null; // Element not ready
        }


        const jobTitle = safeGetText('.job-details-jobs-unified-top-card__job-title h1 a', topCardElement) || safeGetText('.job-details-jobs-unified-top-card__job-title h1', topCardElement); // Handle both cases h1>a or just h1
        const companyName = safeGetText('.job-details-jobs-unified-top-card__company-name a', topCardElement);
        const tertiaryInfo = safeGetText('.job-details-jobs-unified-top-card__primary-description-container .job-details-jobs-unified-top-card__tertiary-description-container', topCardElement).replace(/\s*·\s*/g, ' | '); // Location, Posted, Applicants

        // Extract pills (Salary, Hybrid, Full-time etc.)
        const pillsRaw = safeGetAllText('.job-details-preferences-and-skills__pill span.ui-label', topCardElement, ' | ');
        // Attempt to split Salary/Benefits from other pills if structure is consistent
        let salary = 'N/A';
        let detailsPills = [];
        const pillElements = topCardElement.querySelectorAll('.job-details-preferences-and-skills__pill span.ui-label');
        pillElements.forEach(pill => {
            const text = (pill.innerText || pill.textContent || '').trim();
            if (text.includes('$') || text.toLowerCase().includes('salary') || text.toLowerCase().includes('/yr')) {
                salary = text;
            } else if (!text.toLowerCase().includes('skills match')) { // Exclude the 'skills match' pill
                detailsPills.push(text);
            }
        });
        const otherDetails = detailsPills.join(' | ') || 'N/A';


        // Job Description
        const jobDescriptionElement = document.querySelector('#job-details .jobs-box__html-content') || document.querySelector('#job-details'); // Try specific content div first
        let jobDescription = 'N/A';
        if (jobDescriptionElement) {
            // Clone to avoid modifying the original, remove the "About the job" heading if present
            const clone = jobDescriptionElement.cloneNode(true);
            const heading = clone.querySelector('h2');
            if (heading && (heading.innerText || heading.textContent || '').trim().toLowerCase() === 'about the job') {
                heading.remove();
            }
             // Try to get innerText for better formatting, fallback to textContent
            jobDescription = (clone.innerText || clone.textContent || '').trim().replace(/\n{3,}/g, '\n\n'); // Reduce excessive newlines
        }


        const jobUrl = window.location.href;

        // Format the output
        const formattedInfo = `Job Title: ${jobTitle}\n` +
                              `Company: ${companyName}\n` +
                              `Location/Info: ${tertiaryInfo}\n` +
                              `Salary/Compensation: ${salary}\n` +
                              `Type/Mode: ${otherDetails}\n` +
                              `URL: ${jobUrl}\n\n` +
                              `---------------- Job Description ----------------\n` +
                              `${jobDescription}`;

        return formattedInfo;
    }

    // --- Function to copy text to clipboard and provide feedback ---
    function copyInfoToClipboard(button) {
        const jobInfoText = getJobInfo();
        if (!jobInfoText) {
             alert("Could not extract job information. The page structure might have changed or not fully loaded.");
             return;
        }

        GM_setClipboard(jobInfoText, 'text');

        // Provide feedback
        const originalText = button.textContent;
        button.textContent = BUTTON_COPIED_TEXT;
        button.disabled = true;
        button.classList.add('copied');

        setTimeout(() => {
            button.textContent = originalText;
            button.disabled = false;
            button.classList.remove('copied');
        }, 2000); // Revert after 2 seconds
    }

    // --- Function to create and add the button ---
    function addCopyButton() {
         // Check if button already exists
        if (document.getElementById(BUTTON_ID)) {
            return;
        }

        // Find the container for the "Easy Apply" / "Save" buttons within the top card
        // Look for the div containing buttons like '.jobs-apply-button' or '.jobs-save-button'
        const buttonContainer = document.querySelector('.job-details-jobs-unified-top-card__container--two-pane .mt4 .display-flex');

        if (buttonContainer) {
            const copyButton = document.createElement('button');
            copyButton.id = BUTTON_ID;
            copyButton.textContent = BUTTON_TEXT;
            // Try to mimic LinkedIn button styles
            copyButton.className = 'artdeco-button artdeco-button--secondary artdeco-button--3'; // Using secondary style like 'Save'
            copyButton.style.marginLeft = '8px'; // Add some space

            copyButton.addEventListener('click', () => {
                copyInfoToClipboard(copyButton);
            });

            // Insert after the Easy Apply button if it exists, otherwise append
            const applyButton = buttonContainer.querySelector('.jobs-s-apply');
             if (applyButton && applyButton.nextSibling) {
                 buttonContainer.insertBefore(copyButton, applyButton.nextSibling);
             } else if (applyButton) {
                 buttonContainer.appendChild(copyButton); // Append if apply is last
             }
             else {
                // Fallback: just append if structure is unexpected
                 buttonContainer.appendChild(copyButton);
            }
            console.log("LinkedIn Job Info Copier: Button added successfully.");

        } else {
            console.warn("LinkedIn Job Info Copier: Button container not found. Cannot add button.");
        }
    }

    // --- Add custom styles for the button ---
    GM_addStyle(`
        #${BUTTON_ID} {
            /* Add any specific overrides if needed */
            cursor: pointer;
        }
        #${BUTTON_ID}.copied {
            background-color: #dff0d8 !important; /* Light green background */
            color: #3c763d !important; /* Darker green text */
            border-color: #d6e9c6 !important;
        }
    `);

    // --- Use MutationObserver to wait for the job details section to load ---
    // LinkedIn loads content dynamically, so we need to wait.
    const observerTargetSelector = '.job-view-layout.jobs-details'; // Main container for job view
    let observer = null;

    function startObserver() {
        const targetNode = document.querySelector(observerTargetSelector);
        if (!targetNode) {
            // If the main container isn't even there yet, wait a bit longer
             console.log("LinkedIn Job Info Copier: Waiting for main job view container...");
             setTimeout(startObserver, 500);
             return;
        }

        console.log("LinkedIn Job Info Copier: Observer started.");
        observer = new MutationObserver((mutationsList, obs) => {
            // Check if the button container is now available and the button isn't already added
            const buttonContainer = document.querySelector('.job-details-jobs-unified-top-card__container--two-pane .mt4 .display-flex');
             if (buttonContainer && !document.getElementById(BUTTON_ID)) {
                 console.log("LinkedIn Job Info Copier: Target container appeared, adding button.");
                 addCopyButton();
                 // Optional: Could disconnect observer here if the button container won't be removed/re-added
                 // obs.disconnect();
             }
             // Also check if the job description is loaded, sometimes it loads later
             const description = document.querySelector('#job-details .jobs-box__html-content');
              if (description && buttonContainer && !document.getElementById(BUTTON_ID)) {
                   console.log("LinkedIn Job Info Copier: Description appeared, ensuring button exists.");
                   addCopyButton(); // Try adding again just in case
             }
        });

        observer.observe(targetNode, {
            childList: true, // Watch for additions/removals of children
            subtree: true    // Watch descendants as well
        });

         // Initial attempt in case content is already present when script runs
         addCopyButton();
    }

    // --- Start the process ---
    // Use window.onload or setTimeout as a fallback if observer setup fails immediately
     if (document.readyState === 'complete') {
         startObserver();
     } else {
         window.addEventListener('load', startObserver);
     }
     // Extra fallback timeout
      setTimeout(addCopyButton, 3000); // Attempt to add after 3 seconds regardless


})();