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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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