Optimize Douban layout for widescreen displays, utilizing full screen width with modern responsive design
// ==UserScript==
// @name Douban Widescreen Optimizer
// @name:zh-CN 豆瓣宽屏优化
// @namespace https://github.com/better-douban
// @version 1.0.9
// @description Optimize Douban layout for widescreen displays, utilizing full screen width with modern responsive design
// @description:zh-CN 优化豆瓣网页在宽屏显示器下的布局,充分利用屏幕宽度,采用现代响应式设计
// @author Better Douban Team
// @match *://*.douban.com/*
// @match *://douban.com/*
// @icon https://www.douban.com/favicon.ico
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Configuration for different page types
const config = {
// Global settings
global: {
maxWidth: '95vw', // Use 95% of viewport width
contentMaxWidth: '1800px', // Absolute max to prevent excessive stretching on ultra-wide
padding: '20px',
gap: '24px'
},
// Movie/Book/Music detail pages
subject: {
containerWidth: '90vw',
maxContentWidth: '1600px',
sidebarRatio: '320px', // Fixed sidebar width
gridColumns: 'minmax(300px, 1fr)'
},
// List pages (movie list, book list, etc.)
list: {
containerWidth: '92vw',
maxContentWidth: '1800px',
gridMinSize: '280px',
gridGap: '20px'
},
// Group discussion pages
group: {
containerWidth: '90vw',
maxContentWidth: '1600px'
},
// User profile pages
user: {
containerWidth: '90vw',
maxContentWidth: '1600px'
}
};
// Detect current page type
function getPageType() {
const path = window.location.pathname;
const hostname = window.location.hostname;
// Check for movie photos/images pages first (only on movie domain)
// Matches: /subject/{id}/photos, /subject/{id}/all_photos, /subject/{id}/photos?type=S
if (hostname.includes('movie') && path.match(/^\/subject\/\d+\/(all_)?photos/)) return 'photos';
if (path.match(/^\/subject\//)) return 'subject';
if (path.match(/^\/group/)) return 'group';
if (path.match(/^\/people/)) return 'user';
if (path.match(/\/list/)) return 'list';
if (hostname.includes('movie')) return 'movie';
if (hostname.includes('book')) return 'book';
if (hostname.includes('music')) return 'music';
return 'global';
}
// Create optimized CSS styles
function generateStyles(pageType) {
const global = config.global;
const pageConfig = config[pageType] || config.global;
return `
/* ============================================
Douban Widescreen Optimizer - Base Styles
Version: 1.0.9 (Pixel-Based Subject Summary Expansion)
Key changes in v1.0.9:
- Force summary width from the measured article width in pixels
- Watch style/class changes that collapse summary text late
- Reapply summary layout for several seconds after load
Key changes in v1.0.8:
- Append optimizer stylesheet after page styles instead of before them
- Apply inline important widths to subject summary text nodes
- Reapply summary layout after late DOM updates from other scripts
Key changes in v1.0.7:
- Expanded subject summary text nodes to fill the widened article
- Added explicit width rules for .related-info summary internals
Key changes in v1.0.6:
- Rebuilt subject header as a responsive three-column grid
- Enlarged posters on detail pages while preserving aspect ratio
- Reduced empty middle space between poster, metadata, and rating
Key changes in v1.0.5:
- Added optimization for movie photo gallery pages (/subject/{id}/photos)
- Responsive grid layout for photos with auto-fill
- Hover effects and caption overlays
- Configurable grid size (180px min) and gap (12px)
Key changes in v1.0.4:
- Fixed .aside dropping to bottom on wide screens
- Use flexbox on .grid-16-8.clearfix for robust two-column layout
- .article uses flex: 1 to take remaining space
- .aside uses flex: 0 0 320px to maintain fixed width
- Prevents sidebar from wrapping regardless of screen width
Key changes in v1.0.3:
- Fixed .aside sidebar positioning for compatibility with scripts1.json and scripts2.json
- Float-based layout for maximum compatibility
Key changes in v1.0.2:
- Move rating section to right side of detail pages
- Float-based positioning for rating box
Key changes in v1.0.1:
- More specific CSS selectors to avoid breaking layout
- Only target top-level containers (#wrapper, #content)
============================================ */
/* Only target top-level containers - be very specific */
#wrapper {
max-width: ${pageConfig.maxContentWidth || global.contentMaxWidth} !important;
width: ${pageConfig.containerWidth || global.maxWidth} !important;
min-width: 1200px !important;
}
#content {
max-width: 100% !important;
width: 100% !important;
}
/* Grid layout container - only the main one */
#content .grid-16-8.clearfix,
.grid-16-8.clearfix {
max-width: 100% !important;
width: 100% !important;
display: grid !important;
grid-template-columns: minmax(0, 1fr) 320px !important;
gap: 24px !important;
align-items: flex-start !important;
}
/* Main article column in grid-16-8 layout */
#content .grid-16-8.clearfix > .article,
.grid-16-8.clearfix > .article {
width: 100% !important;
min-width: 0 !important;
max-width: none !important;
box-sizing: border-box !important;
}
/* Sidebar in grid-16-8 layout - fixed width, never shrinks */
#content .grid-16-8.clearfix > .aside,
.grid-16-8.clearfix > .aside {
width: 320px !important;
min-width: 320px !important;
max-width: 320px !important;
}
/* ============================================
Subject Detail Page Layout Optimization
Move rating section to right sidebar area
IMPORTANT: Use float-based layout for maximum compatibility
with other Douban enhancement scripts (scripts1.json, scripts2.json)
============================================ */
/* Subject header: poster + metadata + rating.
On movie pages Douban wraps #mainpic and #info inside
.subject.clearfix, while #interest_sectl is a sibling inside
.subjectwrap.clearfix. Target the real wrapper so all three
regions can share one row. */
#content .article .subjectwrap.clearfix {
display: grid !important;
grid-template-columns: minmax(210px, 260px) minmax(420px, 1fr) 300px !important;
gap: 28px !important;
align-items: start !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
#content .article .subjectwrap.clearfix > .subject.clearfix {
grid-column: 1 / 3 !important;
display: grid !important;
grid-template-columns: minmax(210px, 260px) minmax(420px, 1fr) !important;
gap: 28px !important;
align-items: start !important;
float: none !important;
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
}
/* Poster image container */
#content .article .subjectwrap.clearfix > .subject.clearfix > #mainpic {
float: none !important;
margin: 0 !important;
width: 100% !important;
max-width: 260px !important;
}
#content .article .subjectwrap.clearfix > .subject.clearfix > #mainpic img {
display: block !important;
width: 100% !important;
max-width: 260px !important;
height: auto !important;
}
/* Info section (title, director, cast, etc.) */
#content .article .subjectwrap.clearfix > .subject.clearfix > #info,
#content .article .subjectwrap.clearfix > .subject.clearfix > .subject-info {
float: none !important;
min-width: 0 !important;
width: auto !important;
max-width: none !important;
overflow: visible !important;
line-height: 1.7 !important;
}
/* Rating section - keep it as a stable right column in the subject header */
#content .article .subjectwrap.clearfix > #interest_sectl {
grid-column: 3 !important;
float: none !important;
clear: none !important;
margin: 0 !important;
width: 300px !important;
min-width: 300px !important;
max-width: 300px !important;
box-sizing: border-box !important;
}
/* Ensure content below clears floats properly */
.article > .indent,
.article > div.indent,
.related-info {
clear: both !important;
padding-top: 20px !important;
}
#content .grid-16-8.clearfix > .article > .indent {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
/* Summary text must expand with the widened article, not keep
Douban's original narrow prose measure inside the full section. */
#content .grid-16-8.clearfix > .article .related-info,
#content .grid-16-8.clearfix > .article .related-info .indent,
#content .grid-16-8.clearfix > .article .related-info #link-report-intra,
#content .grid-16-8.clearfix > .article .related-info .short,
#content .grid-16-8.clearfix > .article .related-info .all,
#content .grid-16-8.clearfix > .article .related-info span[property="v:summary"] {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
#content .grid-16-8.clearfix > .article .related-info .short,
#content .grid-16-8.clearfix > .article .related-info .all:not(.hidden),
#content .grid-16-8.clearfix > .article .related-info span[property="v:summary"] {
display: block !important;
line-height: 1.75 !important;
}
#content .grid-16-8.clearfix > .article .related-info .all.hidden {
display: none !important;
}
/* ============================================
Photo Gallery Page Optimization
For movie domain only: /subject/{id}/photos, /subject/{id}/all_photos
Supports:
- /subject/{id}/photos
- /subject/{id}/all_photos
- /subject/{id}/photos?type=S (stills)
- /subject/{id}/photos?type=W (wallpapers)
- /subject/{id}/photos?type=P (posters)
Actual structure (verified via browser):
- <ul class="poster-col3 clearfix"> contains photo items
- Parent is <article> element
============================================ */
/* Photo gallery container - expand to full width (movie photos pages only) */
article > ul.poster-col3 {
max-width: 100% !important;
width: 100% !important;
}
/* Photo grid - responsive masonry layout */
ul.poster-col3 {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(${pageConfig.gridMinSize || '180px'}, 1fr)) !important;
gap: ${pageConfig.gridGap || '12px'} !important;
width: 100% !important;
padding: 0 !important;
margin: 0 !important;
list-style: none !important;
}
/* Individual photo items */
ul.poster-col3 li {
position: relative !important;
overflow: hidden !important;
border-radius: 4px !important;
background-color: #f5f5f5 !important;
}
/* Photo images - maintain aspect ratio */
ul.poster-col3 img {
width: 100% !important;
height: auto !important;
display: block !important;
object-fit: cover !important;
transition: transform 0.3s ease !important;
}
/* Hover effect for photos */
ul.poster-col3 li:hover img {
transform: scale(1.05) !important;
}
/* Photo pagination controls */
.paginator {
margin-top: 24px !important;
text-align: center !important;
}
/* Grid layout improvements for list pages - be specific */
ul.grid-view,
.subject-list ul {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(${pageConfig.gridMinSize || '280px'}, 1fr)) !important;
gap: ${pageConfig.gridGap || global.gap} !important;
width: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
/* Card/item styling for better visual hierarchy */
.subject-item,
li.item,
div.item,
.card,
.info-wrapper {
transition: transform 0.2s ease, box-shadow 0.2s ease !important;
}
.subject-item:hover,
li.item:hover,
div.item:hover {
transform: translateY(-2px) !important;
}
/* Image optimization for high-DPI displays */
img[src*="photo"],
img[class*="cover"],
.poster img {
object-fit: cover !important;
width: 100% !important;
height: auto !important;
}
/* Text readability - only for main content paragraphs */
.article p,
.article-content p {
font-size: 15px !important;
line-height: 1.7 !important;
}
/* Header navigation optimization */
#db-global-nav,
#header,
nav {
position: sticky !important;
top: 0 !important;
z-index: 1000 !important;
backdrop-filter: blur(8px) !important;
}
/* Table responsiveness */
table {
width: 100% !important;
overflow-x: auto !important;
display: block !important;
}
/* Form elements scaling */
input[type="text"],
input[type="search"],
textarea,
select {
max-width: 100% !important;
font-size: 14px !important;
}
/* Modal and overlay centering */
.modal,
.dialog,
div[class*="modal"],
div[class*="dialog"] {
max-width: 90vw !important;
margin: auto !important;
}
/* Video player container */
.video-player,
iframe[src*="video"],
video {
width: 100% !important;
max-width: 100% !important;
aspect-ratio: 16 / 9 !important;
}
/* Comment section - don't force width changes */
.review-list,
.comments-list {
width: 100% !important;
}
/* Rating and score display */
.rating,
.score,
[class*="rating"],
[class*="score"] {
font-size: 16px !important;
}
/* Tag clouds and categories */
.tags,
[class*="tag"] {
display: flex !important;
flex-wrap: wrap !important;
gap: 8px !important;
}
/* Footer spacing */
footer,
#footer,
div[id*="footer"] {
padding: 40px 20px !important;
text-align: center !important;
}
/* Responsive breakpoints - conservative approach */
@media (max-width: 1200px) {
#wrapper {
width: 95vw !important;
max-width: 1200px !important;
min-width: auto !important;
}
#content .article .subjectwrap.clearfix {
grid-template-columns: minmax(180px, 220px) minmax(320px, 1fr) 280px !important;
gap: 22px !important;
}
#content .article .subjectwrap.clearfix > .subject.clearfix {
grid-template-columns: minmax(180px, 220px) minmax(320px, 1fr) !important;
gap: 22px !important;
}
#content .article .subjectwrap.clearfix > .subject.clearfix > #mainpic,
#content .article .subjectwrap.clearfix > .subject.clearfix > #mainpic img {
max-width: 220px !important;
}
#content .article .subjectwrap.clearfix > #interest_sectl {
width: 280px !important;
min-width: 280px !important;
max-width: 280px !important;
}
}
@media (max-width: 768px) {
#wrapper {
width: 100vw !important;
min-width: auto !important;
}
.grid-16-8.clearfix > .article,
.grid-16-8.clearfix > .aside {
width: 100% !important;
float: none !important;
}
#content .grid-16-8.clearfix,
.grid-16-8.clearfix {
grid-template-columns: 1fr !important;
}
#content .article .subjectwrap.clearfix {
display: grid !important;
grid-template-columns: 1fr !important;
gap: 16px !important;
}
#content .article .subjectwrap.clearfix > .subject.clearfix {
grid-column: 1 !important;
grid-template-columns: minmax(120px, 34vw) minmax(0, 1fr) !important;
gap: 16px !important;
}
#content .article .subjectwrap.clearfix > .subject.clearfix > #mainpic,
#content .article .subjectwrap.clearfix > .subject.clearfix > #mainpic img {
max-width: 180px !important;
}
#content .article .subjectwrap.clearfix > #interest_sectl {
grid-column: 1 !important;
width: 100% !important;
min-width: 0 !important;
max-width: none !important;
}
}
/* Dark mode support (if available) */
@media (prefers-color-scheme: dark) {
body,
#wrapper,
.article {
background-color: var(--color-bg, #1a1a1a) !important;
color: var(--color-text, #e0e0e0) !important;
}
}
/* Print optimization */
@media print {
#wrapper,
.grid-16-8 {
width: 100% !important;
max-width: none !important;
}
nav,
aside,
footer,
.ads {
display: none !important;
}
}
/* Accessibility improvements */
*:focus-visible {
outline: 2px solid var(--color-focus, #4a90e2) !important;
outline-offset: 2px !important;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth !important;
}
/* Selection styling */
::selection {
background-color: rgba(74, 144, 226, 0.2) !important;
color: inherit !important;
}
`;
}
// Inject styles into the page
function injectStyles() {
const pageType = getPageType();
const styles = generateStyles(pageType);
const existing = document.getElementById('douban-widescreen-optimizer');
if (existing) {
existing.remove();
}
const styleElement = document.createElement('style');
styleElement.id = 'douban-widescreen-optimizer';
styleElement.type = 'text/css';
styleElement.textContent = styles;
// Append at the end so optimizer rules win over Douban and late user styles.
document.head.appendChild(styleElement);
console.log(`[Douban Widescreen Optimizer] Applied optimizations for page type: ${pageType}`);
}
function applySubjectSummaryLayout() {
if (getPageType() !== 'subject') return;
const article = document.querySelector('#content .grid-16-8.clearfix > .article');
const summary = document.querySelector('#content .grid-16-8.clearfix > .article .related-info');
if (!article || !summary) return;
const articleRect = article.getBoundingClientRect();
const articleStyle = window.getComputedStyle(article);
const horizontalPadding = parseFloat(articleStyle.paddingLeft || '0') + parseFloat(articleStyle.paddingRight || '0');
const summaryWidth = Math.max(600, Math.floor(articleRect.width - horizontalPadding));
const summaryWidthPx = `${summaryWidth}px`;
const selectors = [
'#content .grid-16-8.clearfix > .article .related-info',
'#content .grid-16-8.clearfix > .article .related-info .indent',
'#content .grid-16-8.clearfix > .article .related-info #link-report-intra',
'#content .grid-16-8.clearfix > .article .related-info .short',
'#content .grid-16-8.clearfix > .article .related-info .all',
'#content .grid-16-8.clearfix > .article .related-info span[property="v:summary"]'
];
document.querySelectorAll(selectors.join(',')).forEach((element) => {
element.style.setProperty('width', summaryWidthPx, 'important');
element.style.setProperty('min-width', summaryWidthPx, 'important');
element.style.setProperty('max-width', summaryWidthPx, 'important');
element.style.setProperty('box-sizing', 'border-box', 'important');
element.style.setProperty('float', 'none', 'important');
element.style.setProperty('clear', 'both', 'important');
element.style.setProperty('margin-left', '0', 'important');
element.style.setProperty('margin-right', '0', 'important');
element.style.setProperty('white-space', 'normal', 'important');
element.style.setProperty('word-break', 'normal', 'important');
element.style.setProperty('overflow-wrap', 'break-word', 'important');
});
document.querySelectorAll(
'#content .grid-16-8.clearfix > .article .related-info .short, ' +
'#content .grid-16-8.clearfix > .article .related-info .all:not(.hidden), ' +
'#content .grid-16-8.clearfix > .article .related-info span[property="v:summary"]'
).forEach((element) => {
element.style.setProperty('display', 'block', 'important');
element.style.setProperty('line-height', '1.75', 'important');
});
document.querySelectorAll('#content .grid-16-8.clearfix > .article .related-info .all.hidden').forEach((element) => {
element.style.setProperty('display', 'none', 'important');
});
}
function scheduleSubjectSummaryLayout() {
applySubjectSummaryLayout();
window.setTimeout(applySubjectSummaryLayout, 300);
window.setTimeout(applySubjectSummaryLayout, 1200);
let reapplyCount = 0;
const intervalId = window.setInterval(() => {
applySubjectSummaryLayout();
reapplyCount += 1;
if (reapplyCount >= 20) {
window.clearInterval(intervalId);
}
}, 500);
}
function observeSubjectSummaryChanges() {
if (getPageType() !== 'subject') return;
const summary = document.querySelector('#content .grid-16-8.clearfix > .article .related-info');
if (!summary || summary.dataset.doubanWidescreenObserved === '1') return;
summary.dataset.doubanWidescreenObserved = '1';
let pending = false;
const observer = new MutationObserver(() => {
if (pending) return;
pending = true;
window.requestAnimationFrame(() => {
pending = false;
applySubjectSummaryLayout();
});
});
observer.observe(summary, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['style', 'class']
});
}
// Handle dynamic content loading (SPA-like behavior)
function observeDOMChanges() {
const observer = new MutationObserver((mutations) => {
let shouldReapply = false;
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
// Check if new content containers were added
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches && node.matches('#wrapper, #content, .grid-16-8, main, article')) {
shouldReapply = true;
}
}
});
}
});
if (shouldReapply) {
injectStyles();
scheduleSubjectSummaryLayout();
observeSubjectSummaryChanges();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Initialize when DOM is ready
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
injectStyles();
scheduleSubjectSummaryLayout();
observeSubjectSummaryChanges();
observeDOMChanges();
});
} else {
injectStyles();
scheduleSubjectSummaryLayout();
observeSubjectSummaryChanges();
observeDOMChanges();
}
}
// Start the optimizer
init();
})();