Greasy Fork 还支持 简体中文。

NPM Favorites ❤

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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