Bluesky Enhanced Layout

Customizes the Bluesky Home feed by creating a responsive three-column feed layout and relocating the menus—but only once the user is logged in (auto-detect).

目前為 2025-01-31 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bluesky Enhanced Layout
// @namespace    https://greasyfork.org/en/users/567951-stuart-saddler
// @version      1.3
// @description  Customizes the Bluesky Home feed by creating a responsive three-column feed layout and relocating the menus—but only once the user is logged in (auto-detect).
// @author       Stuart Saddler
// @icon         https://i.ibb.co/Vv9LhQv/bluesky-logo-png-seeklogo-520643.png
// @license      MIT
// @match        https://bsky.app/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    /**
     * Check if nav[role="navigation"] exists -> user is logged in.
     */
    function userIsLoggedIn() {
        return !!document.querySelector('nav[role="navigation"]');
    }

    /**
     * All your CSS injection.
     */
    function injectCSS() {
        const style = document.createElement('style');
        style.textContent = `
            /* 1. Align the Left Navigation Menu */
            nav[role="navigation"] {
                position: fixed !important;
                top: 0 !important;
                left: 0 !important;
                width: 200px !important;
                height: 100vh !important;
                background-color: rgb(22, 30, 39) !important;
                border-right: 1px solid rgb(46, 64, 82) !important;
                overflow-y: auto !important;
                z-index: 1000 !important;
                display: flex !important;
                flex-direction: column !important;
                padding: 20px 0 !important;
                box-sizing: border-box !important;
                -webkit-transform: translateZ(0);
                -moz-transform: translateZ(0);
                transform: translateZ(0);
                will-change: transform;
            }

            /* 4. Adjust the Feed Container */
            [data-testid="FeedPage-feed"],
            [data-testid="customFeedPage-feed"],
            [data-testid="followingFeedPage-feed"] {
                column-count: 3 !important;
                -webkit-column-count: 3 !important;
                -moz-column-count: 3 !important;
                column-gap: 20px !important;
                -webkit-column-gap: 20px !important;
                -moz-column-gap: 20px !important;
                width: calc(100vw - 200px) !important;
                margin-left: 200px !important;
                padding: 20px !important;
                box-sizing: border-box !important;
                display: block !important;
                overflow: visible !important;
            }

            /* 4a. Responsive Column Counts */
            @media (max-width: 1600px) {
                [data-testid="FeedPage-feed"],
                [data-testid="customFeedPage-feed"],
                [data-testid="followingFeedPage-feed"] {
                    column-count: 3 !important;
                }
            }

            @media (max-width: 1200px) {
                [data-testid="FeedPage-feed"],
                [data-testid="customFeedPage-feed"],
                [data-testid="followingFeedPage-feed"] {
                    column-count: 2 !important;
                }
                nav[role="navigation"] {
                    width: 150px !important;
                }
                [data-testid="FeedPage-feed"],
                [data-testid="customFeedPage-feed"],
                [data-testid="followingFeedPage-feed"] {
                    width: calc(100vw - 150px) !important;
                    margin-left: 150px !important;
                }
            }

            @media (max-width: 768px) {
                [data-testid="FeedPage-feed"],
                [data-testid="customFeedPage-feed"],
                [data-testid="followingFeedPage-feed"] {
                    column-count: 1 !important;
                }
                nav[role="navigation"] {
                    position: absolute !important;
                    width: 100% !important;
                    height: auto !important;
                    border-right: none !important;
                    border-bottom: 1px solid rgb(46, 64, 82) !important;
                    flex-direction: row !important;
                    flex-wrap: wrap !important;
                    justify-content: space-between !important;
                    padding: 10px !important;
                }
                [data-testid="FeedPage-feed"],
                [data-testid="customFeedPage-feed"],
                [data-testid="followingFeedPage-feed"] {
                    width: 100% !important;
                    margin-left: 0 !important;
                }
                nav[role="navigation"] > .css-175oi2r.r-1ipicw7.r-1xcajam.r-1rnoaur.r-pm9dpa.r-196lrry.css-175oi2r > .css-175oi2r {
                    width: auto !important;
                    margin-top: 0 !important;
                }
            }

            [data-testid="FeedPage-feed"],
            [data-testid="customFeedPage-feed"],
            [data-testid="followingFeedPage-feed"] {
                column-width: 300px !important;
            }

            /* 5. Style Individual Post Cards */
            .css-175oi2r.r-1habvwh {
                display: inline-block !important;
                width: 100% !important;
                margin: 0 0 20px !important;
                background: rgb(22, 30, 39) !important;
                border: 1px solid rgb(46, 64, 82) !important;
                border-radius: 8px !important;
                overflow: hidden !important;
                break-inside: avoid-column !important;
                page-break-inside: avoid !important;
                -webkit-column-break-inside: avoid !important;
                break-after: avoid-column !important;
                break-before: avoid-column !important;
                box-sizing: border-box !important;
                transition: all 0.2s ease-in-out !important;
                max-width: 100% !important;
                min-height: 150px !important;
            }

            /* 6. Hide Menu and Logo At the Top Of the Page */
            .r-2llsf.css-175oi2r > div.css-175oi2r:nth-of-type(1) > .css-175oi2r {
                display: none !important;
            }

            /* 7. Hide Background Comun Layout */
            .css-175oi2r[style*="position: fixed"][style*="inset: 0px 0px 0px 50%"] {
                position: static !important;
                inset: auto !important;
                transform: none !important;
                width: calc(100vw - 200px) !important;
                margin-left: 200px !important;
                border: none !important;
            }

            /* 8. Hide Feeds At the Top Of the Page */
            .css-175oi2r.r-18u37iz.r-1niwhzg.r-1e084wi {
                display: none !important;
            }
        `;
        document.head.appendChild(style);
    }

    function handleInteractions() {
        document.body.addEventListener('click', (e) => {
            const likeButton = e.target.closest('[data-testid*="like-button"]');
            if (likeButton && !likeButton.dataset.handled) {
                likeButton.dataset.handled = 'true';
            }
        }, { capture: true, passive: true });
    }

    function stabilizePosts() {
        const feed = document.querySelector(
          '[data-testid="FeedPage-feed"], [data-testid="customFeedPage-feed"], [data-testid="followingFeedPage-feed"]'
        );
        if (!feed) return;

        const posts = feed.querySelectorAll('.css-175oi2r.r-1habvwh');
        posts.forEach(post => {
            if (!post.dataset.stabilized) {
                post.classList.add('loading');
                requestAnimationFrame(() => {
                    post.classList.remove('loading');
                    post.dataset.stabilized = 'true';
                });
            }
        });
    }

    // Insert the "right menu" into the nav, applying custom style so it sticks.
    function moveRightMenuIntoNav() {
        const rightMenu = document.querySelector(
            'div[style*="padding: 20px 0px 20px 28px"]' +
            '[style*="position: fixed"]' +
            '[style*="left: 50%"]' +
            '[style*="width: 328px"]'
        );
        if (!rightMenu) return;

        const leftNav = document.querySelector('nav[role="navigation"]');
        if (!leftNav) return;

        // Force style changes so the new layout & spacing "stick"
        rightMenu.style.position = 'static';
        rightMenu.style.left = 'auto';
        rightMenu.style.top = 'auto';
        rightMenu.style.transform = 'none';
        rightMenu.style.width = 'auto';
        rightMenu.style.margin = '0';

        // Updated padding, gap, and scrolling:
        rightMenu.style.padding = '35px 15px 18px 15px';
        rightMenu.style.gap = '16px';
        rightMenu.style.maxHeight = '100%';
        rightMenu.style.overflowY = 'auto';

        // Finally, append under the nav items
        leftNav.appendChild(rightMenu);
    }

    // Set padding to 11px on the <div> with gap:10px etc.
    function adjustTopPadding() {
        const targetDiv = document.querySelector(
            'div.css-175oi2r[style*="gap: 10px;"][style*="padding-bottom: 2px;"][style*="overflow-y: auto"]'
        );
        if (targetDiv) {
            targetDiv.style.paddingTop = '11px';
        }
    }

    function setupMutationObserver() {
        const observerOptions = {
            childList: true,
            subtree: true,
        };
        let debounceTimeout;
        const observer = new MutationObserver(() => {
            clearTimeout(debounceTimeout);
            debounceTimeout = setTimeout(() => {
                stabilizePosts();
                moveRightMenuIntoNav();
                adjustTopPadding();
            }, 200);
        });

        observer.observe(document.body, observerOptions);
        return observer;
    }

    function setupCleanup(observer) {
        function cleanup() {
            observer.disconnect();
        }
        window.addEventListener('unload', cleanup);
    }

    /**
     * The main script (layout modifications) only runs once user is logged in.
     */
    function runLayoutScript() {
        injectCSS();
        moveRightMenuIntoNav();
        adjustTopPadding();
        handleInteractions();
        const observer = setupMutationObserver();
        setupCleanup(observer);
    }

    /**
     * 1) Check if user is already logged in on load. If yes, run right away.
     * 2) Otherwise, poll every 1 second to see if they've logged in. Then run once and stop.
     */
    function waitForLoginAndRun() {
        // If already logged in, run immediately
        if (userIsLoggedIn()) {
            runLayoutScript();
            return;
        }

        // Otherwise, set up a short polling interval
        const checkLoginInterval = setInterval(() => {
            if (userIsLoggedIn()) {
                clearInterval(checkLoginInterval);
                runLayoutScript();
            }
        }, 1000);
    }

    // We can wait until DOM content is loaded, then do our check
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', waitForLoginAndRun);
    } else {
        waitForLoginAndRun();
    }
})();