Douban Widescreen Optimizer

Optimize Douban layout for widescreen displays, utilizing full screen width with modern responsive design

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

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

})();