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).
目前為
// ==UserScript==
// @name Bluesky Enhanced Layout
// @namespace https://greasyfork.org/en/users/567951-stuart-saddler
// @version 1.5
// @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"]');
}
// Prevent the script from executing on profile or notifications pages.
if (
window.location.pathname.startsWith('/profile/') ||
window.location.pathname.startsWith('/notifications')
) {
return;
}
/**
* 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;
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;
}
}
/* 5. Style Individual Post Cards */
.css-175oi2r.r-1habvwh {
display: block !important; /* Ensure posts are block elements */
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;
max-height: 500px !important; /* Prevent excessively tall posts */
overflow-y: auto !important; /* Add scrollbars if content exceeds max-height */
}
/* 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) {
// Reapply styles to ensure proper column behavior
post.style.display = 'block';
post.style.breakInside = 'avoid-column';
post.style.pageBreakInside = 'avoid';
post.style.webkitColumnBreakInside = 'avoid';
post.style.breakAfter = 'avoid-column';
post.style.breakBefore = 'avoid-column';
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 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();
}
})();