Bluesky Media Only Toggle

Toggleable filter to show only those Bluesky posts containing media

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bluesky Media Only Toggle
// @description  Toggleable filter to show only those Bluesky posts containing media
// @author       @plonked.bsky.social
// @match       *://bsky.app/*
// @namespace    plonked
// @version      1.0.0
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    const FILTER_BUTTON_ID = 'bsky-media-filter-btn';
    const HIDDEN_CLASS = 'bsky-hidden-post';
    const HAS_MEDIA_CLASS = 'bsky-has-media';
    const STORAGE_KEY = 'bsky-media-filter-active';

    const POST_SELECTORS = {
        feedItem: '[data-testid^="feedItem-by-"]',
        postPage: '[data-testid^="postThreadItem-by-"]',
        searchItem: 'div[role="link"][tabindex="0"]'
    };

    const styles = `
        .${HIDDEN_CLASS} {
            display: none !important;
        }
    `;

    let isFilterActive = GM_getValue(STORAGE_KEY, false);

    const findVideoInPost = (post) => {
        return post.querySelector('video[poster^="https://video.bsky.app/"], video[src^="https://t.gifs.bsky.app/"]');
    };

    const findImagesInPost = (post) => {
        return Array.from(post.querySelectorAll('img[src^="https://cdn.bsky.app/img/feed_thumbnail/"]'));
    };

    const hasMedia = (post) => {
        if (post.classList.contains(HAS_MEDIA_CLASS)) {
            return true;
        }

        const video = findVideoInPost(post);
        const images = findImagesInPost(post);
        const containsMedia = video || images.length > 0;

        if (containsMedia) {
            post.classList.add(HAS_MEDIA_CLASS);
        }

        return containsMedia;
    };

    const processPost = (post) => {
        if (post.classList.contains(HAS_MEDIA_CLASS)) {
            post.classList.toggle(HIDDEN_CLASS, false);
            return;
        }

        const containsMedia = hasMedia(post);

        if (containsMedia) {
            post.classList.toggle(HIDDEN_CLASS, false);
            return;
        }

        post.classList.toggle(HIDDEN_CLASS, isFilterActive);
    };

    const toggleFilter = () => {
        isFilterActive = !isFilterActive;
        GM_setValue(STORAGE_KEY, isFilterActive);

        const button = document.querySelector(`#${FILTER_BUTTON_ID}`);
        const text = button.querySelector('.filter-text');
        text.textContent = isFilterActive ? 'All Posts' : 'Media Only';

        document.querySelectorAll(Object.values(POST_SELECTORS).join(',')).forEach(post => {
            if (post.classList.contains(HAS_MEDIA_CLASS)) {
                post.classList.remove(HIDDEN_CLASS);
            } else {
                post.classList.toggle(HIDDEN_CLASS, isFilterActive);
            }
        });
    };

    const createFilterButton = () => {
        const navMenu = document.querySelector('nav[role="navigation"]');
        if (!navMenu) return;

        const settingsLink = Array.from(navMenu.querySelectorAll('a')).find(a => a.getAttribute('aria-label') === 'Settings');
        if (!settingsLink) return;

        const button = document.createElement('a');
        button.id = FILTER_BUTTON_ID;
        button.className = 'css-175oi2r r-1loqt21 r-1otgn73';
        button.role = 'button';
        button.style.cssText = `
            flex-direction: row;
            align-items: center;
            padding: 12px;
            border-radius: 8px;
            gap: 8px;
            outline-offset: -1px;
            transition-property: color, background-color;
            transition-timing-function: cubic-bezier(0.17, 0.73, 0.14, 1);
            transition-duration: 100ms;
            cursor: pointer;
        `;

        const iconContainer = document.createElement('div');
        iconContainer.className = 'css-175oi2r';
        iconContainer.style.cssText = 'align-items: center; justify-content: center; z-index: 10; width: 24px; height: 24px;';

        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute('fill', 'none');
        svg.setAttribute('width', '28');
        svg.setAttribute('height', '28');
        svg.setAttribute('viewBox', '0 0 24 24');
        svg.style.color = 'rgb(241, 243, 245)';

        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute('fill', 'hsl(211, 20%, 95.3%)');
        path.setAttribute('d', 'M2 6a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6zm2 0v4h4V6H4zm10-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zm0 2v4h4V6h-4zM2 16a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4zm2 0v4h4v-4H4zm10-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm0 2v4h4v-4h-4z');

        svg.appendChild(path);
        iconContainer.appendChild(svg);

        const text = document.createElement('div');
        text.className = 'css-146c3p1 filter-text';
        text.style.cssText = `
            font-size: 18.75px;
            letter-spacing: 0px;
            color: rgb(241, 243, 245);
            font-weight: 400;
            line-height: 18.75px;
            font-family: InterVariable, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica;
        `;
        text.textContent = isFilterActive ? 'All Posts' : 'Media Only';

        button.appendChild(iconContainer);
        button.appendChild(text);
        button.addEventListener('click', toggleFilter);

        settingsLink.parentNode.insertBefore(button, settingsLink.nextSibling);
    };

    const addStyles = () => {
        const styleSheet = document.createElement('style');
        styleSheet.textContent = styles;
        document.head.appendChild(styleSheet);
    };

    const initialize = () => {
        addStyles();
        createFilterButton();

        const observer = new MutationObserver((mutations) => {
            if (mutations.some(mutation => mutation.addedNodes.length)) {
                document.querySelectorAll(Object.values(POST_SELECTORS).join(',')).forEach(post => {
                    if (!post.classList.contains(HAS_MEDIA_CLASS)) {
                        processPost(post);
                    }
                });
            }

            if (!document.querySelector(`#${FILTER_BUTTON_ID}`)) {
                createFilterButton();
            }
        });

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

        document.querySelectorAll(Object.values(POST_SELECTORS).join(',')).forEach(processPost);
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();