Pixiv QuickSelect

Allow selecting multiple items by holding shift

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Pixiv QuickSelect
// @version      0.2
// @description  Allow selecting multiple items by holding shift
// @author       8uurg
// @match        https://www.pixiv.net/*/users/*/bookmarks/*
// @grant        GM_addStyle
// @namespace https://greasyfork.org/users/368765
// ==/UserScript==

(function() {
    'use strict';

    // Insert css
    GM_addStyle(`
        label {
            user-select: none;
        }
        .betweenSelected {
            mask-image: none !important;
            box-shadow: 0px 0px 5px 2px lightblue;
        }
        section div:nth-child(3) {
            box-shadow: 15px 0px 0px 0px white, -15px 0px 0px 0px white;
        }
    `)

    // Due to this webpage being dynamic now, we may have to inject code later...
    function setupHandlers()
    {
    // Find the checkboxes and add logic.
    let bookmarksOrigin = document.querySelectorAll('input[type=checkbox]');
    if ( bookmarksOrigin.length == 0 ) {
        // If there are none, don't setup for now: wait a bit!
        return false;
    }
    let lastIndex = 0;
    let lastHovered = -1;
    let currentState = false;

    function getLabelGrandparent(checkbox)
    {
        return checkbox.parentElement.parentElement.parentElement;
    }

    function activateHoverEffect(from, to) {
        if(currentState) { return; }
        const tfrom = Math.min(from, to)
        const tto = Math.max(from, to)

        // console.log(`Activating from ${lastIndex} to ${lastHovered}`)
        for (let i=tfrom; i <= tto; i++) {
            getLabelGrandparent(bookmarksOrigin[i]).parentElement.classList.add('betweenSelected')
        }
        currentState = true;
    }
    function deactivateHoverEffect(from, to) {
        if(!currentState) { return; }
        const tfrom = Math.min(from, to)
        const tto = Math.max(from, to)

        // console.log(`Deactivating from ${lastIndex} to ${lastHovered}`)
        for (let i=tfrom; i <= tto; i++) {
            getLabelGrandparent(bookmarksOrigin[i]).parentElement.classList.remove('betweenSelected')
        }
        currentState = false;
    }
    function keyDownHandler(e) {
        if(lastHovered === -1) { return; }

        if(e.keyCode === 16) {
            activateHoverEffect(lastIndex, lastHovered)
        }
    }
    function keyUpHandler(e) {
        if(lastHovered === -1) { return; }

        if(e.keyCode === 16) {
            deactivateHoverEffect(lastIndex, lastHovered)
        }
    }
    document.addEventListener('keydown', keyDownHandler)
    document.addEventListener('keyup', keyUpHandler)

    for(let i=0; i < bookmarksOrigin.length; i++) {

        const checkbox = bookmarksOrigin[i];
        const bookmark = getLabelGrandparent(checkbox);

        const listedIndex = i;

        // We access the variables accessed outside the loop are done on purpose.
        // The actual element where this warning would be correct (i) is fixed in place using
        // a const binding within this scope!
        /* eslint-disable no-loop-func */
        function clickHandler(e) {
            // Avoid duplicating a click. This works because
            // the checkbox itself is hidden and will never be clicked directly!
            if (e.target instanceof HTMLInputElement) {
                // e.stopPropagation();
                return;
            }
            if(currentState && lastHovered !== -1) {
                deactivateHoverEffect(lastIndex, lastHovered);
            }
            // The checkbox itself gets checked afterwards, after this clickHandler has been performed.
            // Hence inverting the state is required.
            const newState = !checkbox.checked;
            const i = listedIndex;
            console.log(`Clicked ${e.target}, set to ${newState} and is index ${i}, is shift being held?: ${e.shiftKey}`);
            if(e.shiftKey) {
                const tfrom = Math.min(i, lastIndex)
                const tto = Math.max(i, lastIndex)
                // handling = tto - tfrom + 1;
                // console.log(`Setting from ${tfrom} to ${tto} to ${newState}`)
                for(let j=tfrom; j<=tto; j++) {
                    // The original approach with simply setting the checkbox state doesn't work anymore...
                    //  bookmarksOrigin[j].checked = newState
                    // Instead click the label when required... That should work.
                    if ( bookmarksOrigin[j].checked != newState ) {
                        const otherCheckbox = bookmarksOrigin[j];
                        const otherLabel = otherCheckbox.parentElement.parentElement.parentElement;
                        // Use a timeout...
                        setTimeout(function() {
                            otherCheckbox.click();
                        }, 0);
                    }
                }
            }
            lastIndex = i;
            if(currentState) {
                activateHoverEffect(lastIndex, lastHovered)
            }
        }
        function mouseEnterHandler(e) {
            const i = listedIndex;
            lastHovered = i;
            if (e.shiftKey) {
                activateHoverEffect(lastIndex, lastHovered)
            }
        }
        function mouseLeaveHandler(e) {
            deactivateHoverEffect(lastIndex, lastHovered)
            lastHovered = -1;
        }
        /* eslint-enable no-loop-func */

        bookmark.addEventListener('click', clickHandler);

        // For fancy hover when holding shift effect.
        // Disable for now.
        bookmark.addEventListener('mouseenter', mouseEnterHandler);
        bookmark.addEventListener('mouseleave', mouseLeaveHandler);
    }

    // Everything has been successfully set up!
    return true;
    // End of setupHandlers.
    }


    let intervalID = -1;

    function initialize()
    {
        // Try running it first (maybe the webpage is already in edit mode)
        const plainAttempt = setupHandlers();
        if (plainAttempt) {
            console.log("Successfully initialized eagerly.")
            return true;
        }

        // Pixiv has been updated and the webpage is now dynamic: we need to wait until checkboxes are available.
        // In order to do so, we place a click listener on the edit button.
        // We here use the oddity that the edit button is not actually a button element to locate its image.
        // And then go up until we find the actual button itself.
        const editButtonSVGElement = document.querySelector("section div>svg");
        if ( editButtonSVGElement === null ) return false;
        const editButtonElement = editButtonSVGElement.parentElement.parentElement.parentElement;
        editButtonElement.addEventListener('click', function(e) {
            window.setTimeout(function() {
                const success = setupHandlers();
                if (success) console.log("Successfully completely initialized!")
            }, 500);
        });
        console.log("Successfully initialized on edit button.")
        return true;
    }

    function intervalInit()
    {
        const success = initialize();
        if ( success )
        {
            window.clearInterval(intervalID);
        }
    }

    intervalID = window.setInterval(intervalInit, 1000);
})();