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).

Verzia zo dňa 31.01.2025. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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