Greasy Fork 支持简体中文。

OkCupid better filtering

Adds filters to the discover feature

// ==UserScript==
// @name         OkCupid better filtering
// @namespace    http://tampermonkey.net/
// @version      20241108.3
// @description  Adds filters to the discover feature
// @author       You
// @match        https://www.okcupid.com/home
// @match        https://www.okcupid.com/discover
// @icon         https://www.google.com/s2/favicons?sz=64&domain=okcupid.com
// @grant        none
// @license      MIT
// ==/UserScript==

const dynamicStylePrefix = `css-${window.crypto.randomUUID()}`;

const locationIcon = `<svg width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39" /></svg>`;
const basicsIcon = `<svg width="24" height="24" viewBox="0 0 24 24"><path d="M17.75 12.843a5.454 5.454 0 1 1-.001 10.907 5.454 5.454 0 0 1 0-10.907zm0 1.5a3.953 3.953 0 1 0-.001 7.907 3.953 3.953 0 0 0 0-7.907zM10.431 8.55a.75.75 0 0 1 .75.75v8.885a.75.75 0 0 1-.75.75H1.547a.75.75 0 0 1-.75-.75V9.3a.75.75 0 0 1 .75-.75h8.885zm-.75 1.5H2.297v7.385h7.385V10.05zM11.42.275l9.91 2.656a.75.75 0 0 1 .336 1.255l-7.254 7.254a.75.75 0 0 1-1.255-.337l-2.655-9.91a.75.75 0 0 1 .918-.918zm.867 1.785l1.983 7.4 5.417-5.417-7.4-1.983z" fill="#1A1A1A" fill-rule="nonzero"></path></svg>`;
const looksIcon = `<svg width="24" height="24" viewBox="0 0 24 24"><path d="M12.03 7.104a3.552 3.552 0 1 1 3.552-3.543 3.543 3.543 0 0 1-3.552 3.543zm0-5.704a2.16 2.16 0 1 0 2.16 2.161 2.15 2.15 0 0 0-2.16-2.16z" fill="#191919"></path><path d="M14.376 24.01a2.059 2.059 0 0 1-1.984-1.743l-.334-2.57-.38 2.57a2.077 2.077 0 0 1-2.069 1.743 2.225 2.225 0 0 1-1.641-.705 2.207 2.207 0 0 1-.621-1.669l.62-6.946-.416.686a1.957 1.957 0 0 1-2.152.928 1.854 1.854 0 0 1-1.215-1.02 1.8 1.8 0 0 1 0-1.586l2.745-5.25a3.617 3.617 0 0 1 2.68-1.947l1.725-.25a4.48 4.48 0 0 1 1.317 0l1.734.25a3.617 3.617 0 0 1 2.7 1.948l2.726 5.212a1.855 1.855 0 0 1-1.178 2.606 1.957 1.957 0 0 1-2.152-.928l-.417-.686.621 6.918a2.27 2.27 0 0 1-2.262 2.402l-.047.037zm-2.365-9.162a.871.871 0 0 1 .872.77l.835 6.426a.668.668 0 0 0 .658.557.873.873 0 0 0 .64-.27.844.844 0 0 0 .232-.648l-.844-9.47a.788.788 0 0 1 1.465-.481l1.762 2.912a.566.566 0 0 0 .622.25.428.428 0 0 0 .278-.612L15.84 9.07a2.215 2.215 0 0 0-1.65-1.187l-1.725-.232a3.051 3.051 0 0 0-.928 0l-1.725.25A2.216 2.216 0 0 0 8.172 9.09L5.482 14.3a.427.427 0 0 0 .279.613.556.556 0 0 0 .621-.26l1.762-2.903a.788.788 0 0 1 1.465.482l-.853 9.497a.862.862 0 0 0 .533.822c.11.046.229.07.348.068a.668.668 0 0 0 .659-.566l.834-6.417a.927.927 0 0 1 .881-.788z" fill="#191919"></path></svg>`;
const backgroundIcon = `<svg width="24" height="24" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="M12.155.016L12 .015C18.627.015 24 5.38 24 12c0 6.566-5.287 11.899-11.841 11.984a.769.769 0 0 1-.317-.001l.158.002C5.373 23.985 0 18.619 0 12 0 5.433 5.289.1 11.844.016a.764.764 0 0 1 .311 0zM6.773 12.748l-5.247.001c.344 4.855 3.996 8.797 8.718 9.592a17.025 17.025 0 0 1-3.47-9.593zm15.7.001h-5.246a17.025 17.025 0 0 1-3.47 9.591c4.721-.794 8.373-4.736 8.717-9.591zm-6.748 0h-7.45A15.543 15.543 0 0 0 12 22.13a15.54 15.54 0 0 0 3.725-9.383zm-5.481-11.09l-.191.034c-4.63.867-8.188 4.768-8.527 9.557h5.247a17.027 17.027 0 0 1 3.47-9.591zm1.756.21l-.253.303a15.537 15.537 0 0 0-3.472 9.078h7.45a15.535 15.535 0 0 0-3.472-9.078L12 1.869zm1.755-.21l.137.18a17.022 17.022 0 0 1 3.335 9.411h5.247c-.344-4.855-3.996-8.798-8.719-9.591z" id="Icon/Background/24" fill="#1A1A1A" fill-rule="nonzero"></path></g></svg>`;
const lifestyleIcon = `<svg width="17" height="24" viewBox="0 0 17 24"><g fill="none" fill-rule="evenodd"><path d="M18.617.153c.305.232.383.65.196.973l-.053.078-.568.743h2.054a.75.75 0 0 1 .655 1.12l-.06.09-7.589 9.762V22.5h2.326c.416 0 .753.336.753.75 0 .385-.29.702-.665.745l-.088.005H9.422a.752.752 0 0 1-.753-.75c0-.385.29-.702.665-.745l.088-.005h2.324v-9.582L4.159 3.157a.75.75 0 0 1 .488-1.202l.107-.007L16.3 1.947 17.562.296a.755.755 0 0 1 1.055-.143zm.093 3.294h-1.664l-3.947 5.167a.755.755 0 0 1-1.055.143.748.748 0 0 1-.196-.973l.052-.078 3.253-4.259H6.289l6.21 7.99 6.211-7.99z" transform="translate(-4)" id="Icon/Lifestyle/24" fill="#1A1A1A" fill-rule="nonzero"></path></g></svg>`;
const familyIcon = `<svg width="24" height="20" viewBox="0 0 24 20"><g fill="none" fill-rule="evenodd"><path d="M17.976 2c.229 0 .445.107.587.29l5.274 6.802a.77.77 0 0 1 .164.455L24 9.57v11.66c0 .389-.282.71-.648.761L23.25 22H.75a.757.757 0 0 1-.743-.664L0 21.23V9.571l.001-.041a.79.79 0 0 1 .004-.044L0 9.571c0-.18.06-.344.16-.475l.003-.003 5.268-6.795a.737.737 0 0 1 .35-.258c.003 0 .007 0 .01-.002A.699.699 0 0 1 6.024 2l-.062.003A.736.736 0 0 1 6.014 2h.01zM6.024 4.005L1.5 9.84v10.62h2.063v-6.921c0-.39.282-.712.648-.763l.102-.007h3.375a.76.76 0 0 1 .75.77v6.921h2.111V9.838L6.024 4.005zM22.5 10.34H12.049v10.12H22.5V10.34zM6.938 14.308H5.063v6.152h1.875v-6.152zM20.3 12.769a.76.76 0 0 1 .75.77V17a.76.76 0 0 1-.75.77h-6a.76.76 0 0 1-.75-.77V13.54a.76.76 0 0 1 .75-.77zm-3.776 1.538H15.05v1.924l1.474-.001v-1.923zm3.026 0h-1.526v1.923h1.526v-1.922zm-1.937-10.77H7.578l4.083 5.264h10.033l-4.081-5.263z" transform="translate(0 -2)" id="Icon/Family/24" fill="#1A1A1A" fill-rule="nonzero"></path></g></svg>`;
const lookingIcon = `<svg width="24" height="22" viewBox="0 0 24 22"><g fill="none" fill-rule="evenodd"><path d="M11.994 6.415l.412.007c1.898.059 3.727.552 5.499 1.51.977.524 1.95 1.221 2.932 2.098.5.467.89.858 1.255 1.27l.428.496c.41.492.814 1.052 1.213 1.68a.75.75 0 0 1 0 .804 16.127 16.127 0 0 1-1.207 1.673l-.427.495c-.247.279-.502.545-.798.834l-.476.455c-.97.866-1.943 1.562-2.919 2.086-1.9 1.027-3.864 1.52-5.912 1.517-2.053-.015-4.012-.51-5.899-1.535-.976-.525-1.948-1.22-2.93-2.095a17.247 17.247 0 0 1-1.438-1.473 15.582 15.582 0 0 1-1.459-1.955.75.75 0 0 1 0-.808 15.51 15.51 0 0 1 1.45-1.945c.432-.505.873-.952 1.458-1.494.971-.864 1.943-1.56 2.917-2.083C7.982 6.927 9.94 6.43 11.994 6.415zm.01 1.5l-.387.01c-1.668.069-3.258.504-4.811 1.347-.868.466-1.746 1.094-2.621 1.874-.446.413-.8.765-1.137 1.14l-.41.473c-.28.335-.561.708-.84 1.119.35.514.701.97 1.06 1.373.388.454.788.86 1.315 1.35.887.788 1.765 1.416 2.635 1.884 1.671.907 3.388 1.342 5.193 1.355 1.794.002 3.513-.429 5.194-1.337.868-.467 1.745-1.095 2.618-1.873.47-.44.827-.797 1.168-1.182l.381-.441c.281-.338.562-.714.842-1.13-.28-.415-.562-.793-.848-1.135l-.41-.473a17.134 17.134 0 0 0-1.12-1.131c-.885-.79-1.763-1.419-2.633-1.886-1.559-.843-3.151-1.275-4.807-1.33l-.383-.007zM12 9.162a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9zm0 1.5a3 3 0 1 0 0 6 3 3 0 0 0 0-6zm10.088-7.307a.75.75 0 0 1 1.06 1.06L20.975 6.59a.75.75 0 0 1-1.06-1.06zm-21.236 0a.75.75 0 0 1 1.06 0l2.174 2.173a.75.75 0 0 1-1.061 1.06L.852 4.416a.75.75 0 0 1 0-1.06zM12 0a.75.75 0 0 1 .75.75v3.074a.75.75 0 1 1-1.5 0V.75A.75.75 0 0 1 12 0z" id="lookingfor-eye-Icon/Looking-For/24" fill="#1A1A1A" fill-rule="nonzero"></path></g></svg>`;
const profileIDTest = /profile\/(.+)\/questions/;
const skippedIds = [];

const currentProfile = {
    id: null,
    username: null,
    exclusions: [],
};

let debugMode = false;

let filters = {
    filterLocation: [],
    filterBasics: [],
    filterLooks: [],
    filterBackground: [],
    filterLifetsyle: [],
    filterFamily: [],
    filterLooking: [],
};

if (localStorage.getItem('filterList') !== null && localStorage.getItem('filterList') !== '') {
    filters = JSON.parse(localStorage.getItem('filterList'));
}

function updateStorage() {
    // Remove empty strings.
    Object.keys(filters).forEach((storageKey) => {
        filters[storageKey] = filters[storageKey].filter((term) => term !== '');
    });

    localStorage.setItem('filterList', JSON.stringify(filters));
}

const filtersSkeleton = [
    {
        icon: locationIcon,
        storageKey: 'filterLocation',
        title: 'Location',
        selector: 'div.card-content-header__location',
    },
    {
        icon: basicsIcon,
        storageKey: 'filterBasics',
        title: 'Basics',
        selector: 'div.matchprofile-details-section--basics div.matchprofile-details-text',
    },
    {
        icon: looksIcon,
        storageKey: 'filterLooks',
        title: 'Looks',
        selector: 'div.matchprofile-details-section--looks div.matchprofile-details-text',
    },
    {
        icon: backgroundIcon,
        storageKey: 'filterBackground',
        title: 'Background',
        selector: 'div.matchprofile-details-section--background div.matchprofile-details-text',
    },
        {
        icon: lifestyleIcon,
        storageKey: 'filterLifetsyle',
        title: 'Lifestyle',
        selector: 'div.matchprofile-details-section--lifestyle div.matchprofile-details-text',
    },
    {
        icon: familyIcon,
        storageKey: 'filterFamily',
        title: 'Family',
        selector: 'div.matchprofile-details-section--family div.matchprofile-details-text',
    },
    {
        icon: lookingIcon,
        storageKey: 'filterLooking',
        title: 'Loooking for',
        selector: 'div.matchprofile-details-section--wiw div.matchprofile-details-text',
    },
];

function buildFilterHTML() {
    const htmlArray = filtersSkeleton.map((filter) => {
        const listElements = filters[filter.storageKey].map((el) => {
            return `<li class="${dynamicStylePrefix}exclusion" data-key="${filter.storageKey}" data-value="${el}">${el}</li>`;
        }).join(`\n`);

        return `
          <div class="${dynamicStylePrefix}filter-item">
            <div class="${dynamicStylePrefix}filter-addition">
              <div title="${filter.title}">${filter.icon}</div>
              <div><input type="text" id="${dynamicStylePrefix}${filter.storageKey}" data-key="${filter.storageKey}" /></div>
            </div>
            <ul class="${dynamicStylePrefix}exclusion-list">
            ${listElements}
            </ul>
          </div>\n`;
    });

    return htmlArray.join(`\n`);
}

function toggleFeature() {
    const toggleElement = document.getElementById(`${dynamicStylePrefix}button-base-root`);
    const toggleTrack = document.getElementById(`${dynamicStylePrefix}track`);

    // Local storage saves only strings.
    const filtersEnabled = localStorage.getItem('filtersEnabled') === 'true';

    if (filtersEnabled) {
        // Turn off the filter.
        toggleElement.classList.remove('checked');
        toggleTrack.classList.remove('checked');
    } else {
        // Turn on the filter.
        toggleElement.classList.add('checked');
        toggleTrack.classList.add('checked');
    }

    console.debug(`[user script] Setting the filtersEnabled value to ${!filtersEnabled}.`);

    localStorage.setItem('filtersEnabled', !filtersEnabled);
}

function bindFilterListItem(el) {
    el.addEventListener('click', () => {
        const value = el.innerText;
        const storageKey = el.getAttribute('data-key');

        console.debug(`[user script] The user wants to remove this value. ${value}`);

        filters[storageKey] = filters[storageKey].filter((item) => item !== value);

        updateStorage();

        el.remove();
    });
}

function bindFilterItem(el) {
    const inputField = el.querySelector('input');
    const listElement = el.querySelector('ul');
    const storageKey = inputField.getAttribute('data-key');

    inputField.addEventListener('keyup', ({ target, key, keyCode }) => {
        const searchTerm = target.value.trim().toLowerCase();

        if (searchTerm !== '' && (key === 'Enter' || keyCode === 13) && !filters[storageKey].includes(searchTerm)) {
            const newListItem = document.createElement('li');

            newListItem.innerHTML = searchTerm;
            newListItem.setAttribute('data-key', storageKey);
            newListItem.setAttribute('data-value', searchTerm);

            bindFilterListItem(newListItem);

            listElement.insertAdjacentElement('beforeend', newListItem);

            inputField.value = null;

            filters[storageKey].push(searchTerm);

            updateStorage();
        }
    });
}

let profileLoop = null;

function getProfileID() {
    let matchButton = document.querySelector('[data-cy="discover.userCardMatchPercentage"]');

    if (matchButton) {
        const { href } = matchButton;
        const matches = href.match(profileIDTest);

        if (matches !== null) {
            return matches[1];
        }
    }

    return 'unknown';
};

function getProfileUsername() {
    const headerElement = document.querySelector('div.card-content-header__name-container h2');

    if (headerElement) {
        return headerElement.innerHTML;
    }

    return 'unknown';
}

function compileExclusions() {
    // Reset the found exclusions list.
    currentProfile.exclusions = [];

    filtersSkeleton.forEach((filter) => {
        // Are there any exlusions set for this category?
        if (filters[filter.storageKey].length === 0) {
            return false;
        }

        const detailElement = document.querySelector(filter.selector);

        if (!detailElement) {
            return false;
        }

        detailElement.innerText.split('|').forEach((detail) => {
            const profileDetail = detail.trim().toLowerCase();

            filters[filter.storageKey].forEach((filterTerm) => {
                const exclusionListItem = document.querySelector(`li[data-value="${filterTerm}"]`);

                exclusionListItem.classList.remove('found');

                if (filterTerm !== '' && (profileDetail.includes(filterTerm) || profileDetail === filterTerm)) {
                    exclusionListItem.classList.add('found');

                    currentProfile.exclusions.push(profileDetail);
                }
            });
        });
    });
}

let passing = false;

function pass() {
    const clickEvent = new Event('click');
    const blockButton = document.getElementById('user-script-block');
    const passButton = document.querySelector('button.dt-action-buttons-button.pass');
    const notificationElement = document.getElementById(`${dynamicStylePrefix}notification`);

    // The page has not yet loaded enough to have rendered the elements we need.
    if (!passButton || !notificationElement) {
        console.debug(`[user script] [${currentProfile.id}] Waiting for elements.`);

        setTimeout(() => pass, 100);
    }

    const { id } = currentProfile;

    if (blockButton) {
        console.debug(`[user script] [${currentProfile.id}] Blocking profile.`);

        blockButton.dispatchEvent(clickEvent);
    } else {
        console.debug(`[user script] [${currentProfile.id}] Passing on profile.`);

        passButton.dispatchEvent(clickEvent);
    }

    skippedIds.push(id);

    setTimeout(() => {
        // The profile still hasn't changed.
        if (id === currentProfile.id) {
            pass();
        }
    }, 2000);
}

function checkProfileLoop() {
    clearInterval(profileLoop);

    const notificationElement = document.getElementById(`${dynamicStylePrefix}notification`);
    const debugSpanID = document.getElementById(`${dynamicStylePrefix}debug-id`);
    const debugSpanUsername = document.getElementById(`${dynamicStylePrefix}debug-username`);
    const debugSpanExclusions = document.getElementById(`${dynamicStylePrefix}debug-exclusions`);

    profileLoop = setInterval(() => {
        const profileId = getProfileID();
        const username = getProfileUsername();

        compileExclusions();

        const hasExclusion = currentProfile.exclusions.length > 0;

        // The profile has changed.
        if (profileId !== currentProfile.id) {
            console.debug(`[user script] [${profileId}] Found a new profile.`);

            passing = false;

            if (hasExclusion) {
                console.debug(`[user script] [${profileId}] This profile contains ${currentProfile.exclusions.length} exclusions.`);
            }
        }

        // Update the current profile.
        currentProfile.id = profileId;
        currentProfile.username = username;

        debugSpanUsername.innerHTML = username;
        debugSpanID.innerHTML = profileId;
        debugSpanExclusions.innerHTML = !hasExclusion ? '' : currentProfile.exclusions.join('<br />');

        notificationElement.style.display = hasExclusion ? 'flex' : 'none';

        if (!skippedIds.includes(profileId) && hasExclusion) {
            if (localStorage.getItem('filtersEnabled') === 'true') {
                console.debug(`[user script] [${currentProfile.id}] Setting this profile to be passed.`);

                passing = true;
                pass();
            }
        }
    }, 500);
}

function initialize(detailsTopElement) {
    console.debug('[user script] Initializing the detail filter.');

    const debugContainer = document.createElement('div');

    debugContainer.classList.add(`${dynamicStylePrefix}hidden`);
    debugContainer.classList.add(`${dynamicStylePrefix}debug-container`);
    debugContainer.innerHTML = `<h3 class="dt-section-title">Debug</h3>
      <div class="dt-section-content">
        <div>
          <span>id</span>
          <span id="${dynamicStylePrefix}debug-id"></span>
        </div>
        <div>
          <span>username</span>
          <span id="${dynamicStylePrefix}debug-username"></span>
        </div>
        <div style="margin-top: 1em;">
          <span>exclusions</span>
          <br>
          <span id="${dynamicStylePrefix}debug-exclusions"></span>
        <div>
      </div>`;

    document.querySelector('body').insertAdjacentElement('beforeend', debugContainer);

    const enabled = localStorage.getItem('filtersEnabled') === 'true';
    const filterContainer = document.createElement('div');

    filterContainer.classList.add('dt-section');
    filterContainer.innerHTML = `
      <h3 class="dt-section-title">Filters</h3>
      <div class="dt-section-content">
        <div class="${dynamicStylePrefix}control-container">
          <div style="flex-grow: 1;">
            <span class="${dynamicStylePrefix}switch-root" id="${dynamicStylePrefix}switch-root">
              <span class="${dynamicStylePrefix}button-base-root ${enabled ? 'checked' : ''}" id="${dynamicStylePrefix}button-base-root">
                <input class="${dynamicStylePrefix}input-toggle" />
                <span class="${dynamicStylePrefix}thumb"></span>
                <span class="${dynamicStylePrefix}ripple"></span>
              </span>
              <span class="${dynamicStylePrefix}track ${enabled ? 'checked' : ''}" id="${dynamicStylePrefix}track"></span>
            </span>
            <span id="${dynamicStylePrefix}toggle-label">Automatically pass</span>
          </div>
          <div id="${dynamicStylePrefix}notification">This profile contains an exclusion.</div>
          <div class="${dynamicStylePrefix}icon-button" id="${dynamicStylePrefix}icon-button">
            <svg><path d="M10 18h4v-2h-4zM3 6v2h18V6zm3 7h12v-2H6z" /></svg>
          </div>
          <div class="${dynamicStylePrefix}icon-button" id="${dynamicStylePrefix}icon-button-debug">
            <svg viewBox="0 0 24 24"><path d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5s-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20zm-6 8h-4v-2h4zm0-4h-4v-2h4z"></path></svg>
          </div>
        </div>
        <div class="${dynamicStylePrefix}filter-container" id="${dynamicStylePrefix}filter-container">
          <p>
            Profiles containing any of these exclusions will be passed. All exclusions are case-insensitive.
            If you are also using the <a href="https://greasyfork.org/en/scripts/504158-okcupid-more-effective-pass">more effective pass</a>
            script then the profiles will instead be blocked.
          </p>
          <div class="${dynamicStylePrefix}filter-list">
            ${buildFilterHTML()}
          </div>
        </div>
      </div>
    `;

    detailsTopElement.insertAdjacentElement('afterend', filterContainer);

    const toggleElement = document.getElementById(`${dynamicStylePrefix}switch-root`);

    toggleElement.addEventListener('click', toggleFeature);

    const showFiltersButton = document.getElementById(`${dynamicStylePrefix}icon-button`);
    const showDebugButton = document.getElementById(`${dynamicStylePrefix}icon-button-debug`);
    const filterContainerElement = document.getElementById(`${dynamicStylePrefix}filter-container`);

    showFiltersButton.addEventListener('click', () => {
        const display = filterContainerElement.style.display === 'none' || filterContainerElement.style.display === '' ? 'flex' : 'none';

        filterContainerElement.style.display = display;
    });

    showDebugButton.addEventListener('click', () => {
        debugContainer.classList.toggle(`${dynamicStylePrefix}hidden`);
    });

    [...document.querySelectorAll(`div.${dynamicStylePrefix}filter-item`)].forEach(bindFilterItem);

    // Bind the existing list items that were loaded from local storage.
    [...document.querySelectorAll(`ul.${dynamicStylePrefix}exclusion-list li`)].forEach(bindFilterListItem);

    checkProfileLoop();
}

const styleString = `
  div.${dynamicStylePrefix}control-container {
    display: flex;
    alignItems: center;
  }

  span.${dynamicStylePrefix}switch-root {
    display: inline-flex;
    width: 58px;
    height: 38px;
    overflow: hidden;
    padding: 12px;
    box-sizing: border-box;
    position: relative;
    flex-shrink: 0;
    z-index: 0;
    vertical-align: middle;
  }

  span.${dynamicStylePrefix}button-base-root {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    background-color: transparent;
    outline: 0px;
    border: 0px;
    margin: 0px;
    cursor: pointer;
    user-select: none;
    vertical-align: middle;
    appearance: none;
    text-decoration: none;
    padding: 9px;
    border-radius: 50%;
    position: absolute;
    top: 0px;
    left: 0px;
    z-index: 1;
    color: rgb(255, 255, 255);
    transition: left 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
  }

  span.${dynamicStylePrefix}button-base-root.checked {
    color: rgb(25, 118, 210);
    transform: translateX(20px);
  }

  span.${dynamicStylePrefix}button-base-root:hover {
    background-color: rgba(0, 0, 0, 0.04);
  }

  input.${dynamicStylePrefix}input-toggle {
    left: -100%;
    width: 300%;
    cursor: inherit;
    position: absolute;
    opacity: 0;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    margin: 0;
    padding: 0;
    z-index: 1;
  }

  span.${dynamicStylePrefix}thumb {
    box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 1px -1px, rgba(0, 0, 0, 0.14) 0px 1px 1px 0px, rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
    background-color: currentcolor;
    width: 20px;
    height: 20px;
    border-radius: 50%;
  }

  span.${dynamicStylePrefix}ripple {
    overflow: hidden;
    pointer-events: none;
    position: absolute;
    z-index: 0;
    inset: 0px;
    border-radius: inherit;
  }

  span.${dynamicStylePrefix}track {
    height: 100%;
    width: 100%;
    border-radius: 7px;
    z-index: -1;
    transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1), background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
    background-color: rgb(0, 0, 0);
    opacity: 0.38;
  }

  span.${dynamicStylePrefix}track.checked {
    background-color: rgb(25, 118, 210);
  }

  div.${dynamicStylePrefix}icon-button {
    width: 32px;
    height: 32px;
    border: 1px solid silver;
    cursor: pointer;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  div.${dynamicStylePrefix}icon-button:hover {
    background-color: rgb(246 246 246);
  }

  div.${dynamicStylePrefix}icon-button:hover svg {
    fill: rgb(25, 118, 210);
  }

  div.${dynamicStylePrefix}icon-button svg {
    width: 24px;
    height: 24px;
  }

  div.${dynamicStylePrefix}filter-container {
    display: none;
    flex-direction: column;
    gap: 1em;
    border-top: 1px solid black;
    padding: 1em;
    margin-top: 1em;
  }

  div.${dynamicStylePrefix}filter-list {
    display: flex;
    flex-basis: auto;
    justify-content: center;
    gap: 1em;
    flex-wrap: wrap;
  }

  div.${dynamicStylePrefix}filter-item {
    display: flex;
    flex-direction: column;
    gap: 0.5em;
    border: 1px solid grey;
    border-radius: 4px;
    width: 260px;
    height: 200px;
    padding: 3px;
  }

  div.${dynamicStylePrefix}filter-addition {
    display: flex;
    gap: 2px;
    align-items: center;
    border-bottom: 1px dashed black;
  }

  div.${dynamicStylePrefix}filter-addition svg {
    width: 32px;
    height: 32px;
  }

  div.${dynamicStylePrefix}filter-addition input {
    width: 100%;
  }

  ul.${dynamicStylePrefix}exclusion-list {
    list-style: none;
    overflow: scroll;
  }

  ul.${dynamicStylePrefix}exclusion-list li {
    cursor: pointer;
  }

  ul.${dynamicStylePrefix}exclusion-list li:hover {
    text-decoration: line-through;
    background-color: rgb(246 246 246);
  }

  div#${dynamicStylePrefix}notification {
    display: none;
    flex-grow: 1;
    font-style: italic;
    color: red;
  }

  li.${dynamicStylePrefix}exclusion.found {
    color: red;
  }

  .${dynamicStylePrefix}hidden {
    display: none !important;
  }

  div.${dynamicStylePrefix}debug-container {
    display: flex;
    flex-direction: column;
    position: fixed;
    bottom: 1em;
    left: 1em;
    width: 300px;
  }

  div.${dynamicStylePrefix}debug-container .dt-section-content {
    background-color: rgb(255 255 255 / 70%);
    display: flex;
    flex-direction: column;
  }

  div#${dynamicStylePrefix}icon-button-debug {
    margin-left: 1em;
  }
`;

(function() {
    'use strict';

    const sheet = new CSSStyleSheet();

    sheet.replaceSync(styleString);

    console.debug(`[user scripts] Adding ${sheet.rules.length} new CSS rules.`);

    document.adoptedStyleSheets.push(sheet);

    let detailsTopElement = null;

    setInterval(() => {
        if (detailsTopElement === null || !document.body.contains(detailsTopElement)) {
            detailsTopElement = document.querySelector('div.desktop-dt-top');

            // The details container now exists. Create our custom elements.
            if (detailsTopElement !== null) {
                initialize(detailsTopElement);
            }
        }
    }, 100);
})();