NPM Favorites ❤

Will allow you to easily organize and sort packages you might want to use in the future

// ==UserScript==
// @name         NPM Favorites ❤
// @namespace    http://tampermonkey.net/
// @version      2024-08-11
// @description  Will allow you to easily organize and sort packages you might want to use in the future
// @author       GV3Dev
// @match        https://www.npmjs.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=npmjs.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @require      http://code.jquery.com/jquery-latest.js
// @require      https://code.jquery.com/ui/1.12.1/jquery-ui.js
// @license MIT
// ==/UserScript==

let $ = window.jQuery;
var j = $.noConflict();

const main = async () => {
    if (location.href.includes("https://www.npmjs.com")) {
        setupPage(); monitorUrlChanges();
    }
}
main();


function setupPage() {
    const mainMenu = document.querySelector("#main-menu");
    if (!mainMenu) return;
    let bookmarksBtn = document.querySelector("#open-favorites-npm");
    if (!bookmarksBtn) {
        bookmarksBtn = document.createElement("li");
        bookmarksBtn.innerHTML = `<a style="cursor:pointer;" role="menuitem" class="c6c55db4 no-underline f6-ns f7 fw5 dim pr2 pl2" id="open-favorites-npm">Favorites</a>`;
        bookmarksBtn.className = "dib";
        bookmarksBtn.title = "view favorited packages ♥";
        mainMenu.append(bookmarksBtn);
        bookmarksBtn.addEventListener("click", openFavorites);
    }
    const heart = mainMenu.parentElement.parentElement.querySelector("span");
    if (location.href.includes("https://www.npmjs.com/package/")) {
        const packageName = location.href.split("/").pop();
        const savedPackage = GM_getValue(packageName);
        heart.style = "cursor:pointer; transition: .5s;";
        heart.title = savedPackage ? `Remove package from favorites` : `Add package to favorites`;
        heart.style.color = savedPackage ? "red" : "";
        heart.addEventListener("click", () => {
            toggleFavorite(packageName, heart);
        });
        if (savedPackage) {
            addHeartEmojiToHeader();
        }else{
            removeHeartEmojiFromHeader();
        }
    }else{
        heart.title = ""; heart.style = "";
    }
}

function openFavorites(evt) {
    evt.preventDefault();

    let menu = document.querySelector('#favorites-menu-npm');
    if (menu) {
        menu.style.display = menu.style.display === 'none' ? 'flex' : 'none';
        if (menu.style.display === 'flex') {
            const toBeFilled = document.querySelector("#fav-contain");
            populateFavorites(toBeFilled);
        }
    } else {
        menu = document.createElement('div');
        menu.id = 'favorites-menu-npm';
        menu.style.cssText = `
            position: fixed; top: 10px;
            right: 10px; width: 300px;
            min-height: 250px; max-height: 500px; background-color: #fff;
            border: 1px solid lightgray;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
            z-index: 1000; display: flex;
            justify-content:flex-start;align-items:center;
            flex-direction:column;border-radius:5px;
            padding:5px; font-family: 'Source Sans Pro', 'Lucida Grande', sans-serif;
        `;
        menu.innerHTML = `
            <h2 style="width:100%; text-align:center;margin-bottom:0; padding-bottom:0;">NPM Favorites <span style="color:red">❤</span></h2>
            <p style="text-align:center;width:95%;">Your favorite packages, within reach!</p>
            <div style="display:flex;justify-content:flex-start;align-items:center;flex-direction:row;background-color:rgba(0,0,0,0.04);padding:8px;padding-left:12px;border-radius:5px;width:90%;margin-top:5px;margin-bottom:10px;">
              <svg width="15px" height="15px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" aria-hidden="true"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g stroke="#777777" stroke-width="1.3"><g><path d="M13.4044,7.0274 C13.4044,10.5494 10.5494,13.4044 7.0274,13.4044 C3.5054,13.4044 0.6504,10.5494 0.6504,7.0274 C0.6504,3.5054 3.5054,0.6504 7.0274,0.6504 C10.5494,0.6504 13.4044,3.5054 13.4044,7.0274 Z"></path><path d="M11.4913,11.4913 L17.8683,17.8683"></path></g></g></g></svg>
              <input id="search-fav-npm" type="text" placeholder="Search favorites" style="outline:none;border:none;background-color:transparent; padding-left:10px; font-family: var(--code); font-size:12px;">
            </div>
            <div id="fav-contain" style="margin-top:5px; width:95%; height:fit-content; max-height:85%; overflow:hidden; overflow-y:auto; padding:5px; margin-bottom:10px;"></div>
            <p style="padding:0;margin:0;text-align:center;margin-top:15px;margin-bottom:20px;font-size:13px;opacity:0.85;">Brought to you by <a href="https://github.com/gv3dev" target="_blank" style="font-weight:bold; color:red; cursor:pointer; text-decoration:none;">GV3Dev</a<p>
        `;
        document.body.appendChild(menu);
        $(menu).draggable();
        injectScrollbarCSS();
        const searchBar = menu.querySelector("#search-fav-npm");
        const toBeFilled = document.querySelector("#fav-contain");
        populateFavorites(toBeFilled);
        searchBar.addEventListener("keyup", (evt)=>{handleSearch(evt, toBeFilled)})
    }
}


function handleSearch(evt, toBeFilled) {
    const searchQuery = evt.target.value.toLowerCase();
    const favorites = GM_listValues();
    if (searchQuery === '') {
        populateFavorites(toBeFilled);
    } else {
        const filteredFavorites = favorites.filter(packageName => {
            const packageData = GM_getValue(packageName);
            return packageName.toLowerCase().includes(searchQuery) ||
                (packageData.description && packageData.description.toLowerCase().includes(searchQuery));
        });
        populateFavorites(toBeFilled, filteredFavorites);
    }
}

function populateFavorites(menu, filteredFavorites = null) {
    menu.innerHTML = '';
    const favorites = filteredFavorites || GM_listValues();

    if (favorites.length > 0) {
        const sortedFavorites = favorites
        .map(packageName => {
            const packageData = GM_getValue(packageName);
            return {
                name: packageName,
                data: packageData
            };
        })
        .sort((a, b) => new Date(b.data.addedAt) - new Date(a.data.addedAt));

        sortedFavorites.forEach(({ name, data }) => {
            let item = document.createElement("div");
            item.style = `
                width: 100%;
                min-height: 50px;
                height: fit-content;
                border-bottom: 1px solid rgba(0,0,0,0.05);
                display: flex;
                align-items: flex-start;
                justify-content: center;
                flex-direction: column;
                padding: 5px;
                padding-bottom:8px;
                margin-top:5px;
                margin-bottom:5px;
                cursor:pointer;
                transition: background-color 0.3s ease;
            `;
            item.innerHTML = `
                <span style="width: 100%; display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 5px;">
                    <h2 style="margin: 0; font-size: 16px;">${name}</h2>
                    <span title="remove from favorites" style="cursor: pointer; transition: color 0.3s; font-size: 18px; padding: 2px 5px;" class="removeFavorite">&times;</span>
                </span>
                <p style="margin: 0; font-size: 14px; color: gray; text-align:left;">${data.description.length > 0 ? data.description : "For use in future projects."}</p>
            `;
            item.addEventListener('mouseover', () => {
                item.style.backgroundColor = '#f5f5f5';
            });

            item.addEventListener('mouseout', () => {
                item.style.backgroundColor = '';
            });
            item.addEventListener('click', () => {
                window.location.href = data.url;
            });

            item.querySelector('.removeFavorite').addEventListener('click', (e) => {
                e.stopPropagation();
                GM_deleteValue(name);
                populateFavorites(menu);
            });
            item.querySelector('.removeFavorite').addEventListener('mouseover', (e) => {
                e.target.style.color = 'red';
            });

            item.querySelector('.removeFavorite').addEventListener('mouseout', (e) => {
                e.target.style.color = '';
            });
            menu.append(item);
        });
    } else {
        menu.innerHTML = `<p style="text-align:center;">You have no favorites</p>`;
    }
}


function toggleFavorite(packageName, btn) {
    if (location.href.includes("https://www.npmjs.com/package/")) {
        const savedPackage = GM_getValue(packageName);

        if (savedPackage) {
            GM_deleteValue(packageName);
            btn.style = "color:; transition:.5s; cursor:pointer;";
            btn.title = `Add package to favorites`;
            removeHeartEmojiFromHeader();
        } else {
            const description = prompt("💡 Add a reminder\n\nWhat do you plan to use this package for? Describe it here to help you remember later!\n\nYou can leave empty if you would like.\n","For use in future projects.");
            if (description !== null) {
                GM_setValue(packageName, { url: location.href, description, addedAt: new Date().toISOString() });
                btn.style = "color:red; transition:.5s; cursor:pointer;";
                btn.title = `Remove package from favorites`;
                addHeartEmojiToHeader();
            }
        }
    }
}

function addHeartEmojiToHeader() {
    const header = document.querySelector("#top").firstChild.firstChild;
    if (header && !header.innerText.includes("😍")) {
        header.innerText += "😍";
        header.title = "You have favorited this package ♥";
    }
}

function removeHeartEmojiFromHeader() {
    const header = document.querySelector("#top").firstChild.firstChild;
    if (header) {
        header.innerText = header.innerText.replace("😍", "");
        header.title = "";
    }
}




function monitorUrlChanges() {
    let previousUrl = location.href;
    setInterval(() => {
        const currentUrl = location.href;
        if (currentUrl !== previousUrl) {
            previousUrl = currentUrl;
            setupPage();
        }
    }, 100);
}


// helper functions

function injectScrollbarCSS() {
    const css = `
            #fav-contain::-webkit-scrollbar {
                width: 5px;
            }
            #fav-contain::-webkit-scrollbar-track {
                background: #f1f1f1;
                border-radius: 10px;
            }
            #fav-contain::-webkit-scrollbar-thumb {
                background: rgba(0,0,0,0.2);
                border-radius: 10px;
            }
            #fav-contain::-webkit-scrollbar-thumb:hover {
                background:rgba(0,0,0,0.5);
            }
        `;
    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);
}

async function waitForElem(selector, all = false) {
    return new Promise((resolve) => {
        const checkElements = () => {
            const elements = all ? document.querySelectorAll(selector) : document.querySelector(selector);
            if (!all) {
                if (elements) {
                    resolve(elements);
                } else {
                    requestAnimationFrame(checkElements);
                }
            } else {
                if (elements.length > 0) {
                    resolve(elements);
                } else {
                    requestAnimationFrame(checkElements);
                }
            }
        };
        checkElements();
    });
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址