Youtube Save to... playlist incremental search

This script injects a search field into the dialog where user can save a video to a playlist. When the user starts to type an incremental search is implemented and the playlists are filtered out

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Youtube Save to... playlist incremental search
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  This script injects a search field into the dialog where user can save a video to a playlist. When the user starts to type an incremental search is implemented and the playlists are filtered out
// @author       Jaq Drako
// @match        *://www.youtube.com/*
// @grant        none
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// ==/UserScript==

(function () {
    'use strict';

    const $ = window.$;
    if (!$) {
        console.warn("[YT Playlist Filter] jQuery missing");
        return;
    }

    // returns jQuery-wrapped dropdown element if the "Save to..." sheet is open, else null
    function findSaveDialog() {
        const candidates = $("tp-yt-iron-dropdown:visible");
        for (let i = 0; i < candidates.length; i++) {
            const dlg = $(candidates[i]);
            if (dlg.find("yt-list-view-model.ytListViewModelHost[role='list']").length > 0) {
                return dlg;
            }
        }
        return null;
    }

    function swallowEventsPreventClose($el) {
        // block all the "this is an outside click" detectors higher up
        const stopper = function (e) {
            e.stopPropagation();
            e.stopImmediatePropagation();
        };

        // mousedown/up/click + focus just in case
        $el.on("mousedown click mouseup touchstart touchend", stopper);
    }

    function ensureSearchBox(dialogRoot) {
        const contentWrapper = dialogRoot.find("#contentWrapper").first();
        if (!contentWrapper.length) {
            return;
        }

        const headerContainer = contentWrapper.find(".ytContextualSheetLayoutHeaderContainer").first();
        if (!headerContainer.length) {
            return;
        }

        if (contentWrapper.find("#ytPlaylistSearchWrapper").length > 0) {
            return; // already injected
        }

        const searchHtml = [
            "<div id='ytPlaylistSearchWrapper'",
            "     style='box-sizing:border-box;padding:8px 16px 0 16px;display:flex;flex-direction:row;align-items:center;gap:8px;'>",
            "   <label for='ytPlaylistSearch'",
            "          style='font-size:12px;font-weight:500;white-space:nowrap;color:var(--yt-spec-text-primary,#fff);'>",
            "       Search:",
            "   </label>",
            "   <input id='ytPlaylistSearch' type='search'",
            "          placeholder='filter playlists...'",
            "          style='flex:1;font-size:12px;line-height:16px;padding:4px 6px;",
            "                 color:var(--yt-spec-text-primary,#fff);",
            "                 background-color:transparent;",
            "                 border:1px solid var(--yt-spec-text-secondary,#888);",
            "                 border-radius:4px;outline:none;'",
            "   />",
            "</div>"
        ].join("");

        const $injected = $(searchHtml).insertAfter(headerContainer);
        const input = $injected.find("#ytPlaylistSearch");

        // prevent dialog close on click/focus in our UI
        swallowEventsPreventClose($injected);
        swallowEventsPreventClose(input);

        // bind filtering
        input.on("input search", function () {
            filterPlaylists(dialogRoot);
        });
    }

    function filterPlaylists(dialogRoot) {
        const contentWrapper = dialogRoot.find("#contentWrapper").first();
        const termRaw = contentWrapper.find("#ytPlaylistSearch").val() || "";
        const term = termRaw.trim().toLowerCase();

        const rows = contentWrapper
            .find("yt-list-view-model.ytListViewModelHost[role='list']")
            .find("toggleable-list-item-view-model.toggleableListItemViewModelHost");

        rows.each(function () {
            const row = $(this);

            // try nice title span first
            const titleSpan = row.find(".yt-list-item-view-model__title").first();
            let name = "";

            if (titleSpan.length > 0) {
                name = (titleSpan.text() || "").trim().toLowerCase();
            } else {
                const item = row.find(".yt-list-item-view-model").first();
                name = (item.attr("aria-label") || "").trim().toLowerCase();
            }

            if (!term || name.indexOf(term) !== -1) {
                row.show();
            } else {
                row.hide();
            }
        });
    }

    function attachCloseHandler(dialogRoot) {
        if (dialogRoot.data("ytPlaylistFilterObserverAttached")) {
            return;
        }
        dialogRoot.data("ytPlaylistFilterObserverAttached", true);

        const observer = new MutationObserver(function () {
            if (!document.contains(dialogRoot[0])) {
                startPolling();
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    let pollHandle = null;

    function pollStep() {
        const dlg = findSaveDialog();
        if (!dlg) {
            return;
        }

        stopPolling();

        ensureSearchBox(dlg);
        filterPlaylists(dlg);
        attachCloseHandler(dlg);
    }

    function startPolling() {
        stopPolling();
        pollHandle = setInterval(pollStep, 200);
    }

    function stopPolling() {
        if (pollHandle) {
            clearInterval(pollHandle);
            pollHandle = null;
        }
    }

    startPolling();
})();