Gemini Wide Screen and Input Box Height Adjuster

Makes the Google Gemini conversation window the full width of the browser window and adds buttons to change the input text area height.

  1. // ==UserScript==
  2. // @name Gemini Wide Screen and Input Box Height Adjuster
  3. // @namespace http://www.jeffbyers.com
  4. // @match https://gemini.google.com/*
  5. // @grant none
  6. // @version 2.0
  7. // @author Jeff Byers <jeff@jeffbyers.com>
  8. // @license GPLv3 - http://www.gnu.org/licenses/gpl-3.0.txt
  9. // @copyright Copyright (C) 2024, by Jeff Byers <jeff@jeffbyers.com>
  10. // @icon https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png
  11. // @description Makes the Google Gemini conversation window the full width of the browser window and adds buttons to change the input text area height.
  12. // @icon https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. // global toggles
  19. let wideModeEnabled = localStorage.getItem('geminiWideModeEnabled') === 'true';
  20. let buttonCreated = false;
  21. let pageReady = false;
  22.  
  23. // global variables for max rows (load from sessionStorage if available)
  24. let maxRowsWide = parseInt(sessionStorage.getItem('maxRowsWide'), 10) || 12;
  25. let minRowsWide = parseInt(sessionStorage.getItem('minRowsWide'), 10) || 6;
  26. let minRowsNormal = parseInt(sessionStorage.getItem('minRowsNormal'), 10) || 3;
  27. let maxRowsNormal = parseInt(sessionStorage.getItem('maxRowsNormal'), 10) || 8;
  28.  
  29. // custom trusted types handling
  30. let needsTrustedHTML = false;
  31. const passThroughFunc = (string, sink) => string;
  32. const TTPName = "geminiStylePolicy";
  33. let TP = {
  34. createHTML: passThroughFunc,
  35. createScript: passThroughFunc,
  36. createScriptURL: passThroughFunc,
  37. };
  38.  
  39. try {
  40. if (
  41. typeof window.isSecureContext !== "undefined" &&
  42. window.isSecureContext
  43. ) {
  44. if (window.trustedTypes && window.trustedTypes.createPolicy) {
  45. if (trustedTypes.defaultPolicy) {
  46. TP = trustedTypes.defaultPolicy;
  47. } else {
  48. TP = window.trustedTypes.createPolicy(TTPName, TP);
  49. }
  50. needsTrustedHTML = true;
  51. }
  52. }
  53. } catch (e) {
  54. // log trusted types initialization error
  55. console.error("Error initializing Trusted Types:", e);
  56. }
  57.  
  58. function trustedHTML(string) {
  59. try {
  60. return needsTrustedHTML ? TP.createHTML(string) : string;
  61. } catch (error) {
  62. console.error("Error in trustedHTML:", error, "Original string:", string);
  63. // fallback to returning the original string (unsafe, but allows debugging)
  64. return string;
  65. }
  66. }
  67.  
  68. function getNonce() {
  69. const cspHeader = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
  70. if (!cspHeader) return null;
  71. const cspContent = cspHeader.content;
  72. const nonceMatch = cspContent.match(/nonce-(.+?)[';]/);
  73. return nonceMatch ? nonceMatch[1] : null;
  74. }
  75.  
  76. function resizeStuff() {
  77. // directly set max-width
  78. const elementsToSetMaxWidth = document.querySelectorAll(
  79. 'div.bottom-container, ' +
  80. 'div.input-area-container, ' +
  81. 'div.conversation-container, ' +
  82. '.text-input-field'
  83. );
  84. elementsToSetMaxWidth.forEach((element) => {
  85. element.style.maxWidth = wideModeEnabled ? '100%' : '';
  86. });
  87.  
  88. // set --textarea-max-rows dynamically based on wideModeEnabled
  89. document.documentElement.style.setProperty('--textarea-max-rows', wideModeEnabled ? maxRowsWide : maxRowsNormal);
  90. document.documentElement.style.setProperty('--textarea-min-rows', wideModeEnabled ? Math.max(3, minRowsWide) : Math.max(3, minRowsNormal));
  91.  
  92. // find and adjust rich-textarea .ql-editor
  93. const qlEditors = document.querySelectorAll('rich-textarea .ql-editor');
  94. qlEditors.forEach((editor) => {
  95. // Set --textarea-max-rows directly on the rich-textarea element
  96. editor.style.setProperty(
  97. '--textarea-max-rows',
  98. wideModeEnabled ? maxRowsWide : maxRowsNormal
  99. );
  100.  
  101. editor.style.minHeight = 'calc((var(--textarea-min-rows, 0)) * 24px)';
  102. editor.style.maxHeight = 'calc((var(--textarea-max-rows, 0)) * 24px)';
  103. });
  104. }
  105.  
  106. function toggleWideMode() {
  107. wideModeEnabled = !wideModeEnabled;
  108. resizeStuff();
  109. updateButtonIcon(); // update the icon
  110. }
  111.  
  112. function windowSizeUp() {
  113. const preFullscreenElements = document.querySelectorAll('.pre-fullscreen');
  114. if (preFullscreenElements.length > 0 && wideModeEnabled) {
  115. maxRowsWide++; // increase max rows for wide mode if in fullscreen
  116. sessionStorage.setItem('maxRowsWide', maxRowsWide); // store in sessionStorage
  117. } else if (preFullscreenElements.length > 0 && !wideModeEnabled) {
  118. maxRowsNormal++; // increase max rows for normal mode if in fullscreen
  119. sessionStorage.setItem('maxRowsNormal', maxRowsNormal);
  120. } else if (!wideModeEnabled) {
  121. minRowsNormal++; // increase min rows for normal mode if not in fullscreen
  122. sessionStorage.setItem('minRowsNormal', minRowsNormal);
  123. } else {
  124. minRowsWide++; // increase min rows for wide mode if not in fullscreen
  125. sessionStorage.setItem('minRowsWide', minRowsWide);
  126. }
  127. resizeStuff(); // reapply styles to reflect the changes
  128. }
  129.  
  130. function windowSizeDown() {
  131. const preFullscreenElements = document.querySelectorAll('.pre-fullscreen');
  132. if (preFullscreenElements.length > 0 && wideModeEnabled) {
  133. maxRowsWide = Math.max(minRowsWide, maxRowsWide - 1); // decrease max rows for wide mode if in fullscreen (but not below minRowsWide)
  134. sessionStorage.setItem('maxRowsWide', maxRowsWide); // store in sessionStorage
  135.  
  136. } else if (preFullscreenElements.length > 0 && !wideModeEnabled) {
  137. maxRowsNormal = Math.max(minRowsNormal, maxRowsNormal - 1); // decrease max rows for normal mode if in fullscreen (but not below minRowsNormal)
  138. sessionStorage.setItem('maxRowsNormal', maxRowsNormal);
  139.  
  140. } else if (!wideModeEnabled) {
  141. minRowsNormal = Math.max(1, minRowsNormal - 1); // decrease min rows for normal mode if not in fullscreen (but not below 1)
  142. sessionStorage.setItem('minRowsNormal', minRowsNormal);
  143.  
  144. } else {
  145. minRowsWide = Math.max(1, minRowsWide - 1); // decrease min rows for wide mode if not in fullscreen (but not below 1)
  146. sessionStorage.setItem('minRowsWide', minRowsWide);
  147. }
  148. resizeStuff(); // reapply styles to reflect the changes
  149. }
  150.  
  151. // modified addGlobalStyle using CSSStyleSheet (with fallback)
  152. function addGlobalStyle(css) {
  153. if (window.CSSStyleSheet && CSSStyleSheet.prototype.replaceSync) {
  154. const sheet = new CSSStyleSheet();
  155. sheet.replaceSync(trustedHTML(css));
  156. document.adoptedStyleSheets = [sheet];
  157. } else {
  158. let head = document.head || document.getElementsByTagName('head')[0];
  159. if (!head) return;
  160. let style = document.createElement('style');
  161. style.type = 'text/css';
  162. style.textContent = trustedHTML(css);
  163. head.appendChild(style);
  164. }
  165. }
  166.  
  167. // function to create and insert the button (with error handling)
  168. function createToggleButton() {
  169. try {
  170. const inputButtonsWrapperBottom = document.querySelector('.input-buttons-wrapper-bottom');
  171. const inputButtonsWrapperTop = document.querySelector('.input-buttons-wrapper-top');
  172.  
  173. if (!inputButtonsWrapperBottom || !inputButtonsWrapperTop) {
  174. console.log("One or both wrappers not found. Retrying...");
  175. setTimeout(createToggleButton, 100); // Retry after a delay
  176. return; // Exit the function if either wrapper is not found
  177. }
  178.  
  179. // Check for existing buttons using a unique identifier (e.g., aria-label)
  180. if (inputButtonsWrapperTop.querySelector('[aria-label="Toggle Wide Mode"]') ||
  181. inputButtonsWrapperBottom.querySelector('[aria-label="Window Size Up"]') ||
  182. inputButtonsWrapperBottom.querySelector('[aria-label="Window Size Down"]')) {
  183. console.log("Buttons already exist. Skipping creation.");
  184. return; // Exit the function if any button already exists
  185. }
  186.  
  187. // Create the buttons using Trusted Types
  188. const toggleButtonHTML = trustedHTML(`
  189. <div class="speech-dictation-mic-button ng-star-inserted">
  190. <button data-node-type="speech_dictation_mic_button" maticonsuffix="" mat-icon-button=""
  191. mattooltip="Wide Mode" aria-label="Toggle Wide Mode"
  192. 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">
  193. <span class="mat-mdc-button-persistent-ripple mdc-icon-button__ripple"></span>
  194. <div>
  195. <span class="mat-icon notranslate google-symbols mat-icon-no-color" id="wide_screen_image" data-mat-iic class="material-symbols-outlined"></span>
  196. </div>
  197. <span class="mat-mdc-focus-indicator"></span>
  198. <span class="mat-mdc-button-touch-target"></span>
  199. </button>
  200. </div>
  201. `);
  202.  
  203. const SizeButtonsHTML = trustedHTML(`
  204. <div class="speech-dictation-mic-button ng-star-inserted">
  205. <button data-node-type="speech_dictation_mic_button" maticonsuffix="" mat-icon-button=""
  206. mattooltip="Size Down" aria-label="Window Size Down"
  207. 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">
  208. <span class="mat-mdc-button-persistent-ripple mdc-icon-button__ripple"></span>
  209. <div>
  210. <span class="mat-icon notranslate google-symbols mat-icon-no-color" data-mat-iic class="material-symbols-outlined">keyboard_arrow_down</span>
  211. </div>
  212. <span class="mat-mdc-focus-indicator"></span>
  213. <span class="mat-mdc-button-touch-target"></span>
  214. </button>
  215. </div>
  216. <div class="speech-dictation-mic-button ng-star-inserted">
  217. <button data-node-type="speech_dictation_mic_button" maticonsuffix="" mat-icon-button=""
  218. mattooltip="Size Up" aria-label="Window Size Up"
  219. 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">
  220. <span class="mat-mdc-button-persistent-ripple mdc-icon-button__ripple"></span>
  221. <div>
  222. <span class="mat-icon notranslate google-symbols mat-icon-no-color" data-mat-iic class="material-symbols-outlined">keyboard_arrow_up</span>
  223. </div>
  224. <span class="mat-mdc-focus-indicator"></span>
  225. <span class="mat-mdc-button-touch-target"></span>
  226. </button>
  227. </div>
  228. `);
  229.  
  230. // use insertAdjacentHTML to add the buttons
  231. inputButtonsWrapperTop.insertAdjacentHTML('afterbegin', toggleButtonHTML);
  232. inputButtonsWrapperBottom.insertAdjacentHTML('afterend', SizeButtonsHTML);
  233.  
  234. // add click event listeners
  235. const toggleButton = document.querySelector('[aria-label="Toggle Wide Mode"]');
  236. const sizeUpButton = document.querySelector('[aria-label="Window Size Up"]');
  237. const sizeDownButton = document.querySelector('[aria-label="Window Size Down"]');
  238.  
  239. toggleButton.addEventListener('click', toggleWideMode);
  240. sizeUpButton.addEventListener('click', windowSizeUp);
  241. sizeDownButton.addEventListener('click', windowSizeDown);
  242.  
  243. buttonCreated = true;
  244. console.log('Buttons created successfully! We\'re done.');
  245. } catch (error) {
  246. console.error("Error in createToggleButton:", error);
  247. }
  248. }
  249.  
  250. // have to set pre-fullscreen globally as the elements don't exist when the page loads
  251. addGlobalStyle(trustedHTML(`
  252. div .pre-fullscreen {
  253. height: auto !important;
  254. }
  255. div .input-buttons-wrapper-top {
  256. right: 8px !important;
  257. }
  258. div .isFullscreen {
  259. max-height: 100% !important;
  260. }
  261. `));
  262.  
  263. // function to update the button icon based on wideModeEnabled
  264. function updateButtonIcon() {
  265. const iconSpan = document.getElementById("wide_screen_image");
  266. if (iconSpan) {
  267. iconSpan.textContent = wideModeEnabled ? 'remove' : 'add';
  268. }
  269. // Store the updated state in localStorage
  270. localStorage.setItem('geminiWideModeEnabled', wideModeEnabled);
  271. }
  272.  
  273. // function to check if page is loaded and ready for our button
  274. function checkIfPageReadyForButton() {
  275. if (document.readyState === 'complete' && !buttonCreated) {
  276. pageReady = true;
  277. console.log('Page is ready. Now make the button.');
  278. createToggleButton();
  279. }
  280. }
  281.  
  282. // callback function for the MutationObserver
  283. function mutationObserverCallback() {
  284. checkIfPageReadyForButton(); // check if button needs to be created
  285. updateButtonIcon();
  286. resizeStuff(); // apply or update styles
  287. }
  288.  
  289. // initial check and then MutationObserver to handle potential changes
  290. checkIfPageReadyForButton();
  291.  
  292. const observer = new MutationObserver(mutationObserverCallback);
  293. observer.observe(document.body, { childList: true, subtree: true });
  294. })();

QingJ © 2025

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