Better NotebookLM

Enhanced NotebookLM: Auto-collapse sidebar, resizable Studio panel, UI improvements

// ==UserScript==
// @name         Better NotebookLM
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  Enhanced NotebookLM: Auto-collapse sidebar, resizable Studio panel, UI improvements
// @author       djshigel
// @license      MIT
// @match        https://notebooklm.google.com/*
// @match        https://notebooklm.google/*
// @icon         https://notebooklm.google.com/favicon.ico
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        defaultStudioWidth: 30, // Default Studio panel width (%)
        expandStudioWidth: 53, // Width to expand Studio panel when min-inline-size is detected (%)
        minPanelWidth: 20, // Minimum panel width (%)
        maxPanelWidth: 80, // Maximum panel width (%)
        dragHandleWidth: 8, // Drag handle width (px)
        rightPanelOffset: 88, // Fixed offset to prevent right panel overflow (px)
    };

    // ========================
    // Auto-collapse sidebar
    // ========================
    
    function collapseSidebar() {
        const panel = document.querySelector('section.source-panel:not(.panel-collapsed)');
        
        if (panel) {
            const toggleButton = panel.querySelector('button.toggle-source-panel-button');
            
            if (toggleButton) {
                toggleButton.click();
                console.log('Better NotebookLM: Sidebar collapsed');
                return true;
            }
        }
        
        const collapsedPanel = document.querySelector('section.source-panel.panel-collapsed');
        if (collapsedPanel) {
            return true;
        }
        
        return false;
    }

    // ========================
    // Studio panel resizer
    // ========================
    
    let dragHandlers = {
        mousedown: null,
        mousemove: null,
        mouseup: null
    };
    
    let resizerInitialized = false;
    
    function destroyStudioResizer() {
        const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
        const gutter = studioPanel?.previousElementSibling;
        const chatPanel = gutter?.previousElementSibling;
        
        if (gutter && gutter.dataset.resizerInitialized) {
            // Remove event listeners
            if (dragHandlers.mousedown) {
                gutter.removeEventListener('mousedown', dragHandlers.mousedown);
            }
            if (dragHandlers.mousemove) {
                document.removeEventListener('mousemove', dragHandlers.mousemove);
            }
            if (dragHandlers.mouseup) {
                document.removeEventListener('mouseup', dragHandlers.mouseup);
            }
            
            // Reset gutter styles
            gutter.style.cursor = '';
            gutter.style.backgroundColor = '';
            gutter.style.width = '';
            delete gutter.dataset.resizerInitialized;
            
            // Remove ALL custom styles from panels
            if (studioPanel) {
                studioPanel.removeAttribute('style');
            }
            if (chatPanel) {
                chatPanel.removeAttribute('style');
            }
            
            // Reset initialization flag
            resizerInitialized = false;
            
            console.log('Better NotebookLM: Studio resizer destroyed');
        }
    }
    
    function observeStudioCollapse() {
        let lastCollapseState = null;
        
        const observer = new MutationObserver(() => {
            const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
            if (!studioPanel) return;
            
            // Check for min-inline-size and handle appropriately
            if (studioPanel.style.minInlineSize) {
                const minInlineValue = studioPanel.style.minInlineSize;
                
                // Always remove min-inline-size
                studioPanel.style.minInlineSize = '';
                
                // Only expand for specific large values (like 37.5vw that NotebookLM uses)
                // This indicates actual content like video or report is being shown
                if ((minInlineValue.includes('vw') && parseFloat(minInlineValue) >= 30) ||
                    (minInlineValue.includes('%') && parseFloat(minInlineValue) >= 40)) {
                    
                    const chatPanel = document.querySelector('section.chat-panel');
                    if (!studioPanel.classList.contains('panel-collapsed') && chatPanel) {
                        // Add transition for smooth animation
                        studioPanel.style.transition = 'all 0.3s ease';
                        chatPanel.style.transition = 'all 0.3s ease';
                        
                        // Apply expanded width
                        studioPanel.style.flex = '';
                        studioPanel.style.maxWidth = `calc(${CONFIG.expandStudioWidth}% - ${CONFIG.rightPanelOffset}px)`;
                        studioPanel.style.width = `calc(${CONFIG.expandStudioWidth}% - ${CONFIG.rightPanelOffset}px)`;
                        chatPanel.style.flex = `0 0 ${100 - CONFIG.expandStudioWidth}%`;
                        chatPanel.style.maxWidth = `${100 - CONFIG.expandStudioWidth}%`;
                        
                        // Save the expanded width
                        localStorage.setItem('betterNotebookLM_studioWidth', CONFIG.expandStudioWidth.toString());
                        
                        // Remove transition after animation
                        setTimeout(() => {
                            studioPanel.style.transition = '';
                            chatPanel.style.transition = '';
                        }, 300);
                        
                        console.log(`Better NotebookLM: Detected ${minInlineValue} - expanded Studio to ${CONFIG.expandStudioWidth}%`);
                    }
                } else {
                    console.log(`Better NotebookLM: Removed min-inline-size (${minInlineValue})`);
                }
            }
            
            const isCollapsed = studioPanel.classList.contains('panel-collapsed');
            
            // Only act if state actually changed
            if (lastCollapseState !== isCollapsed) {
                lastCollapseState = isCollapsed;
                
                if (isCollapsed) {
                    // Destroy resizer when collapsed
                    destroyStudioResizer();
                    
                    // Remove flex and max-width from chat panel when Studio is collapsed
                    const chatPanel = document.querySelector('section.chat-panel');
                    if (chatPanel) {
                        chatPanel.style.flex = '';
                        chatPanel.style.maxWidth = '';
                        console.log('Better NotebookLM: Removed chat panel custom styles on Studio collapse');
                    }
                    
                    console.log('Better NotebookLM: Studio collapsed - resizer destroyed, width forced to 56px');
                } else {
                    // Re-initialize resizer when expanded
                    setTimeout(() => {
                        resizerInitialized = false; // Reset flag before re-initializing
                        resizerInitialized = initStudioResizer();
                        
                        // Restore saved size if available
                        const savedWidth = localStorage.getItem('betterNotebookLM_studioWidth');
                        if (savedWidth && resizerInitialized) {
                            const gutter = studioPanel.previousElementSibling;
                            const chatPanel = gutter?.previousElementSibling;
                            
                            if (chatPanel && studioPanel) {
                                const width = parseFloat(savedWidth);
                                studioPanel.style.flex = '';
                                studioPanel.style.maxWidth = `calc(${width}% - ${CONFIG.rightPanelOffset}px)`;
                                studioPanel.style.width = `calc(${width}% - ${CONFIG.rightPanelOffset}px)`;
                                chatPanel.style.flex = `0 0 ${100 - width}%`;
                                chatPanel.style.maxWidth = `${100 - width}%`;
                                console.log('Better NotebookLM: Studio expanded - resizer and size restored');
                            }
                        } else {
                            console.log('Better NotebookLM: Studio expanded - resizer restored');
                        }
                    }, 100);
                }
            }
        });
        
        // Observe the entire document for class changes
        observer.observe(document.body, {
            attributes: true,
            attributeFilter: ['class', 'style'],
            subtree: true
        });
        
        return observer;
    }
    
    function initStudioResizer() {
        // Find the gutter between chat and Studio panels
        const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
        if (!studioPanel) return false;
        
        // Don't initialize if Studio is collapsed
        if (studioPanel.classList.contains('panel-collapsed')) {
            console.log('Better NotebookLM: Studio is collapsed, skipping resizer init');
            return false;
        }
        
        // Find the gutter that's immediately before the Studio panel
        const gutter = studioPanel.previousElementSibling;
        if (!gutter || !gutter.classList.contains('panel-gutter')) return false;
        
        // Skip if already initialized
        if (gutter.dataset.resizerInitialized) return true;
        
        // Apply resize handle styles
        gutter.style.cursor = 'col-resize';
        gutter.style.position = 'relative';
        gutter.style.userSelect = 'none';
        gutter.style.width = CONFIG.dragHandleWidth + 'px';
        gutter.style.backgroundColor = 'transparent';
        gutter.style.transition = 'background-color 0.2s';
        
        // Visual feedback on hover
        gutter.addEventListener('mouseenter', () => {
            gutter.style.backgroundColor = 'rgba(66, 133, 244, 0.2)';
        });
        
        gutter.addEventListener('mouseleave', () => {
            if (!gutter.dataset.dragging) {
                gutter.style.backgroundColor = 'transparent';
            }
        });
        
        // Drag handling
        let isDragging = false;
        let startX = 0;
        let startRightWidth = 0;
        let chatPanel = null;
        let rightPanel = studioPanel;
        let container = null;
        
        const handleMouseDown = (e) => {
            isDragging = true;
            startX = e.clientX;
            gutter.dataset.dragging = 'true';
            gutter.style.backgroundColor = 'rgba(66, 133, 244, 0.4)';
            
            // Get adjacent panels
            chatPanel = gutter.previousElementSibling;
            container = gutter.parentElement;
            
            if (rightPanel && container) {
                const containerWidth = container.offsetWidth;
                startRightWidth = (rightPanel.offsetWidth / containerWidth) * 100;
            }
            
            // Prevent cursor and text selection during drag
            document.body.style.cursor = 'col-resize';
            document.body.style.userSelect = 'none';
            
            // Add overlay to prevent iframe interference
            const overlay = document.createElement('div');
            overlay.id = 'resize-overlay';
            overlay.style.cssText = `
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                z-index: 9999;
                cursor: col-resize;
            `;
            document.body.appendChild(overlay);
            
            e.preventDefault();
        };
        
        const handleMouseMove = (e) => {
            if (!isDragging || !chatPanel || !rightPanel || !container) return;
            
            const containerWidth = container.offsetWidth;
            const deltaX = e.clientX - startX;
            const deltaPercent = (deltaX / containerWidth) * 100;
            let newRightWidth = startRightWidth - deltaPercent;
            
            // Limit width
            newRightWidth = Math.max(CONFIG.minPanelWidth, Math.min(CONFIG.maxPanelWidth, newRightWidth));
            const newLeftWidth = 100 - newRightWidth;
            
            // Update panel sizes
            chatPanel.style.flex = '';
            chatPanel.style.maxWidth = `${newLeftWidth}%`;
            rightPanel.style.flex = '';
            rightPanel.style.maxWidth = `calc(${newRightWidth}% - ${CONFIG.rightPanelOffset}px)`;
            rightPanel.style.width = `calc(${newRightWidth}% - ${CONFIG.rightPanelOffset}px)`;
            
            // Save to localStorage
            localStorage.setItem('betterNotebookLM_studioWidth', newRightWidth.toString());
            
            e.preventDefault();
        };
        
        const handleMouseUp = () => {
            if (!isDragging) return;
            
            isDragging = false;
            delete gutter.dataset.dragging;
            gutter.style.backgroundColor = 'transparent';
            
            // Restore cursor and text selection
            document.body.style.cursor = '';
            document.body.style.userSelect = '';
            
            // Remove overlay
            const overlay = document.getElementById('resize-overlay');
            if (overlay) overlay.remove();
        };
        
        // Store handlers globally for cleanup
        dragHandlers.mousedown = handleMouseDown;
        dragHandlers.mousemove = handleMouseMove;
        dragHandlers.mouseup = handleMouseUp;
        
        gutter.addEventListener('mousedown', handleMouseDown);
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
        
        // Mark as initialized
        gutter.dataset.resizerInitialized = 'true';
        
        // Apply saved width
        const savedWidth = localStorage.getItem('betterNotebookLM_studioWidth');
        if (savedWidth && chatPanel && rightPanel) {
            const width = parseFloat(savedWidth);
            rightPanel.style.flex = `0 0 ${width}%`;
            rightPanel.style.maxWidth = `calc(${width}% - ${CONFIG.rightPanelOffset}px)`;
            rightPanel.style.width = `calc(${width}% - ${CONFIG.rightPanelOffset}px)`;
            chatPanel.style.flex = `0 0 ${100 - width}%`;
            chatPanel.style.maxWidth = `${100 - width}%`;
        }
        
        console.log('Better NotebookLM: Studio resizer initialized');
        return true;
    }

    // ========================
    // Main processing
    // ========================
    
    let sidebarProcessed = false;
    let retryCount = 0;
    const maxRetries = 20;
    
    function processElements() {
        // Process sidebar
        if (!sidebarProcessed) {
            sidebarProcessed = collapseSidebar();
        }
        
        // Initialize Studio resizer
        if (!resizerInitialized) {
            const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
            
            // If Studio exists and is collapsed, mark as complete but don't initialize
            if (studioPanel && studioPanel.classList.contains('panel-collapsed')) {
                resizerInitialized = true;
                console.log('Better NotebookLM: Studio is collapsed, skipping resizer');
            } else {
                resizerInitialized = initStudioResizer();
            }
        }
        
        // Check completion
        if ((sidebarProcessed && resizerInitialized) || retryCount >= maxRetries) {
            if (retryCount >= maxRetries) {
                console.log('Better NotebookLM: Maximum retries reached');
            } else {
                console.log('Better NotebookLM: All features enabled');
            }
            return true;
        }
        
        return false;
    }
    
    // Periodic element check
    const checkInterval = setInterval(() => {
        retryCount++;
        
        if (processElements()) {
            clearInterval(checkInterval);
            // Start observing Studio collapse state after initialization
            observeStudioCollapse();
        }
    }, 500);
    
    // Handle page navigation
    let lastUrl = location.href;
    const urlObserver = new MutationObserver(() => {
        const currentUrl = location.href;
        if (currentUrl !== lastUrl) {
            lastUrl = currentUrl;
            console.log('Better NotebookLM: Page changed, reinitializing...');
            
            // Reset flags
            sidebarProcessed = false;
            resizerInitialized = false;
            retryCount = 0;
            
            setTimeout(() => {
                const retryInterval = setInterval(() => {
                    retryCount++;
                    
                    if (!sidebarProcessed) {
                        sidebarProcessed = collapseSidebar();
                    }
                    
                    if (!resizerInitialized) {
                        resizerInitialized = initStudioResizer();
                    }
                    
                    if ((sidebarProcessed && resizerInitialized) || retryCount >= 10) {
                        clearInterval(retryInterval);
                        observeStudioCollapse();
                    }
                }, 500);
            }, 1000);
        }
    });
    
    // Monitor URL changes
    urlObserver.observe(document.body, {
        childList: true,
        subtree: true
    });
    
    // ========================
    // Style injection
    // ========================
    
    const style = document.createElement('style');
    style.textContent = `
        /* Force logo margins */
        labs-tailwind-logo img {
            margin-left: 5px !important;
            margin-right: 5px !important;
        }
        
        /* Force Source panel expanded width */
        section.source-panel:not(.panel-collapsed) {
            width: 395px !important;
            min-width: 395px !important;
            max-width: 395px !important;
            flex: 0 0 395px !important;
        }
        
        /* Panel gutter hover effect */
        .panel-gutter[data-resizer-initialized="true"] {
            position: relative;
            z-index: 100;
        }
        
        .panel-gutter[data-resizer-initialized="true"]::before {
            content: '';
            position: absolute;
            top: 0;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%);
            width: 3px;
            background: rgba(66, 133, 244, 0.5);
            opacity: 0;
            transition: opacity 0.2s;
        }
        
        .panel-gutter[data-resizer-initialized="true"]:hover::before {
            opacity: 1;
        }
        
        /* Disable animations during drag to prevent jitter */
        .panel-gutter[data-dragging="true"] ~ *,
        .panel-gutter[data-dragging="true"] ~ * * {
            transition: none !important;
        }
        
        /* Source panel collapsed state - hide scrollbar until hover */
        .source-panel.panel-collapsed .source-panel-content {
            overflow: hidden !important;
        }
        
        .source-panel.panel-collapsed .source-panel-content:hover {
            overflow-y: auto !important;
            overflow-x: hidden !important;
        }
        
        /* Force Studio panel width when collapsed */
        section.studio-panel.panel-collapsed,
        section[class*="studio"].panel-collapsed,
        section[class*="right-panel"].panel-collapsed {
            width: 56px !important;
            min-width: 56px !important;
            max-width: 56px !important;
            flex: 0 0 56px !important;
        }
        
        /* Remove min-inline-size from Studio panel */
        section.studio-panel,
        section[class*="studio"],
        section[class*="right-panel"] {
            min-inline-size: unset !important;
        }
        
        /* Chat panel should use available space when Studio is collapsed */
        section.chat-panel:has(~ .panel-collapsed) {
            flex: 1 1 auto !important;
            max-width: none !important;
        }
        
        /* Sticky Studio panel buttons to prevent overflow */
        .studio-panel button.toggle-studio-panel-button,
        .studio-panel button[aria-label*="Studio"],
        .studio-panel button[aria-label*="メモ"],
        .studio-panel button[aria-label*="note"] {
            position: sticky;
            right: 0;
            z-index: 10;
        }
        
        /* Studio panel header sticky positioning */
        .studio-panel .panel-header,
        .studio-panel .studio-header {
            position: sticky;
            top: 0;
            z-index: 10;
            background: inherit;
        }
    `;
    document.head.appendChild(style);
    
    console.log('Better NotebookLM: Initialization complete');
    
    // Additional monitoring for min-inline-size changes as a backup
    const styleObserver = new MutationObserver(() => {
        const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
        if (studioPanel && studioPanel.style.minInlineSize) {
            const minInlineValue = studioPanel.style.minInlineSize;
            
            // Always remove min-inline-size
            studioPanel.style.minInlineSize = '';
            
            // Only expand for specific large values
            if ((minInlineValue.includes('vw') && parseFloat(minInlineValue) >= 30) ||
                (minInlineValue.includes('%') && parseFloat(minInlineValue) >= 40)) {
                
                const chatPanel = document.querySelector('section.chat-panel');
                if (!studioPanel.classList.contains('panel-collapsed') && chatPanel) {
                    // Add transition for smooth animation
                    studioPanel.style.transition = 'all 0.3s ease';
                    chatPanel.style.transition = 'all 0.3s ease';
                    
                    // Apply expanded width
                    studioPanel.style.flex = `0 0 ${CONFIG.expandStudioWidth}%`;
                    studioPanel.style.maxWidth = `calc(${CONFIG.expandStudioWidth}% - ${CONFIG.rightPanelOffset}px)`;
                    studioPanel.style.width = `calc(${CONFIG.expandStudioWidth}% - ${CONFIG.rightPanelOffset}px)`;
                    chatPanel.style.flex = `0 0 ${100 - CONFIG.expandStudioWidth}%`;
                    chatPanel.style.maxWidth = `${100 - CONFIG.expandStudioWidth}%`;
                    
                    // Save the expanded width
                    localStorage.setItem('betterNotebookLM_studioWidth', CONFIG.expandStudioWidth.toString());
                    
                    // Remove transition after animation
                    setTimeout(() => {
                        studioPanel.style.transition = '';
                        chatPanel.style.transition = '';
                    }, 300);
                    
                    console.log(`Better NotebookLM (backup): Detected ${minInlineValue} - expanded to ${CONFIG.expandStudioWidth}%`);
                }
            }
        }
    });
    
    // Start monitoring after initial setup
    setTimeout(() => {
        const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
        if (studioPanel) {
            styleObserver.observe(studioPanel, {
                attributes: true,
                attributeFilter: ['style']
            });
        }
    }, 2000);
    
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址