// ==UserScript==
// @name Gemini Wide Screen and Input Box Height Adjuster
// @namespace http://www.jeffbyers.com
// @match https://gemini.google.com/*
// @grant none
// @version 2.0
// @author Jeff Byers <[email protected]>
// @license GPLv3 - http://www.gnu.org/licenses/gpl-3.0.txt
// @copyright Copyright (C) 2024, by Jeff Byers <[email protected]>
// @icon https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png
// @description Makes the Google Gemini conversation window the full width of the browser window and adds buttons to change the input text area height.
// @icon https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png
// ==/UserScript==
(function () {
'use strict';
// global toggles
let wideModeEnabled = localStorage.getItem('geminiWideModeEnabled') === 'true';
let buttonCreated = false;
let pageReady = false;
// global variables for max rows (load from sessionStorage if available)
let maxRowsWide = parseInt(sessionStorage.getItem('maxRowsWide'), 10) || 12;
let minRowsWide = parseInt(sessionStorage.getItem('minRowsWide'), 10) || 6;
let minRowsNormal = parseInt(sessionStorage.getItem('minRowsNormal'), 10) || 3;
let maxRowsNormal = parseInt(sessionStorage.getItem('maxRowsNormal'), 10) || 8;
// custom trusted types handling
let needsTrustedHTML = false;
const passThroughFunc = (string, sink) => string;
const TTPName = "geminiStylePolicy";
let TP = {
createHTML: passThroughFunc,
createScript: passThroughFunc,
createScriptURL: passThroughFunc,
};
try {
if (
typeof window.isSecureContext !== "undefined" &&
window.isSecureContext
) {
if (window.trustedTypes && window.trustedTypes.createPolicy) {
if (trustedTypes.defaultPolicy) {
TP = trustedTypes.defaultPolicy;
} else {
TP = window.trustedTypes.createPolicy(TTPName, TP);
}
needsTrustedHTML = true;
}
}
} catch (e) {
// log trusted types initialization error
console.error("Error initializing Trusted Types:", e);
}
function trustedHTML(string) {
try {
return needsTrustedHTML ? TP.createHTML(string) : string;
} catch (error) {
console.error("Error in trustedHTML:", error, "Original string:", string);
// fallback to returning the original string (unsafe, but allows debugging)
return string;
}
}
function getNonce() {
const cspHeader = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
if (!cspHeader) return null;
const cspContent = cspHeader.content;
const nonceMatch = cspContent.match(/nonce-(.+?)[';]/);
return nonceMatch ? nonceMatch[1] : null;
}
function resizeStuff() {
// directly set max-width
const elementsToSetMaxWidth = document.querySelectorAll(
'div.bottom-container, ' +
'div.input-area-container, ' +
'div.conversation-container, ' +
'.text-input-field'
);
elementsToSetMaxWidth.forEach((element) => {
element.style.maxWidth = wideModeEnabled ? '100%' : '';
});
// set --textarea-max-rows dynamically based on wideModeEnabled
document.documentElement.style.setProperty('--textarea-max-rows', wideModeEnabled ? maxRowsWide : maxRowsNormal);
document.documentElement.style.setProperty('--textarea-min-rows', wideModeEnabled ? Math.max(3, minRowsWide) : Math.max(3, minRowsNormal));
// find and adjust rich-textarea .ql-editor
const qlEditors = document.querySelectorAll('rich-textarea .ql-editor');
qlEditors.forEach((editor) => {
// Set --textarea-max-rows directly on the rich-textarea element
editor.style.setProperty(
'--textarea-max-rows',
wideModeEnabled ? maxRowsWide : maxRowsNormal
);
editor.style.minHeight = 'calc((var(--textarea-min-rows, 0)) * 24px)';
editor.style.maxHeight = 'calc((var(--textarea-max-rows, 0)) * 24px)';
});
}
function toggleWideMode() {
wideModeEnabled = !wideModeEnabled;
resizeStuff();
updateButtonIcon(); // update the icon
}
function windowSizeUp() {
const preFullscreenElements = document.querySelectorAll('.pre-fullscreen');
if (preFullscreenElements.length > 0 && wideModeEnabled) {
maxRowsWide++; // increase max rows for wide mode if in fullscreen
sessionStorage.setItem('maxRowsWide', maxRowsWide); // store in sessionStorage
} else if (preFullscreenElements.length > 0 && !wideModeEnabled) {
maxRowsNormal++; // increase max rows for normal mode if in fullscreen
sessionStorage.setItem('maxRowsNormal', maxRowsNormal);
} else if (!wideModeEnabled) {
minRowsNormal++; // increase min rows for normal mode if not in fullscreen
sessionStorage.setItem('minRowsNormal', minRowsNormal);
} else {
minRowsWide++; // increase min rows for wide mode if not in fullscreen
sessionStorage.setItem('minRowsWide', minRowsWide);
}
resizeStuff(); // reapply styles to reflect the changes
}
function windowSizeDown() {
const preFullscreenElements = document.querySelectorAll('.pre-fullscreen');
if (preFullscreenElements.length > 0 && wideModeEnabled) {
maxRowsWide = Math.max(minRowsWide, maxRowsWide - 1); // decrease max rows for wide mode if in fullscreen (but not below minRowsWide)
sessionStorage.setItem('maxRowsWide', maxRowsWide); // store in sessionStorage
} else if (preFullscreenElements.length > 0 && !wideModeEnabled) {
maxRowsNormal = Math.max(minRowsNormal, maxRowsNormal - 1); // decrease max rows for normal mode if in fullscreen (but not below minRowsNormal)
sessionStorage.setItem('maxRowsNormal', maxRowsNormal);
} else if (!wideModeEnabled) {
minRowsNormal = Math.max(1, minRowsNormal - 1); // decrease min rows for normal mode if not in fullscreen (but not below 1)
sessionStorage.setItem('minRowsNormal', minRowsNormal);
} else {
minRowsWide = Math.max(1, minRowsWide - 1); // decrease min rows for wide mode if not in fullscreen (but not below 1)
sessionStorage.setItem('minRowsWide', minRowsWide);
}
resizeStuff(); // reapply styles to reflect the changes
}
// modified addGlobalStyle using CSSStyleSheet (with fallback)
function addGlobalStyle(css) {
if (window.CSSStyleSheet && CSSStyleSheet.prototype.replaceSync) {
const sheet = new CSSStyleSheet();
sheet.replaceSync(trustedHTML(css));
document.adoptedStyleSheets = [sheet];
} else {
let head = document.head || document.getElementsByTagName('head')[0];
if (!head) return;
let style = document.createElement('style');
style.type = 'text/css';
style.textContent = trustedHTML(css);
head.appendChild(style);
}
}
// function to create and insert the button (with error handling)
function createToggleButton() {
try {
const inputButtonsWrapperBottom = document.querySelector('.input-buttons-wrapper-bottom');
const inputButtonsWrapperTop = document.querySelector('.input-buttons-wrapper-top');
if (!inputButtonsWrapperBottom || !inputButtonsWrapperTop) {
console.log("One or both wrappers not found. Retrying...");
setTimeout(createToggleButton, 100); // Retry after a delay
return; // Exit the function if either wrapper is not found
}
// Check for existing buttons using a unique identifier (e.g., aria-label)
if (inputButtonsWrapperTop.querySelector('[aria-label="Toggle Wide Mode"]') ||
inputButtonsWrapperBottom.querySelector('[aria-label="Window Size Up"]') ||
inputButtonsWrapperBottom.querySelector('[aria-label="Window Size Down"]')) {
console.log("Buttons already exist. Skipping creation.");
return; // Exit the function if any button already exists
}
// Create the buttons using Trusted Types
const toggleButtonHTML = trustedHTML(`
<div class="speech-dictation-mic-button ng-star-inserted">
<button data-node-type="speech_dictation_mic_button" maticonsuffix="" mat-icon-button=""
mattooltip="Wide Mode" aria-label="Toggle Wide Mode"
class="wide-screen-button mat-mdc-tooltip-trigger speech_dictation_mic_button mdc-icon-button mat-mdc-icon-button gmat-mdc-button-with-prefix mat-unthemed mat-mdc-button-base gmat-mdc-button">
<span class="mat-mdc-button-persistent-ripple mdc-icon-button__ripple"></span>
<div>
<span class="mat-icon notranslate google-symbols mat-icon-no-color" id="wide_screen_image" data-mat-iic class="material-symbols-outlined"></span>
</div>
<span class="mat-mdc-focus-indicator"></span>
<span class="mat-mdc-button-touch-target"></span>
</button>
</div>
`);
const SizeButtonsHTML = trustedHTML(`
<div class="speech-dictation-mic-button ng-star-inserted">
<button data-node-type="speech_dictation_mic_button" maticonsuffix="" mat-icon-button=""
mattooltip="Size Down" aria-label="Window Size Down"
class="wide-screen-button mat-mdc-tooltip-trigger speech_dictation_mic_button mdc-icon-button mat-mdc-icon-button gmat-mdc-button-with-prefix mat-unthemed mat-mdc-button-base gmat-mdc-button">
<span class="mat-mdc-button-persistent-ripple mdc-icon-button__ripple"></span>
<div>
<span class="mat-icon notranslate google-symbols mat-icon-no-color" data-mat-iic class="material-symbols-outlined">keyboard_arrow_down</span>
</div>
<span class="mat-mdc-focus-indicator"></span>
<span class="mat-mdc-button-touch-target"></span>
</button>
</div>
<div class="speech-dictation-mic-button ng-star-inserted">
<button data-node-type="speech_dictation_mic_button" maticonsuffix="" mat-icon-button=""
mattooltip="Size Up" aria-label="Window Size Up"
class="wide-screen-button mat-mdc-tooltip-trigger speech_dictation_mic_button mdc-icon-button mat-mdc-icon-button gmat-mdc-button-with-prefix mat-unthemed mat-mdc-button-base gmat-mdc-button">
<span class="mat-mdc-button-persistent-ripple mdc-icon-button__ripple"></span>
<div>
<span class="mat-icon notranslate google-symbols mat-icon-no-color" data-mat-iic class="material-symbols-outlined">keyboard_arrow_up</span>
</div>
<span class="mat-mdc-focus-indicator"></span>
<span class="mat-mdc-button-touch-target"></span>
</button>
</div>
`);
// use insertAdjacentHTML to add the buttons
inputButtonsWrapperTop.insertAdjacentHTML('afterbegin', toggleButtonHTML);
inputButtonsWrapperBottom.insertAdjacentHTML('afterend', SizeButtonsHTML);
// add click event listeners
const toggleButton = document.querySelector('[aria-label="Toggle Wide Mode"]');
const sizeUpButton = document.querySelector('[aria-label="Window Size Up"]');
const sizeDownButton = document.querySelector('[aria-label="Window Size Down"]');
toggleButton.addEventListener('click', toggleWideMode);
sizeUpButton.addEventListener('click', windowSizeUp);
sizeDownButton.addEventListener('click', windowSizeDown);
buttonCreated = true;
console.log('Buttons created successfully! We\'re done.');
} catch (error) {
console.error("Error in createToggleButton:", error);
}
}
// have to set pre-fullscreen globally as the elements don't exist when the page loads
addGlobalStyle(trustedHTML(`
div .pre-fullscreen {
height: auto !important;
}
div .input-buttons-wrapper-top {
right: 8px !important;
}
div .isFullscreen {
max-height: 100% !important;
}
`));
// function to update the button icon based on wideModeEnabled
function updateButtonIcon() {
const iconSpan = document.getElementById("wide_screen_image");
if (iconSpan) {
iconSpan.textContent = wideModeEnabled ? 'remove' : 'add';
}
// Store the updated state in localStorage
localStorage.setItem('geminiWideModeEnabled', wideModeEnabled);
}
// function to check if page is loaded and ready for our button
function checkIfPageReadyForButton() {
if (document.readyState === 'complete' && !buttonCreated) {
pageReady = true;
console.log('Page is ready. Now make the button.');
createToggleButton();
}
}
// callback function for the MutationObserver
function mutationObserverCallback() {
checkIfPageReadyForButton(); // check if button needs to be created
updateButtonIcon();
resizeStuff(); // apply or update styles
}
// initial check and then MutationObserver to handle potential changes
checkIfPageReadyForButton();
const observer = new MutationObserver(mutationObserverCallback);
observer.observe(document.body, { childList: true, subtree: true });
})();