Greasy Fork 支持简体中文。

YouTube Alchemy

Toolkit for YouTube. Settings panels for easy customization. Features include: export transcripts to LLMs or download it as text files, prevent autoplay, hide shorts, disable play on hover, square design, auto theater mode, auto close chat window, adjust number of videos per row, display remaining time under a video adjusted for playback speed, persistent progress bar with chapter markers and SponsorBlock support, links in the header, and change or hide various ui elements.

目前為 2025-02-06 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name YouTube Alchemy
  3. // @description Toolkit for YouTube. Settings panels for easy customization. Features include: export transcripts to LLMs or download it as text files, prevent autoplay, hide shorts, disable play on hover, square design, auto theater mode, auto close chat window, adjust number of videos per row, display remaining time under a video adjusted for playback speed, persistent progress bar with chapter markers and SponsorBlock support, links in the header, and change or hide various ui elements.
  4. // @author Tim Macy
  5. // @license GNU AFFERO GENERAL PUBLIC LICENSE-3.0
  6. // @version 7.5.1
  7. // @namespace TimMacy.YouTubeAlchemy
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  9. // @match https://*.youtube.com/*
  10. // @grant GM.setValue
  11. // @grant GM.getValue
  12. // @run-at document-start
  13. // @noframes
  14. // @homepageURL https://github.com/TimMacy/YouTubeAlchemy
  15. // @supportURL https://github.com/TimMacy/YouTubeAlchemy/issues
  16. // ==/UserScript==
  17.  
  18. (async function() {
  19. 'use strict';
  20. // CSS
  21. const styleSheet = document.createElement('style');
  22. styleSheet.textContent = `
  23. /* main CSS */
  24. .overlay {
  25. position: fixed;
  26. z-index: 2053;
  27. left: 0;
  28. top: 0;
  29. width: 100%;
  30. height: 100%;
  31. display: flex;
  32. align-items: center;
  33. justify-content: center;
  34. background-color: rgba(0,0,0,0.5);
  35. backdrop-filter: blur(5px);
  36. }
  37.  
  38. .modal-content {
  39. z-index: 2077;
  40. background-color: rgba(17, 17, 17, 0.8);
  41. padding: 20px 0 20px 20px;
  42. border: 1px solid rgba(255, 255, 255, 0.25);
  43. border-radius: 8px;
  44. width: 420px;
  45. max-height: 90vh;
  46. font-family: "Roboto","Arial",sans-serif;
  47. font-size: 9px;
  48. line-height: 1.2;
  49. color: white;
  50. text-rendering: optimizeLegibility !important;
  51. -webkit-font-smoothing: antialiased !important;
  52. -moz-osx-font-smoothing: grayscale !important;
  53. }
  54.  
  55. #yt-transcript-settings-form {
  56. max-height: calc(90vh - 40px);
  57. overflow-y: auto;
  58. padding-right: 20px;
  59. }
  60.  
  61. .notification {
  62. background:hsl(0,0%,7%);
  63. padding: 20px 30px;
  64. border-radius: 8px;
  65. border: 1px solid hsl(0,0%,18.82%);
  66. max-width: 80%;
  67. text-align: center;
  68. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  69. font-size: 16px;
  70. color: white;
  71. -webkit-user-select: none;
  72. -moz-user-select: none;
  73. -ms-user-select: none;
  74. user-select: none;
  75. }
  76.  
  77. .header {
  78. margin: 0 20px 20px 0;
  79. padding: 0;
  80. border: 0;
  81. text-align:center;
  82. text-decoration: none;
  83. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  84. font-size: 2.5em;
  85. line-height: 1em;
  86. font-weight: 700;
  87. text-overflow: ellipsis;
  88. white-space: normal;
  89. text-shadow: 0 0 20px black;
  90. cursor: pointer;
  91. background: transparent;
  92. display: block;
  93. background-image: linear-gradient(45deg, #FFFFFF, #F2F2F2, #E6E6E6, #D9D9D9, #CCCCCC);
  94. -webkit-background-clip: text;
  95. background-clip: text;
  96. color: transparent;
  97. transition: color .3s ease-in-out;
  98. -webkit-user-select: none;
  99. -moz-user-select: none;
  100. -ms-user-select: none;
  101. user-select: none;
  102. }
  103.  
  104. .header:hover { color: white; }
  105.  
  106. .label-style-settings {
  107. display: block;
  108. margin-bottom: 5px;
  109. font-family: "Roboto","Arial",sans-serif;
  110. font-size: 1.4em;
  111. line-height: 1.5em;
  112. font-weight: 500;
  113. }
  114.  
  115. .label-NotebookLM { color: hsl(134, 61%, 40%); }
  116. .label-ChatGPT { color: hsl(217, 91%, 59%); }
  117. .label-download { color: hsl(359, 88%, 57%); }
  118. .label-settings { color: hsl(0, 0%, 100%); }
  119. .input-field-targetNotebookLMUrl:focus { border: 1px solid hsl(134, 61%, 40%); }
  120. .input-field-targetChatGPTUrl:focus { border: 1px solid hsl(217, 91%, 59%); }
  121. .buttonIconNotebookLM-input-field:focus { border: 1px solid hsl(134, 61%, 40%); }
  122. .buttonIconChatGPT-input-field:focus { border: 1px solid hsl(217, 91%, 59%); }
  123. .buttonIconDownload-input-field:focus { border: 1px solid hsl(359, 88%, 57%); }
  124.  
  125. .buttonIconSettings-input-field:focus,
  126. .links-header-container input:focus,
  127. .sidebar-container input:focus,
  128. #custom-css-form .select-file-naming:focus,
  129. #custom-css-form .dropdown-list {
  130. border: 1px solid hsl(0, 0%, 100%);
  131. }
  132.  
  133. .input-field-targetNotebookLMUrl:hover,
  134. .input-field-targetChatGPTUrl:hover,
  135. .buttonIconNotebookLM-input-field:hover,
  136. .buttonIconChatGPT-input-field:hover,
  137. .buttonIconDownload-input-field:hover,
  138. .buttonIconSettings-input-field:hover,
  139. .select-file-naming:hover,
  140. .input-field-url:hover,
  141. .chatgpt-prompt-textarea:hover
  142. { background-color: hsl(0, 0%, 10.37%); }
  143.  
  144. .btn-style-settings {
  145. padding: 5px 10px;
  146. cursor: pointer;
  147. color: whitesmoke;
  148. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  149. font-size: 1.4em;
  150. line-height: 1.5em;
  151. font-weight: 400;
  152. background-color: hsl(0, 0%, 7%);
  153. border: 1px solid hsl(0, 0%, 18.82%);
  154. border-radius: 2px;
  155. transition: all 0.2s ease-out;
  156. }
  157.  
  158. .btn-style-settings:hover { color: white; background-color: hsl(0, 0%, 25%); border-color: transparent; }
  159. .btn-style-settings:active { color: white; background-color: hsl(0, 0%, 35%); border-color: hsl(0, 0%, 45%); }
  160. .button-icons { display: block; font-family: "Roboto","Arial",sans-serif; font-size: 1.4em; line-height: 1.5em; font-weight: 500; }
  161. .icons-container { display: flex; justify-content: space-between; margin-bottom: 20px; }
  162. .container-button { display: flex; flex-direction: column; align-items: center; margin: 5px 0 0 0; }
  163.  
  164. .button-icons.features-text {
  165. margin: 10px 0 -5px 0;
  166. font-size: 1.7em;
  167. display: flex;
  168. justify-content: center;
  169. }
  170.  
  171. .container-button-input {
  172. width: 80px;
  173. padding: 8px;
  174. text-align: center;
  175. color: ghostwhite;
  176. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  177. font-size: 2em;
  178. line-height: 1.5em;
  179. font-weight: 400;
  180. transition: all .5s ease-in-out;
  181. outline: none;
  182. background-color: hsl(0,0%,7%);
  183. border: 1px solid hsl(0,0%,18.82%);
  184. border-radius: 1px;
  185. box-sizing: border-box;
  186. }
  187.  
  188. .container-button-label {
  189. margin-top: 5px;
  190. text-align: center;
  191. font-family: "Roboto","Arial",sans-serif;
  192. font-size: 1.4em;
  193. line-height: 1.5em;
  194. font-weight: 500;
  195. }
  196.  
  197. .container-button-input:focus { color: white; background-color: hsl(0, 0%, 10.37%); border-radius: 2px; }
  198. .spacer-5 { height: 5px; }
  199. .spacer-10 { height: 10px; }
  200. .spacer-15 { height: 15px; }
  201. .spacer-20 { height: 20px; }
  202.  
  203. .copyright {
  204. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  205. font-size: 1.4em;
  206. line-height: 1.5em;
  207. font-weight: 500;
  208. color: white;
  209. text-decoration: none;
  210. transition: color 0.2s ease-in-out;
  211. }
  212.  
  213. .copyright:hover { color: #369eff; }
  214. .url-container { margin-bottom: 10px; }
  215.  
  216. .input-field-url {
  217. width: 100%;
  218. padding: 8px;
  219. color: ghostwhite;
  220. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  221. font-size: 1.4em;
  222. line-height: 1.5em;
  223. font-weight: 400;
  224. transition: all .5s ease-in-out;
  225. outline: none;
  226. background-color:hsl(0,0%,7%);
  227. border: 1px solid hsl(0,0%,18.82%);
  228. border-radius: 1px;
  229. box-sizing: border-box;
  230. }
  231.  
  232. .input-field-url:focus { color: white; background-color: hsl(0, 0%, 10.37%); border-radius: 2px; }
  233. .file-naming-container { position: relative; margin-bottom: 20px; }
  234.  
  235. .select-file-naming {
  236. width: 100%;
  237. padding: 8px;
  238. cursor:pointer;
  239. color: ghostwhite;
  240. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  241. font-size: 1.4em;
  242. line-height: 1.5em;
  243. font-weight: 400;
  244. transition: all .5s ease-in-out;
  245. outline: none;
  246. appearance: none;
  247. -webkit-appearance: none;
  248. background-color:hsl(0,0%,7%);
  249. border: 1px solid hsl(0,0%,18.82%);
  250. border-radius: 1px;
  251. box-sizing: border-box;
  252.  
  253. background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true" style="pointer-events: none; display: inherit; width: 100%; height: 100%;" fill="ghostwhite"%3E%3Cpath d="m18 9.28-6.35 6.35-6.37-6.35.72-.71 5.64 5.65 5.65-5.65z"%3E%3C/path%3E%3C/svg%3E');
  254. background-repeat: no-repeat;
  255. background-position: right 10px center;
  256. background-size: 20px;
  257.  
  258. -webkit-user-select: none;
  259. -moz-user-select: none;
  260. -ms-user-select: none;]
  261. user-select: none;
  262. }
  263.  
  264. .hidden-select {
  265. position: absolute;
  266. visibility: hidden;
  267. opacity: 0;
  268. pointer-events: none;
  269. width: 100%;
  270. height: 100%;
  271. top: 0;
  272. left: 0;
  273. }
  274.  
  275. .dropdown-list {
  276. visibility: hidden;
  277. opacity: 0;
  278. position: absolute;
  279. z-index: 2100;
  280. top: 115%;
  281. left: 0;
  282. width: 100%;
  283. max-height: 200px;
  284. overflow-y: auto;
  285. background-color:hsl(0,0%,7%);
  286. border: 1px solid hsl(359,88%,57%);
  287. border-radius: 1px 1px 8px 8px;
  288. box-sizing: border-box;
  289. transition: opacity .5s ease-in-out, transform .5s ease-in-out;
  290. transform: translateY(-10px);
  291. }
  292.  
  293. .dropdown-list.show {
  294. visibility: visible;
  295. opacity: 1;
  296. transform: translateY(0);
  297. }
  298.  
  299. .dropdown-item {
  300. padding: 15px;
  301. cursor: pointer;
  302. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  303. font-size: 1.47em;
  304. line-height: 1em;
  305. font-weight: 400;
  306. color: lightgray;
  307. position: relative;
  308. transition: background-color .3s;
  309. padding-left: 1.6em;
  310. }
  311.  
  312. .dropdown-item:hover {
  313. color: ghostwhite;
  314. background-color: rgba(255, 255, 255, .05);
  315. }
  316.  
  317. .dropdown-item:hover::before {
  318. color: ghostwhite;
  319. font-weight: 600;
  320. }
  321.  
  322. .dropdown-item-selected {
  323. color: hsl(359,88%,57%);
  324. font-weight: 600;
  325. }
  326.  
  327. .dropdown-item-selected::before {
  328. content: '✓';
  329. position: absolute;
  330. left: 6px;
  331. color: hsl(359,88%,57%);
  332. }
  333.  
  334. .select-file-naming:focus {
  335. color: white;
  336. background-color: hsl(0, 0%, 10.37%);
  337. border-radius: 2px;
  338. border-color: hsl(359, 88%, 57%);
  339. }
  340.  
  341. .checkbox-label,
  342. .number-input-label span {
  343. display: flex;
  344. align-items: center;
  345. font-family: "Roboto","Arial",sans-serif;
  346. font-size: 1.4em;
  347. line-height: 1.5em;
  348. font-weight: 500;
  349. cursor: pointer;
  350. text-decoration: none;
  351. -webkit-user-select: none;
  352. -moz-user-select: none;
  353. -ms-user-select: none;
  354. user-select: none;
  355. }
  356.  
  357. .checkbox-container { margin-bottom: 5px; }
  358. .checkbox-label:hover { text-decoration: underline; }
  359. .checkbox-field { margin-right: 10px; }
  360.  
  361. .chrome-info {
  362. color: rgba(175, 175, 175, .9);
  363. font-family: "Roboto","Arial",sans-serif;
  364. font-size: 1.2em;
  365. line-height: 1.5em;
  366. font-weight: 400;
  367. display: block;
  368. margin:-5px 0px 5px 24px;
  369. pointer-events: none;
  370. cursor: default;
  371. }
  372.  
  373. .extra-button-container {
  374. display: flex;
  375. justify-content: center;
  376. gap: 5%;
  377. margin: 20px 0;
  378. }
  379.  
  380. .chatgpt-prompt-textarea {
  381. width: 100%;
  382. padding: 8px;
  383. height: 65px;
  384. transition: all .5s ease-in-out;
  385. outline: none;
  386. resize: none;
  387. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  388. font-size: 1.4em;
  389. line-height: 1.5em;
  390. font-weight: 400;
  391. color: ghostwhite;
  392. background-color:hsl(0,0%,7%);
  393. border: 1px solid hsl(0,0%,18.82%);
  394. border-radius: 1px;
  395. box-sizing: border-box;
  396. }
  397.  
  398. .chatgpt-prompt-textarea:focus {
  399. height: 450px;
  400. color: white;
  401. background-color: hsl(0, 0%, 10.37%);
  402. border: 1px solid hsl(217, 91%, 59%);
  403. border-radius: 2px;
  404. }
  405.  
  406. .button-container-end {
  407. display: flex;
  408. flex-direction: column;
  409. gap: 10px;
  410. margin-top: 20px;
  411. padding-top: 10px;
  412. border: none;
  413. border-top: solid 1px transparent;
  414. border-image: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, .5), rgba(255, 255, 255, 0));
  415. border-image-slice: 1;
  416. -webkit-user-select: none;
  417. -moz-user-select: none;
  418. -ms-user-select: none;
  419. user-select: none;
  420. }
  421.  
  422. .button-container-backup {
  423. display: flex;
  424. justify-content: end;
  425. gap: 23.5px;
  426. }
  427.  
  428. .button-container-settings {
  429. display: flex;
  430. align-items: center;
  431. justify-content: end;
  432. gap: 10px;
  433.  
  434. }
  435.  
  436. .button-wrapper {
  437. position: relative;
  438. margin-right: 8px;
  439. display: flex;
  440. background-color: transparent;
  441. text-rendering: optimizeLegibility !important;
  442. -webkit-font-smoothing: antialiased !important;
  443. -moz-osx-font-smoothing: grayscale !important;
  444. }
  445.  
  446. .button-wrapper:not(:has(.button-style-settings)):hover { background-color: rgba(255, 255, 255, 0.1); border-radius: 24px; }
  447. .button-wrapper:not(:has(.button-style-settings)):active { background-color: rgba(255, 255, 255, 0.2); border-radius: 24px; }
  448.  
  449. .button-style {
  450. width: 40px;
  451. height: 40px;
  452. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  453. font-size: 24px;
  454. display: inline-block;
  455. position: relative;
  456. box-sizing: border-box;
  457. vertical-align: middle;
  458. color: white;
  459. outline: none;
  460. background: transparent;
  461. margin: 0;
  462. border: none;
  463. padding: 0;
  464. cursor: pointer;
  465. -webkit-tap-highlight-color: rgba(0,0,0,0);
  466. -webkit-tap-highlight-color: transparent;
  467. }
  468.  
  469. .button-style-settings { width: 10px; color: rgb(170, 170, 170); }
  470. .button-style-settings:hover { color: white; }
  471.  
  472. .button-tooltip {
  473. visibility: hidden;
  474. background-color: black;
  475. color: white;
  476. text-align: center;
  477. border-radius: 2px;
  478. padding: 6px 8px;
  479. position: absolute;
  480. z-index: 2053;
  481. top: 120%;
  482. left: 50%;
  483. transform: translateX(-50%);
  484. opacity: 0;
  485. white-space: nowrap;
  486. font-size: 12px;
  487. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  488. border-top: solid 1px white;
  489. border-bottom: solid 1px black;
  490. border-left: solid 1px transparent;
  491. border-right: solid 1px transparent;
  492. border-image: linear-gradient(to bottom, white, black);
  493. border-image-slice: 1;
  494. }
  495.  
  496. .button-tooltip-arrow {
  497. position: absolute;
  498. top: -5px;
  499. left: 50%;
  500. transform: translateX(-50%) rotate(45deg);
  501. width: 10px;
  502. height: 10px;
  503. background: linear-gradient(135deg, white 0%, white 50%, black 50%, black 100%);
  504. z-index: -1;
  505. }
  506.  
  507. .remaining-time-container {
  508. position: relative;
  509. display: block;
  510. height: 0;
  511. top: 4px;
  512. text-align: right;
  513. font-family: "Roboto", "Arial", sans-serif;
  514. font-size: 1.4rem;
  515. font-weight: 500;
  516. line-height: 1em;
  517. color: ghostwhite;
  518. pointer-events: auto;
  519. cursor: auto;
  520. text-rendering: optimizeLegibility !important;
  521. -webkit-font-smoothing: antialiased !important;
  522. -moz-osx-font-smoothing: grayscale !important;
  523. }
  524.  
  525. .remaining-time-container.live,
  526. #movie_player .remaining-time-container.live {
  527. display: none;
  528. pointer-events: none;
  529. cursor: default;
  530. }
  531.  
  532. #movie_player .remaining-time-container {
  533. position: absolute;
  534. z-index: 2053;
  535. bottom: 15px;
  536. left: 50%;
  537. transform: translateX(-50%);
  538. font-weight: 800 !important;
  539. font-size: 109%;
  540. display: inline-block;
  541. vertical-align: top;
  542. white-space: nowrap;
  543. line-height: 53px;
  544. color: #ddd;
  545. text-shadow: black 0 0 3px !important;
  546. }
  547.  
  548. .transcript-preload {
  549. position: fixed !important;
  550. top: var(--ytd-toolbar-height) !important;
  551. left: 50% !important;
  552. transform: translateX(-50%) !important;
  553. z-index: -1 !important;
  554. opacity: 0 !important;
  555. pointer-events: none !important;
  556. }
  557.  
  558. .notification-error {
  559. z-index: 2053;
  560. box-shadow: none;
  561. text-decoration: none;
  562. display: inline-block;
  563. background-color: hsl(0, 0%, 7%);
  564. padding: 10px 12px;
  565. margin: 0 8px;
  566. border: 1px solid hsl(0, 0%, 18.82%);
  567. border-radius: 5px;
  568. pointer-events: none;
  569. cursor: default;
  570. font-family: 'Roboto', Arial, sans-serif;
  571. color: rgba(255, 255, 255, 0.5);
  572. text-align: center;
  573. font-weight: 500;
  574. font-size: 14px;
  575. text-rendering: optimizeLegibility !important;
  576. -webkit-font-smoothing: antialiased !important;
  577. -moz-osx-font-smoothing: grayscale !important;
  578. }
  579.  
  580. #CentAnni-playback-speed-popup {
  581. position: fixed;
  582. top: var(--ytd-masthead-height,var(--ytd-toolbar-height));
  583. left: 50%;
  584. transform: translateX(-50%);
  585. padding: 8px 16px;
  586. background:hsl(0,0%,7%);
  587. border-radius: 2px;
  588. border: 1px solid hsl(0,0%,18.82%);
  589. color: whitesmoke;
  590. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  591. font-size: 2.3rem;
  592. font-weight: 600;
  593. text-align: center;
  594. z-index: 2077;
  595. transition: opacity .3s ease;
  596. opacity: 0;
  597. -webkit-user-select: none;
  598. -moz-user-select: none;
  599. -ms-user-select: none;
  600. user-select: none;
  601. text-rendering: optimizeLegibility !important;
  602. -webkit-font-smoothing: antialiased !important;
  603. -moz-osx-font-smoothing: grayscale !important;
  604. }
  605.  
  606. #CentAnni-playback-speed-popup.active {
  607. opacity: 1;
  608. }
  609.  
  610. .loading span::before {
  611. content: "Transcript Is Loading";
  612. position: absolute;
  613. inset: initial;
  614. color: rgba(255, 250, 250, .86);
  615. opacity: 0;
  616. -webkit-animation: pulse 1.5s infinite;
  617. animation: pulse 1.5s infinite;
  618. text-rendering: optimizeLegibility !important;
  619. -webkit-font-smoothing: antialiased !important;
  620. -moz-osx-font-smoothing: grayscale !important;
  621. }
  622.  
  623. @-webkit-keyframes pulse {
  624. 0% { opacity: 0; }
  625. 50% { opacity: .71; }
  626. 100% { opacity: 0; }
  627. }
  628.  
  629. @keyframes pulse {
  630. 0% { opacity: 0; }
  631. 50% { opacity: .71; }
  632. 100% { opacity: 0; }
  633. }
  634.  
  635. .buttons-left {
  636. font-family: "Roboto", "Arial", sans-serif;
  637. font-size: 14px;
  638. font-weight: 500;
  639. line-height: 1em;
  640. display: inline-block;
  641. position: relative;
  642. color: ghostwhite;
  643. text-decoration: none;
  644. cursor: pointer;
  645. margin: 0 8px;
  646. outline: none;
  647. background: transparent;
  648. border: none;
  649. text-align: center;
  650. text-rendering: optimizeLegibility !important;
  651. -webkit-font-smoothing: antialiased !important;
  652. -moz-osx-font-smoothing: grayscale !important;
  653. }
  654.  
  655. .buttons-left:hover { color: #ff0000 !important; }
  656. .buttons-left:active { color:rgb(200, 25, 25) !important; }
  657.  
  658. .sub-panel-overlay {
  659. position: fixed;
  660. z-index: 2100;
  661. left: 0;
  662. top: 0;
  663. width: 100%;
  664. height: 100%;
  665. display: none;
  666. background-color: rgba(0,0,0,0.5);
  667. justify-content: center;
  668. align-items: center;
  669. backdrop-filter: blur(1px);
  670. }
  671.  
  672. .sub-panel-overlay.active {
  673. display: flex;
  674. }
  675.  
  676. .sub-panel {
  677. z-index: 2177;
  678. background-color: rgba(17, 17, 17, 0.8);
  679. padding: 20px;
  680. border: 1px solid rgba(255, 255, 255, 0.25);
  681. border-radius: 8px;
  682. width: 50vw;
  683. max-width: 70vw;
  684. max-height: 90vh;
  685. position: relative;
  686. display: flex;
  687. flex-direction: column;
  688. overflow-y: auto;
  689. color: whitesmoke;
  690. text-rendering: optimizeLegibility !important;
  691. -webkit-font-smoothing: antialiased !important;
  692. -moz-osx-font-smoothing: grayscale !important;
  693. }
  694.  
  695. .sub-panel button {
  696. position: sticky;
  697. top: 0;
  698. align-self: flex-end;
  699. box-shadow: 0 0 20px 10px black;
  700. }
  701.  
  702. .sub-panel-header {
  703. margin: -24px 60px 20px 0px;
  704. padding: 0px 0px 0px 0px;
  705. border: 0;
  706. text-align: left;
  707. text-decoration: none;
  708. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  709. font-size: 2em;
  710. line-height: 1em;
  711. font-weight: 700;
  712. text-overflow: ellipsis;
  713. white-space: normal;
  714. text-shadow: 0 0 30px 20px black;
  715. cursor: auto;
  716. color: white;
  717. align-self: left;
  718. position: relative;
  719. z-index: 2180;
  720. }
  721.  
  722. #links-in-header-form .chrome-info {
  723. margin: -10px 80px 20px 0px;
  724. }
  725.  
  726. .links-header-container {
  727. display: flex;
  728. align-items: center;
  729. gap: 20px;
  730. }
  731.  
  732. .links-header-container label {
  733. color: whitesmoke;
  734. }
  735.  
  736. .links-header-container .url-container:first-child {
  737. flex: 1;
  738. }
  739.  
  740. .links-header-container .url-container:last-child {
  741. flex: 2;
  742. }
  743.  
  744. .sidebar-container {
  745. display: flex;
  746. align-items: center;
  747. margin: 10px 0 0 0;
  748. color: whitesmoke;
  749. justify-content: flex-start;
  750. gap: 20px;
  751. }
  752.  
  753. .sidebar-container .checkbox-container {
  754. margin-bottom: 0 !important;
  755. flex: 1;
  756. }
  757.  
  758. .sidebar-container .url-container {
  759. flex: 2;
  760. }
  761.  
  762. .playback-speed-container {
  763. display: flex;
  764. gap: 10px;
  765. margin: 10px 0;
  766. }
  767.  
  768. #custom-css-form .playback-speed-container .checkbox-container {
  769. max-width: calc(53% - 5px);
  770. margin: 0;
  771. align-content: center;
  772. flex: 0 0 auto;
  773. }
  774.  
  775. #custom-css-form .playback-speed-container .number-input-container {
  776. max-width: calc(47% - 5px);
  777. margin: 0;
  778. align-self: center;
  779. flex: 1 0 auto;
  780. }
  781.  
  782. #custom-css-form .playback-speed-container .number-input-container .number-input-field {
  783. width: 5ch;
  784. }
  785.  
  786. #custom-css-form .playback-speed-container .number-input-label {
  787. display: flex;
  788. }
  789.  
  790. #color-code-videos-form .checkbox-container { margin: 20px 0 0 0; }
  791. #color-code-videos-form .label-style-settings {margin: 0; }
  792. #color-code-videos-form > div.videos-old-container > span { margin: 0; }
  793. #color-code-videos-form .chrome-info { margin: -10px 80px 20px 0px; }
  794. #custom-css-form .checkbox-container { margin: 10px 0; }
  795.  
  796. #custom-css-form .file-naming-container {
  797. max-width: 90%;
  798. margin: 20px 0;
  799. display: flex;
  800. gap: 25px;
  801. align-content: center;
  802. }
  803.  
  804. #custom-css-form .label-style-settings {
  805. margin-bottom: 0;
  806. white-space: nowrap;
  807. align-content: center;
  808. }
  809.  
  810. #custom-css-form .dropdown-item-selected,
  811. #custom-css-form .dropdown-item-selected::before {
  812. color: hsl(0, 0%, 100%);
  813. }
  814.  
  815. input[type="range"] {
  816. -webkit-appearance: none;
  817. appearance: none;
  818. width: 100%;
  819. height: 6px;
  820. background: #ccc;
  821. border-radius: 5px;
  822. outline: none;
  823. }
  824.  
  825. input[type="range"]::-moz-range-thumb,
  826. input[type="range"]::-webkit-slider-thumb {
  827. -webkit-appearance: none;
  828. appearance: none;
  829. width: 16px;
  830. height: 16px;
  831. background: #007bff;
  832. border-radius: 50%;
  833. cursor: pointer;
  834. border: 2px solid #ffffff;
  835. }
  836.  
  837. input[type="range"]::-moz-range-track,
  838. input[type="range"]::-webkit-slider-runnable-track {
  839. background: #007bff;
  840. height: 6px;
  841. border-radius: 5px;
  842. }
  843.  
  844. .videos-old-container {
  845. display: flex;
  846. max-width: 90%;
  847. align-items: center;
  848. gap: 25px;
  849. margin: 20px 0;
  850. }
  851.  
  852. .slider-container {
  853. display: flex;
  854. align-items: center;
  855. gap: 10px;
  856. flex: 1;
  857. }
  858.  
  859. .slider-container input[type="range"] {
  860. flex: 1;
  861. }
  862.  
  863. .videos-age-container {
  864. display: flex;
  865. flex-direction: column;
  866. align-items: center;
  867. gap: 50px;
  868. }
  869.  
  870. .videos-age-row {
  871. display: flex;
  872. justify-content: flex-start;
  873. align-items: center;
  874. width: 100%;
  875. gap: 20px;
  876. margin: 0;
  877. }
  878.  
  879. .videos-age-row span {
  880. text-align: right;
  881. flex: 1;
  882. max-width: 50%;
  883. }
  884.  
  885. .videos-age-row input {
  886. flex: 1;
  887. margin: 0;
  888. padding: 0;
  889. max-width: 62px;
  890. height: 26px;
  891. cursor: pointer;
  892. background: none;
  893. border: none;
  894. box-shadow: none;
  895. appearance: none;
  896. -webkit-appearance: none;
  897. -moz-appearance: none;
  898. }
  899.  
  900. input[type="color"]::-webkit-color-swatch-wrapper {
  901. border: none;
  902. padding: 0;
  903. }
  904.  
  905. input[type="color"]::-webkit-color-swatch {
  906. border: none;
  907. }
  908.  
  909.  
  910. #custom-css-form .videos-age-row span {
  911. text-align: left;
  912. flex: initial;
  913. }
  914.  
  915. #custom-css-form .videos-age-row input {
  916. margin: 0 0 0 -3px;
  917. }
  918.  
  919. #custom-css-form .videos-age-row {
  920. gap: 10px;
  921. }
  922.  
  923. .number-input-container {
  924. margin: 10px 0;
  925. }
  926.  
  927. .number-input-field {
  928. width: 5ch;
  929. margin: 0 10px 0 0;
  930. align-items: center;
  931. font-size: 1.4em;
  932. line-height: 1.5em;
  933. font-weight: 700;
  934. cursor: auto;
  935. text-decoration: none;
  936. text-align: center;
  937. display: inline-block;
  938.  
  939. }
  940.  
  941. .number-input-label span {
  942. display: initial;
  943. cursor: auto;
  944. }
  945.  
  946. /* progress bar css */
  947. .CentAnni-progress-bar {
  948. #progress-bar-bar {
  949. width: 100%;
  950. height: 3px;
  951. background: rgba(255, 255, 255, 0.2);
  952. position: absolute;
  953. bottom: 0;
  954. opacity: 0;
  955. z-index: 50;
  956. }
  957.  
  958. #progress-bar-progress, #progress-bar-buffer {
  959. width: 100%;
  960. height: 3px;
  961. transform-origin: 0 0;
  962. position: absolute;
  963. }
  964.  
  965. #progress-bar-progress {
  966. background: var(--progressBarColor);
  967. filter: none;
  968. z-index: 1;
  969. }
  970.  
  971. .ytp-autohide .ytp-chrome-bottom .ytp-load-progress, .ytp-autohide .ytp-chrome-bottom .ytp-play-progress { display: none !important; }
  972. .ytp-autohide .ytp-chrome-bottom { opacity: 1 !important; display: block !important; }
  973. .ytp-autohide .ytp-chrome-bottom .ytp-chrome-controls { opacity: 0 !important; }
  974. .ad-interrupting #progress-bar-progress { background: transparent; }
  975. .ytp-ad-persistent-progress-bar-container { display: none; }
  976. #progress-bar-buffer { background: rgba(255, 255, 255, 0.4); }
  977.  
  978. .ytp-autohide #progress-bar-start.active,
  979. .ytp-autohide #progress-bar-bar.active,
  980. .ytp-autohide #progress-bar-end.active
  981. { opacity: 1; }
  982.  
  983. .ytp-autohide .ytp-chrome-bottom .ytp-progress-bar-container {
  984. bottom: 0px !important;
  985. opacity: 1 !important;
  986. height: 4px !important;
  987. transform: translateX(0px) !important;
  988. z-index: 100;
  989. }
  990.  
  991. .ytp-autohide .ytp-chrome-bottom .ytp-progress-bar,
  992. .ytp-autohide .ytp-chrome-bottom .ytp-progress-list {
  993. background: transparent !important;
  994. box-shadow: none !important;
  995. }
  996.  
  997. .ytp-autohide .ytp-chrome-bottom .previewbar {
  998. height: calc(100% + 1px) !important;
  999. bottom: -1px !important;
  1000. margin-bottom: 0px !important;
  1001. opacity: 1 !important;
  1002. border: none !important;
  1003. box-shadow: none !important;
  1004. will-change: opacity, transform !important;
  1005. }
  1006.  
  1007. .ytp-autohide .ytp-chrome-bottom .ytp-progress-bar-container:not(.active) .ytp-scrubber-container {
  1008. opacity: 0;
  1009. pointer-events: none;
  1010. }
  1011.  
  1012. #progress-bar-start, #progress-bar-end {
  1013. position: absolute;
  1014. height: 3px;
  1015. width: 12px;
  1016. bottom: 0;
  1017. z-index: 2077;
  1018. opacity: 0;
  1019. pointer-events: none;
  1020. }
  1021.  
  1022. :fullscreen #progress-bar-start, :fullscreen #progress-bar-end { width: 24px; }
  1023. :-webkit-full-screen #progress-bar-start, :-webkit-full-screen #progress-bar-end { width: 24px; }
  1024. .html5-video-player.ytp-fullscreen #progress-bar-start, .html5-video-player.ytp-fullscreen #progress-bar-end { width: 24px !important; }
  1025.  
  1026. #progress-bar-start {
  1027. left: 0;
  1028. background: var(--progressBarColor);
  1029. }
  1030.  
  1031. #progress-bar-end {
  1032. right: 0;
  1033. background: rgba(255, 255, 255, 0.2);
  1034. }
  1035. }
  1036.  
  1037. /* customCSS CSS */
  1038. html {
  1039. font-size: var(--fontSize) !important;
  1040. font-family: "Roboto", Arial, sans-serif;
  1041. }
  1042.  
  1043. .CentAnni-style-hide-default-sidebar {
  1044. ytd-mini-guide-renderer.ytd-app { display: none !important; }
  1045. ytd-app[mini-guide-visible] ytd-page-manager.ytd-app { margin-left: 0 !important; }
  1046. #guide-button.ytd-masthead { display: none !important; }
  1047. #contents.ytd-rich-grid-renderer { justify-content: center !important; }
  1048. ytd-browse[mini-guide-visible] ytd-playlist-header-renderer.ytd-browse, ytd-browse[mini-guide-visible] ytd-playlist-sidebar-renderer.ytd-browse, ytd-browse[mini-guide-visible] .page-header-sidebar.ytd-browse {
  1049. left: 0;
  1050. }
  1051. }
  1052.  
  1053. html #above-the-fold h1,
  1054. h1.ytd-watch-metadata,
  1055. #video-title {
  1056. text-transform: var(--textTransform) !important;
  1057. }
  1058.  
  1059. .CentAnni-style-full-title {
  1060. #video-title.ytd-rich-grid-media {
  1061. white-space: normal;
  1062. text-overflow: unset;
  1063. overflow: unset;
  1064. display: inline-block;
  1065. }
  1066.  
  1067. #video-title {
  1068. max-height: unset !important;
  1069. -webkit-line-clamp: unset !important;
  1070. }
  1071. }
  1072.  
  1073. ytd-compact-video-renderer ytd-thumbnail:has(ytd-thumbnail-overlay-resume-playback-renderer),
  1074. ytd-rich-item-renderer ytd-thumbnail:has(ytd-thumbnail-overlay-resume-playback-renderer),
  1075. ytd-thumbnail:has(ytd-thumbnail-overlay-resume-playback-renderer) {
  1076. opacity: var(--watchedOpacity);
  1077. }
  1078.  
  1079. ytd-search ytd-thumbnail:has(ytd-thumbnail-overlay-resume-playback-renderer) {
  1080. opacity: .8;
  1081. }
  1082.  
  1083. .ytd-page-manager[page-subtype="history"] {
  1084. ytd-thumbnail:has(ytd-thumbnail-overlay-resume-playback-renderer) {
  1085. opacity: 1;
  1086. }
  1087. }
  1088.  
  1089. .CentAnni-style-hide-watched-videos-global {
  1090. ytd-rich-item-renderer:has(ytd-thumbnail-overlay-resume-playback-renderer),
  1091. ytd-grid-video-renderer:has(ytd-thumbnail-overlay-resume-playback-renderer) {
  1092. display: none !important;
  1093. }
  1094. }
  1095.  
  1096. .ytp-cairo-refresh-signature-moments .ytp-play-progress, .ytp-swatch-background-color {
  1097. background: var(--progressBarColor) !important;
  1098. }
  1099.  
  1100. .CentAnni-style-disable-play-on-hover {
  1101. ytd-thumbnail[is-preview-loading] ytd-thumbnail-overlay-toggle-button-renderer.ytd-thumbnail,
  1102. ytd-thumbnail[is-preview-loading] ytd-thumbnail-overlay-time-status-renderer.ytd-thumbnail,
  1103. ytd-thumbnail[is-preview-loading] ytd-thumbnail-overlay-endorsement-renderer.ytd-thumbnail,
  1104. ytd-thumbnail[is-preview-loading] ytd-thumbnail-overlay-hover-text-renderer.ytd-thumbnail,
  1105. ytd-thumbnail[is-preview-loading] ytd-thumbnail-overlay-button-renderer.ytd-thumbnail,
  1106. ytd-thumbnail[now-playing] ytd-thumbnail-overlay-time-status-renderer.ytd-thumbnail,
  1107. ytd-thumbnail-overlay-loading-preview-renderer[is-preview-loading],
  1108. ytd-grid-video-renderer a#thumbnail div#mouseover-overlay,
  1109. ytd-rich-item-renderer a#thumbnail div#mouseover-overlay,
  1110. ytd-thumbnail-overlay-loading-preview-renderer,
  1111. #mouseover-overlay,
  1112. ytd-video-preview,
  1113. div#video-preview,
  1114. #video-preview,
  1115. #preview {
  1116. display: none !important;
  1117. }
  1118. }
  1119.  
  1120. .CentAnni-style-hide-end-cards {
  1121. .ytp-ce-element,
  1122. .ytp-gradient-bottom {
  1123. display: none !important;
  1124. }
  1125. }
  1126.  
  1127. .CentAnni-style-hide-endscreen {
  1128. .html5-video-player .html5-endscreen.videowall-endscreen {
  1129. display: none !important;
  1130. }
  1131. }
  1132.  
  1133. .ytd-page-manager[page-subtype="home"],
  1134. .ytd-page-manager[page-subtype="channels"],
  1135. .ytd-page-manager[page-subtype="subscriptions"] {
  1136. .style-scope.ytd-two-column-browse-results-renderer {
  1137. --ytd-rich-grid-items-per-row: var(--itemsPerRow) !important;
  1138. --ytd-rich-grid-posts-per-row: var(--itemsPerRow) !important;
  1139. --ytd-rich-grid-slim-items-per-row: var(--itemsPerRowCalc) !important;
  1140. --ytd-rich-grid-game-cards-per-row: var(--itemsPerRowCalc) !important;
  1141. --ytd-rich-grid-mini-game-cards-per-row: var(--itemsPerRowCalc) !important;
  1142. }
  1143.  
  1144. ytd-rich-grid-renderer {
  1145. --ytd-rich-grid-items-per-row: var(--itemsPerRow) !important;
  1146. }
  1147. }
  1148.  
  1149. .CentAnni-style-hide-voice-search {
  1150. #voice-search-button.ytd-masthead {
  1151. display: none;
  1152. }
  1153. }
  1154.  
  1155. .CentAnni-style-hide-create-button {
  1156. ytd-button-renderer.ytd-masthead[button-renderer][button-next]:has(button[aria-label="Create"]) {
  1157. display: none !important;
  1158. }
  1159. }
  1160.  
  1161. .CentAnni-style-hide-brand-text {
  1162. #country-code.ytd-topbar-logo-renderer,
  1163. #logo-icon [id^="youtube-paths_yt"] {
  1164. display: none;
  1165. }
  1166.  
  1167. #logo.ytd-masthead {
  1168. width: 50px;
  1169. overflow: hidden;
  1170. }
  1171. }
  1172.  
  1173. .CentAnni-style-hide-miniplayer {
  1174. ytd-miniplayer {
  1175. display: none !important;
  1176. }
  1177.  
  1178. #ytd-player .ytp-miniplayer-button {
  1179. display: none !important;
  1180. }
  1181. }
  1182.  
  1183. .CentAnni-style-square-search-bar {
  1184. #center.ytd-masthead { flex: 0 1 500px; }
  1185. .YtSearchboxComponentInputBox { border: 1px solid hsl(0,0%,18.82%); border-radius: 0; }
  1186. .YtSearchboxComponentSuggestionsContainer { border-radius: 0 0 10px 10px; }
  1187. .YtSearchboxComponentSearchButton, .YtSearchboxComponentSearchButtonDark { display: none; }
  1188. .YtSearchboxComponentHost { margin: 0; }
  1189.  
  1190. .ytSearchboxComponentInputBox { border: 1px solid hsl(0,0%,18.82%); border-radius: 0; }
  1191. .ytSearchboxComponentSuggestionsContainer { border-radius: 0 0 10px 10px; }
  1192. .ytSearchboxComponentSearchButton, .ytSearchboxComponentSearchButtonDark { display: none; }
  1193. .ytSearchboxComponentHost { margin: 0; }
  1194. }
  1195.  
  1196. .ytd-page-manager[page-subtype="home"] {
  1197. #avatar-container.ytd-rich-grid-media {
  1198. margin: 12px 12px 0 6px;
  1199. }
  1200. }
  1201.  
  1202. .CentAnni-style-square-design {
  1203. #endpoint.yt-simple-endpoint.ytd-guide-entry-renderer:hover,
  1204. #endpoint.yt-simple-endpoint.ytd-guide-entry-renderer:focus,
  1205. #endpoint.yt-simple-endpoint.ytd-guide-entry-renderer:active,
  1206. #chip-container.yt-chip-cloud-chip-renderer {
  1207. border-radius: 2px;
  1208. }
  1209.  
  1210. tp-yt-paper-dialog[modern],
  1211. yt-dropdown-menu {
  1212. border-radius: 2px;
  1213. }
  1214.  
  1215. #thumbnail,
  1216. .CentAnni-player-tabview,
  1217. .yt-thumbnail-view-model--medium,
  1218. #related.style-scope.ytd-watch-flexy,
  1219. .collections-stack-wiz__collection-stack2,
  1220. .collections-stack-wiz__collection-stack1--medium,
  1221. .CentAnni-player-tabview:has(.CentAnni-player-tabview-tab.active[data-tab="tab-2"]),
  1222. ytd-live-chat-frame[theater-watch-while][rounded-container],
  1223. ytd-live-chat-frame[rounded-container] iframe.ytd-live-chat-frame,
  1224. ytd-watch-flexy[flexy][js-panel-height_]:not([fixed-panels]) #chat.ytd-watch-flexy:not([collapsed]),
  1225. ytd-playlist-panel-renderer[modern-panels]:not([within-miniplayer]) #container.ytd-playlist-panel-renderer {
  1226. border-radius: 0 !important;
  1227. }
  1228.  
  1229. .CentAnni-player-tabView-tab {
  1230. border-radius: 2px;
  1231. }
  1232.  
  1233. ytd-watch-flexy[theater] .CentAnni-player-tabView-tab {
  1234. border-radius: 0;
  1235. }
  1236.  
  1237. .badge-shape-wiz--thumbnail-badge {
  1238. border-radius: 2px;
  1239. }
  1240.  
  1241. .ytd-page-manager[page-subtype="home"] {
  1242. yt-chip-cloud-chip-renderer {
  1243. border-radius: 2px;
  1244. }
  1245.  
  1246. .CentAnni-style-live-video, .CentAnni-style-upcoming-video, .CentAnni-style-newly-video, .CentAnni-style-recent-video, .CentAnni-style-lately-video, .CentAnni-style-old-video { border-radius: 0; }
  1247. }
  1248.  
  1249. .ytd-page-manager[page-subtype="channels"] {
  1250. .yt-spec-button-shape-next--size-m {
  1251. border-radius: 2px;
  1252. }
  1253.  
  1254. .yt-thumbnail-view-model--medium,
  1255. .yt-image-banner-view-model-wiz--inset,
  1256. .collections-stack-wiz__collection-stack2,
  1257. #chip-container.yt-chip-cloud-chip-renderer,
  1258. .collections-stack-wiz__collection-stack1--medium {
  1259. border-radius: 0 !important;
  1260. }
  1261. }
  1262.  
  1263. .yt-spec-button-shape-next--size-m.yt-spec-button-shape-next--segmented-start {
  1264. border-radius: 2px 0 0 3px;
  1265. }
  1266.  
  1267. .yt-spec-button-shape-next--size-m.yt-spec-button-shape-next--segmented-end {
  1268. border-radius: 0 3px 3px 0;
  1269. }
  1270.  
  1271. ytd-engagement-panel-section-list-renderer[modern-panels]:not([live-chat-engagement-panel]),
  1272. .immersive-header-container.ytd-playlist-header-renderer,
  1273. .ytVideoMetadataCarouselViewModelHost,
  1274. .yt-spec-button-shape-next--size-s,
  1275. .yt-spec-button-shape-next--size-m,
  1276. #description.ytd-watch-metadata,
  1277. ytd-multi-page-menu-renderer,
  1278. yt-chip-cloud-chip-renderer,
  1279. .ytChipShapeChip {
  1280. border-radius: 2px;
  1281. }
  1282.  
  1283. ytd-menu-popup-renderer {
  1284. border-radius: 0 0 5px 5px;
  1285. }
  1286.  
  1287. ytd-macro-markers-list-item-renderer[rounded] #thumbnail.ytd-macro-markers-list-item-renderer,
  1288. ytd-thumbnail[size="medium"] a.ytd-thumbnail, ytd-thumbnail[size="medium"]::before,
  1289. ytd-thumbnail[size="large"] a.ytd-thumbnail, ytd-thumbnail[size="large"]::before,
  1290. ytd-watch-flexy[rounded-player-large][default-layout] #ytd-player.ytd-watch-flexy,
  1291. ytd-engagement-panel-section-list-renderer[modern-panels]:not([live-chat-engagement-panel]) {
  1292. border-radius: 0 !important;
  1293. }
  1294. }
  1295.  
  1296. .CentAnni-style-remove-scrubber {
  1297. .ytp-scrubber-container {
  1298. display: none;
  1299. pointer-events: none;
  1300. }
  1301. }
  1302.  
  1303. .CentAnni-style-compact-layout {
  1304. ytd-rich-section-renderer:has(.grid-subheader.ytd-shelf-renderer) {
  1305. display: none;
  1306. }
  1307.  
  1308. #page-manager.ytd-app {
  1309. --ytd-toolbar-offset: 0 !important;
  1310. }
  1311.  
  1312. .ytd-page-manager[page-subtype="home"],
  1313. .ytd-page-manager[page-subtype="channels"],
  1314. .ytd-page-manager[page-subtype="subscriptions"] {
  1315. #contents.ytd-rich-grid-renderer {
  1316. width: 100%;
  1317. max-width: 100%;
  1318. padding-top: 0;
  1319. display: flex;
  1320. flex-wrap: wrap;
  1321. justify-content: flex-start;
  1322. }
  1323.  
  1324. .style-scope.ytd-two-column-browse-results-renderer {
  1325. --ytd-rich-grid-item-max-width: 100vw;
  1326. --ytd-rich-grid-item-min-width: 310px;
  1327. --ytd-rich-grid-item-margin: 1px !important;
  1328. --ytd-rich-grid-content-offset-top: 56px;
  1329. }
  1330.  
  1331. ytd-rich-item-renderer[rendered-from-rich-grid][is-in-first-column] {
  1332. margin-left: 5px !important;
  1333. }
  1334.  
  1335. ytd-rich-item-renderer[rendered-from-rich-grid] {
  1336. margin: 5px 0 20px 5px !important;
  1337. }
  1338.  
  1339. #meta.ytd-rich-grid-media {
  1340. overflow-x: hidden;
  1341. padding-right: 6px;
  1342. }
  1343.  
  1344. #avatar-container.ytd-rich-grid-media {
  1345. margin:7px 6px 0px 6px;
  1346. }
  1347.  
  1348. h3.ytd-rich-grid-media {
  1349. margin: 7px 0 4px 0;
  1350. }
  1351. }
  1352.  
  1353. .ytd-page-manager[page-subtype="home"] {
  1354. ytd-menu-renderer.ytd-rich-grid-media {
  1355. position: absolute;
  1356. height: 36px;
  1357. width: 36px;
  1358. top: 48px;
  1359. right: auto;
  1360. left: 6px;
  1361. align-items: center;
  1362. transform: rotate(90deg);
  1363. background-color: rgba(255,255,255,.1);
  1364. border-radius: 50%;
  1365. }
  1366.  
  1367. .title-badge.ytd-rich-grid-media, .video-badge.ytd-rich-grid-media {
  1368. position: absolute;
  1369. right: 0;
  1370. bottom: 0;
  1371. margin: 10px 10%;
  1372. }
  1373.  
  1374. ytd-rich-item-renderer[rendered-from-rich-grid] {
  1375. margin: 5px 5px 20px 5px !important;
  1376. }
  1377.  
  1378. .style-scope.ytd-two-column-browse-results-renderer {
  1379. --ytd-rich-grid-item-margin: .5% !important;
  1380. }
  1381. }
  1382.  
  1383. .ytd-page-manager[page-subtype="channels"] {
  1384. ytd-tabbed-page-header.grid-5-columns #page-header.ytd-tabbed-page-header, ytd-tabbed-page-header.grid-5-columns[has-inset-banner] #page-header-banner.ytd-tabbed-page-header {
  1385. padding: 0 !important;
  1386. }
  1387.  
  1388. ytd-two-column-browse-results-renderer.grid-5-columns, .grid-5-columns.ytd-two-column-browse-results-renderer {
  1389. width: 100% !important;
  1390. }
  1391.  
  1392. ytd-rich-grid-renderer:not([is-default-grid]) #header.ytd-rich-grid-renderer {
  1393. transform: translateX(800px) translateY(-40px);
  1394. z-index: 2000;
  1395. }
  1396.  
  1397. ytd-feed-filter-chip-bar-renderer[component-style="FEED_FILTER_CHIP_BAR_STYLE_TYPE_CHANNEL_PAGE_GRID"] {
  1398. margin-bottom: -32px;
  1399. margin-top: 0;
  1400. }
  1401.  
  1402. .page-header-view-model-wiz__page-header-headline-image {
  1403. margin-left: 110px;
  1404. }
  1405.  
  1406. ytd-menu-renderer.ytd-rich-grid-media {
  1407. position: absolute;
  1408. height: 36px;
  1409. width: 36px;
  1410. top: 2.5em;
  1411. right: 0;
  1412. left: auto;
  1413. align-items: center;
  1414. transform: rotate(90deg);
  1415. background-color: rgba(255,255,255,.1);
  1416. border-radius: 50%;
  1417. }
  1418.  
  1419. .yt-tab-group-shape-wiz__slider,.yt-tab-shape-wiz__tab-bar {
  1420. display:none;
  1421. }
  1422.  
  1423. .yt-tab-shape-wiz__tab--tab-selected,.yt-tab-shape-wiz__tab:hover {
  1424. color:white;
  1425. }
  1426.  
  1427. #tabsContent > yt-tab-group-shape > div.yt-tab-group-shape-wiz__tabs > yt-tab-shape:nth-child(3) {
  1428. display:none!important;
  1429. }
  1430.  
  1431. .style-scope.ytd-two-column-browse-results-renderer {
  1432. --ytd-rich-grid-item-margin: .5% !important;
  1433. }
  1434. }
  1435.  
  1436. .ytd-page-manager[page-subtype="channels"] #contentContainer {
  1437. padding-top: 0 !important;
  1438. }
  1439.  
  1440. .ytd-page-manager[page-subtype="channels"] tp-yt-app-header {
  1441. position: static !important;
  1442. transform: none !important;
  1443. transition: none !important;
  1444. }
  1445.  
  1446. .ytd-page-manager[page-subtype="channels"] tp-yt-app-header[fixed] {
  1447. position: static !important;
  1448. transform: none !important;
  1449. transition: none !important;
  1450. }
  1451.  
  1452. .ytd-page-manager[page-subtype="channels"] tp-yt-app-header #page-header {
  1453. position: static !important;
  1454. transform: none !important;
  1455. }
  1456.  
  1457. .ytd-page-manager[page-subtype="subscriptions"] {
  1458. ytd-menu-renderer.ytd-rich-grid-media {
  1459. position: absolute;
  1460. height: 36px;
  1461. width: 36px;
  1462. top: 50px;
  1463. right: auto;
  1464. left: 3px;
  1465. align-items: center;
  1466. transform: rotate(90deg);
  1467. background-color: rgba(255,255,255,.1);
  1468. border-radius: 50%;
  1469. }
  1470.  
  1471. .title-badge.ytd-rich-grid-media, .video-badge.ytd-rich-grid-media {
  1472. position: absolute;
  1473. margin: 0px 10% 0 0;
  1474. right: 0;
  1475. top: 6em;
  1476. }
  1477. }
  1478.  
  1479. .item.ytd-watch-metadata {
  1480. margin-top: 7px;
  1481. }
  1482.  
  1483. #description {
  1484. margin-top: 0;
  1485. }
  1486.  
  1487. #subheader.ytd-engagement-panel-title-header-renderer:not(:empty) {
  1488. padding: 0 !important;
  1489. transform: translateX(110px) translateY(-44px);
  1490. background-color: transparent;
  1491. border-top: none;
  1492. }
  1493.  
  1494. #header.ytd-engagement-panel-title-header-renderer {
  1495. padding: 4px 7px 4px 7px;
  1496. }
  1497.  
  1498. #visibility-button.ytd-engagement-panel-title-header-renderer, #information-button.ytd-engagement-panel-title-header-renderer {
  1499. z-index: 1;
  1500. }
  1501.  
  1502. .ytChipShapeChip:hover {
  1503. background: rgba(255,255,255,0.2);
  1504. border-color: transparent;
  1505. }
  1506.  
  1507. .ytChipShapeActive:hover {
  1508. background-color: #f1f1f1;
  1509. color: #0f0f0f;
  1510. }
  1511.  
  1512. ytd-engagement-panel-title-header-renderer {
  1513. height: 54px;
  1514. }
  1515.  
  1516. .yt-spec-button-shape-next--icon-only-default {
  1517. width: 35px;
  1518. height: 35px;
  1519. }
  1520. }
  1521.  
  1522. .ytd-page-manager[page-subtype="home"] {
  1523. .CentAnni-style-live-video, .CentAnni-style-upcoming-video, .CentAnni-style-newly-video, .CentAnni-style-recent-video, .CentAnni-style-lately-video { outline: 2px solid; border-radius: 12px; }
  1524. .CentAnni-style-old-video { outline: none;}
  1525.  
  1526. .CentAnni-style-live-video { outline-color: var(--liveVideo);}
  1527. .CentAnni-style-streamed-text { color: var(--streamedText);}
  1528. .CentAnni-style-upcoming-video { outline-color: var(--upComingVideo);}
  1529. .CentAnni-style-newly-video { outline-color: var(--newlyVideo);}
  1530. .CentAnni-style-recent-video { outline-color: var(--recentVideo);}
  1531. .CentAnni-style-lately-video { outline-color: var(--latelyVideo);}
  1532. .CentAnni-style-old-video { opacity: var(--oldVideo);}
  1533. }
  1534.  
  1535. .CentAnni-style-hide-watched-videos {
  1536. .ytd-page-manager[page-subtype="home"] {
  1537. ytd-rich-item-renderer:has(ytd-thumbnail-overlay-resume-playback-renderer) {
  1538. display: none;
  1539. }
  1540. }
  1541. }
  1542.  
  1543. .CentAnni-close-live-chat {
  1544. #chat-container {
  1545. z-index: -1 !important;
  1546. opacity: 0 !important;
  1547. visibility: hidden;
  1548. pointer-events: none !important;
  1549. }
  1550.  
  1551. ytd-watch-flexy[fixed-panels] #panels-full-bleed-container.ytd-watch-flexy {
  1552. width: var(--ytd-watch-flexy-sidebar-width);
  1553. display: none;
  1554. }
  1555.  
  1556. .video-stream.html5-main-video {
  1557. width: 100%;
  1558. }
  1559.  
  1560. ytd-watch-flexy[fixed-panels] #columns.ytd-watch-flexy {
  1561. padding-right: 0;
  1562. }
  1563. }
  1564.  
  1565. .CentAnni-style-hide-join-button {
  1566. #sponsor-button.ytd-video-owner-renderer:not(:empty),
  1567. ytd-browse[page-subtype="channels"] ytd-recognition-shelf-renderer,
  1568. ytd-browse[page-subtype="channels"] yt-page-header-view-model yt-flexible-actions-view-model button-view-model {
  1569. display: none !important;
  1570. }
  1571. }
  1572.  
  1573. .CentAnni-style-hide-playnext-button {
  1574. a.ytp-next-button {
  1575. display: none;
  1576. }
  1577. }
  1578.  
  1579. .CentAnni-style-small-subscribe-button {
  1580. .ytd-page-manager:not([page-subtype="channels"]) .yt-spec-button-shape-next.yt-spec-button-shape-next--tonal.yt-spec-button-shape-next--mono.yt-spec-button-shape-next--size-m.yt-spec-button-shape-next--icon-leading-trailing {
  1581. display: flex;
  1582. align-items: center;
  1583. justify-content: flex-start;
  1584. overflow: hidden;
  1585. width: 36px;
  1586. padding: 0 12px;
  1587. }
  1588. }
  1589.  
  1590. .CentAnni-style-hide-share-button {
  1591. yt-button-view-model.ytd-menu-renderer:has(button.yt-spec-button-shape-next[aria-label="Share"]) {
  1592. display: none;
  1593. }
  1594. }
  1595.  
  1596. .CentAnni-style-hide-add-comment {
  1597. ytd-comments ytd-comments-header-renderer #simple-box {
  1598. display: none;
  1599. }
  1600.  
  1601. #title.ytd-comments-header-renderer {
  1602. margin-bottom: 0;
  1603. }
  1604. }
  1605.  
  1606. .CentAnni-style-hide-news-home {
  1607. ytd-rich-grid-renderer ytd-rich-section-renderer:has(yt-icon:empty) {
  1608. display: none;
  1609. }
  1610. }
  1611.  
  1612. .CentAnni-style-hide-playlists-home {
  1613. ytd-rich-grid-renderer > #contents > ytd-rich-item-renderer:has(a[href*="list="]) {
  1614. display: none;
  1615. }
  1616. }
  1617.  
  1618. .CentAnni-style-hide-reply-button {
  1619. ytd-comments ytd-comment-engagement-bar #reply-button-end {
  1620. display: none;
  1621. }
  1622. }
  1623.  
  1624. .CentAnni-style-search-hide-right-sidebar {
  1625. #container.ytd-search ytd-secondary-search-container-renderer {
  1626. display: none;
  1627. }
  1628. }
  1629.  
  1630. .CentAnni-style-hide-shorts {
  1631. a[title="Shorts"],
  1632. #container.ytd-search ytd-reel-shelf-renderer,
  1633. ytd-watch-metadata #description ytd-reel-shelf-renderer,
  1634. ytd-browse[page-subtype="channels"] ytd-reel-shelf-renderer,
  1635. ytd-video-renderer:has(a.yt-simple-endpoint[href*="shorts"]),
  1636. yt-chip-cloud-chip-renderer[chip-shape-data*='"text":"Shorts"'],
  1637. ytd-reel-shelf-renderer.ytd-structured-description-content-renderer,
  1638. ytd-rich-section-renderer:has(div ytd-rich-shelf-renderer[is-shorts]),
  1639. #container.ytd-search ytd-video-renderer:has(a.yt-simple-endpoint[href*="shorts"]) {
  1640. display: none !important;
  1641. }
  1642. }
  1643.  
  1644. /* hide main scrollbar in safari */
  1645. html {
  1646. scrollbar-width: none;
  1647. -ms-overflow-style: none;
  1648. }
  1649.  
  1650. html::-webkit-scrollbar {
  1651. display: none;
  1652. }
  1653.  
  1654. .scrollable-div {
  1655. scrollbar-width: auto;
  1656. -ms-overflow-style: auto;
  1657. }
  1658.  
  1659. .scrollable-div::-webkit-scrollbar {
  1660. display: block;
  1661. }
  1662.  
  1663. /* adjustments for light mode */
  1664. ytd-masthead:not([dark]):not([page-dark-theme]) .buttons-left {
  1665. color: black;
  1666. }
  1667.  
  1668. ytd-masthead:not([dark]):not([page-dark-theme]) .button-style-settings {
  1669. color: slategray !important;
  1670. }
  1671.  
  1672. ytd-masthead:not([dark]):not([page-dark-theme]) .button-style-settings:hover {
  1673. color: black !important;
  1674. }
  1675.  
  1676. ytd-masthead:not([dark]):not([page-dark-theme]) .button-style {
  1677. color: black;
  1678. }
  1679.  
  1680. ytd-masthead:not([dark]):not([page-dark-theme]) .button-wrapper:not(:has(.button-style-settings)):hover {
  1681. background-color: rgba(0, 0, 0, 0.1); border-radius: 24px;
  1682. }
  1683.  
  1684. ytd-masthead:not([dark]):not([page-dark-theme]) .button-wrapper:not(:has(.button-style-settings)):active {
  1685. background-color: rgba(0, 0, 0, 0.2); border-radius: 24px;
  1686. }
  1687.  
  1688. html:not([dark]) .CentAnni-playback-speed-button:active {
  1689. background: rgb(205,205,205) !important;
  1690. }
  1691.  
  1692. html:not([dark]) .CentAnni-player-tabView-tab,
  1693. html:not([dark]) .CentAnni-playback-speed-display {
  1694. background-color: rgba(0,0,0,0.05);
  1695. color: #0f0f0f;
  1696. }
  1697.  
  1698. html:not([dark]) .CentAnni-player-tabView-tab:hover {
  1699. background: rgba(0,0,0,0.1);
  1700. border-color: transparent;
  1701. }
  1702.  
  1703. html:not([dark]) .CentAnni-player-tabView-tab.active {
  1704. background-color: #0f0f0f;
  1705. color: white;
  1706. }
  1707.  
  1708. html:not([dark]) .CentAnni-player-tabView {
  1709. border: 1px solid var(--yt-spec-10-percent-layer);
  1710. }
  1711.  
  1712. html:not([dark]) ytd-watch-flexy[flexy][js-panel-height_] #panels.ytd-watch-flexy ytd-engagement-panel-section-list-renderer.ytd-watch-flexy[target-id="engagement-panel-structured-description"] {
  1713. background-color: var(--yt-spec-badge-chip-background);
  1714. }
  1715.  
  1716. html:not([dark]) #CentAnni-playback-speed-control > div > svg > path {
  1717. fill: black;
  1718. }
  1719.  
  1720. html:not([dark]) ytd-watch-flexy[flexy][js-panel-height_] #panels.ytd-watch-flexy ytd-engagement-panel-section-list-renderer.ytd-watch-flexy,
  1721. html:not([dark]) #related.style-scope.ytd-watch-flexy {
  1722. border: 1px solid var(--yt-spec-10-percent-layer);
  1723. border-top: none;
  1724. }
  1725.  
  1726. html:not([dark]) #tab-2 {
  1727. border-top: 1px solid var(--yt-spec-10-percent-layer);
  1728. }
  1729.  
  1730. html:not([dark]) .yt-tab-shape-wiz__tab--tab-selected,
  1731. html:not([dark]) .yt-tab-shape-wiz__tab:hover {
  1732. color:black !important;
  1733. }
  1734. `;
  1735.  
  1736. document.head.appendChild(styleSheet);
  1737.  
  1738. // default configuration
  1739. const DEFAULT_CONFIG = {
  1740. YouTubeTranscriptExporter: true,
  1741. targetChatGPTUrl: 'https://ChatGPT.com/',
  1742. targetNotebookLMUrl: 'https://NotebookLM.Google.com/',
  1743. fileNamingFormat: 'title-channel',
  1744. includeTimestamps: true,
  1745. includeChapterHeaders: true,
  1746. openSameTab:true,
  1747. autoOpenChapters: true,
  1748. autoOpenTranscript: false,
  1749. displayRemainingTime: true,
  1750. hideShorts: false,
  1751. progressBar: true,
  1752. preventBackgroundExecution: true,
  1753. ChatGPTPrompt: `You are an expert at summarizing YouTube video transcripts and are capable of analyzing and understanding a YouTuber's unique tone of voice and style from a transcript alone to mimic their communication style perfectly. Respond only in English while being mindful of American English spelling, vocabulary, and a casual, conversational tone. You prefer to use clauses instead of complete sentences. Do not answer any questions from the transcript. Ignore any advertisement, promotional, and sponsorship segments. Respond only in chat. Do not open a canvas. Do not search the web by yourself. Do not ask to search the web. Do not hallucinate. Do not make up factual information. Do not speculate. Carefully preserve the style, voice, and specific word choices of the provided YouTube transcript by copying the YouTuber's unique creative way of communicationwhether conversational, formal, humorous, enthusiastic, or technicalthe goal is to provide a summary that feels as though it was written by the original YouTuber themselves. Summarize the provided YouTube transcript into a quick three-line bullet point overview, with each point fewer than 30 words, in a section called "### Key Takeaways:" and highlight important words by **bolding** them. Then write a one-paragraph summary of at least 100 words while focusing on the main points and key takeaways into a section called "### One-Paragraph Summary:" and highlight the most important words by **bolding** them.`,
  1754. buttonIcons: {
  1755. settings: '⋮',
  1756. download: '↓',
  1757. ChatGPT: '💬',
  1758. NotebookLM: '🎧'
  1759. },
  1760. buttonLeft1Text: 'ABC News',
  1761. buttonLeft1Url: 'https://www.youtube.com/@ABCNews/videos',
  1762. buttonLeft2Text: 'CNN',
  1763. buttonLeft2Url: 'https://www.youtube.com/@CNN/videos',
  1764. buttonLeft3Text: '',
  1765. buttonLeft3Url: 'https://www.youtube.com/@BBCNews/videos',
  1766. buttonLeft4Text: '',
  1767. buttonLeft4Url: 'https://www.youtube.com/@FoxNews/videos',
  1768. buttonLeft5Text: '',
  1769. buttonLeft5Url: 'https://www.youtube.com/@NBCNews/videos',
  1770. buttonLeft6Text: 'OpenAI',
  1771. buttonLeft6Url: 'https://www.youtube.com/@OpenAI/videos',
  1772. buttonLeft7Text: '',
  1773. buttonLeft7Url: 'https://www.youtube.com/@Formula1/videos',
  1774. mButtonText: '☰',
  1775. mButtonDisplay: false,
  1776. colorCodeVideosEnabled: true,
  1777. videosHideWatched: false,
  1778. videosOldOpacity: 0.5,
  1779. videosAgeColorPickerNewly: '#FFFF00',
  1780. videosAgeColorPickerRecent: '#FF9B00',
  1781. videosAgeColorPickerLately: '#006DFF',
  1782. videosAgeColorPickerLive: '#FF0000',
  1783. videosAgeColorPickerStreamed: '#FF0000',
  1784. videosAgeColorPickerUpcoming: '#32CD32',
  1785. progressbarColorPicker: '#FF0033',
  1786. textTransform: 'normal-case',
  1787. defaultFontSize: 10,
  1788. videosWatchedOpacity: 0.5,
  1789. videosPerRow: 3,
  1790. videosHideWatchedGlobal: false,
  1791. preventAutoplay: false,
  1792. hideVoiceSearch: false,
  1793. hideCreateButton: false,
  1794. hideBrandText: false,
  1795. hideJoinButton: false,
  1796. hidePlayNextButton: false,
  1797. hideShareButton: false,
  1798. hideRightSidebarSearch: false,
  1799. hideAddComment: false,
  1800. hideReplyButton: false,
  1801. hidePlaylistsHome: false,
  1802. hideNewsHome: false,
  1803. hideEndCards: false,
  1804. hideEndscreen: false,
  1805. smallSubscribeButton: false,
  1806. removeScrubber: false,
  1807. disablePlayOnHover: false,
  1808. hideMiniPlayer: false,
  1809. closeChatWindow: true,
  1810. displayFullTitle: true,
  1811. autoTheaterMode: false,
  1812. channelReindirizzare: false,
  1813. squareSearchBar: true,
  1814. squareDesign: false,
  1815. compactLayout: false
  1816. };
  1817.  
  1818. // load user configuration or use defaults
  1819. let storedConfig = {};
  1820. try {
  1821. storedConfig = await GM.getValue('USER_CONFIG', {});
  1822. } catch (error) {
  1823. showNotification('Error loading user save!');
  1824. console.error("YouTubeAlchemy: Error loading user configuration:", error);
  1825. }
  1826.  
  1827. let USER_CONFIG = {
  1828. ...DEFAULT_CONFIG,
  1829. ...storedConfig,
  1830. buttonIcons: {
  1831. ...DEFAULT_CONFIG.buttonIcons,
  1832. ...storedConfig.buttonIcons
  1833. }
  1834. };
  1835.  
  1836. // ensure CSS settings load immediately
  1837. function loadCSSsettings() {
  1838. const body = document.querySelector('body');
  1839.  
  1840. // features css
  1841. if (USER_CONFIG.progressBar) { body.classList.add('CentAnni-progress-bar'); } else { body.classList.remove('CentAnni-progress-bar'); }
  1842. if (USER_CONFIG.mButtonDisplay) { body.classList.add('CentAnni-style-hide-default-sidebar'); } else { body.classList.remove('CentAnni-style-hide-default-sidebar'); }
  1843.  
  1844. // custom css
  1845. document.documentElement.style.setProperty('--progressBarColor', USER_CONFIG.progressbarColorPicker);
  1846. document.documentElement.style.setProperty('--textTransform', USER_CONFIG.textTransform);
  1847. document.documentElement.style.setProperty('--fontSize', `${USER_CONFIG.defaultFontSize}px`);
  1848. document.documentElement.style.setProperty('--watchedOpacity', USER_CONFIG.videosWatchedOpacity);
  1849. document.documentElement.style.setProperty('--itemsPerRow', USER_CONFIG.videosPerRow);
  1850. document.documentElement.style.setProperty('--itemsPerRowCalc', USER_CONFIG.videosPerRow + 2);
  1851. if (USER_CONFIG.videosHideWatchedGlobal) { body.classList.add('CentAnni-style-hide-watched-videos-global'); } else { body.classList.remove('CentAnni-style-hide-watched-videos-global'); }
  1852. if (USER_CONFIG.hideVoiceSearch) { body.classList.add('CentAnni-style-hide-voice-search'); } else { body.classList.remove('CentAnni-style-hide-voice-search'); }
  1853. if (USER_CONFIG.hideCreateButton) { body.classList.add('CentAnni-style-hide-create-button'); } else { body.classList.remove('CentAnni-style-hide-create-button'); }
  1854. if (USER_CONFIG.hideBrandText) { body.classList.add('CentAnni-style-hide-brand-text'); } else { body.classList.remove('CentAnni-style-hide-brand-text'); }
  1855. if (USER_CONFIG.hideMiniPlayer) { body.classList.add('CentAnni-style-hide-miniplayer'); } else { body.classList.remove('CentAnni-style-hide-miniplayer'); }
  1856. if (USER_CONFIG.disablePlayOnHover) { body.classList.add('CentAnni-style-disable-play-on-hover'); } else { body.classList.remove('CentAnni-style-disable-play-on-hover'); }
  1857. if (USER_CONFIG.smallSubscribeButton) { body.classList.add('CentAnni-style-small-subscribe-button'); } else { body.classList.remove('CentAnni-style-small-subscribe-button'); }
  1858. if (USER_CONFIG.hideShareButton) { body.classList.add('CentAnni-style-hide-share-button'); } else { body.classList.remove('CentAnni-style-hide-share-button'); }
  1859. if (USER_CONFIG.hideAddComment) { body.classList.add('CentAnni-style-hide-add-comment'); } else { body.classList.remove('CentAnni-style-hide-add-comment'); }
  1860. if (USER_CONFIG.hideReplyButton) { body.classList.add('CentAnni-style-hide-reply-button'); } else { body.classList.remove('CentAnni-style-hide-reply-button'); }
  1861. if (USER_CONFIG.hidePlaylistsHome) { body.classList.add('CentAnni-style-hide-playlists-home'); } else { body.classList.remove('CentAnni-style-hide-playlists-home'); }
  1862. if (USER_CONFIG.hideNewsHome) { body.classList.add('CentAnni-style-hide-news-home'); } else { body.classList.remove('CentAnni-style-hide-news-home'); }
  1863. if (USER_CONFIG.hideRightSidebarSearch) { body.classList.add('CentAnni-style-search-hide-right-sidebar'); } else { body.classList.remove('CentAnni-style-search-hide-right-sidebar'); }
  1864. if (USER_CONFIG.hideShorts) { body.classList.add('CentAnni-style-hide-shorts'); } else { body.classList.remove('CentAnni-style-hide-shorts'); }
  1865. if (USER_CONFIG.hideEndCards) { body.classList.add('CentAnni-style-hide-end-cards'); } else { body.classList.remove('CentAnni-style-hide-end-cards'); }
  1866. if (USER_CONFIG.hideEndscreen) { body.classList.add('CentAnni-style-hide-endscreen'); } else { body.classList.remove('CentAnni-style-hide-endscreen'); }
  1867. if (USER_CONFIG.displayFullTitle) { body.classList.add('CentAnni-style-full-title'); } else { body.classList.remove('CentAnni-style-full-title'); }
  1868. if (USER_CONFIG.hideJoinButton) { body.classList.add('CentAnni-style-hide-join-button'); } else { body.classList.remove('CentAnni-style-hide-join-button'); }
  1869. if (USER_CONFIG.hidePlayNextButton) { body.classList.add('CentAnni-style-hide-playnext-button'); } else { body.classList.remove('CentAnni-style-hide-playnext-button'); }
  1870. if (USER_CONFIG.squareSearchBar) { body.classList.add('CentAnni-style-square-search-bar'); } else { body.classList.remove('CentAnni-style-square-search-bar'); }
  1871. if (USER_CONFIG.squareDesign) { body.classList.add('CentAnni-style-square-design'); } else { body.classList.remove('CentAnni-style-square-design'); }
  1872. if (USER_CONFIG.removeScrubber) { body.classList.add('CentAnni-style-remove-scrubber'); } else { body.classList.remove('CentAnni-style-remove-scrubber'); }
  1873. if (USER_CONFIG.compactLayout) { body.classList.add('CentAnni-style-compact-layout'); } else { body.classList.remove('CentAnni-style-compact-layout'); }
  1874. if (USER_CONFIG.closeChatWindow) { body.classList.add('CentAnni-close-live-chat'); } else { body.classList.remove('CentAnni-close-live-chat'); }
  1875.  
  1876. // color code videos
  1877. if (USER_CONFIG.videosHideWatched) { body.classList.add('CentAnni-style-hide-watched-videos'); } else { body.classList.remove('CentAnni-style-hide-watched-videos'); }
  1878. document.documentElement.style.setProperty('--liveVideo', USER_CONFIG.videosAgeColorPickerLive);
  1879. document.documentElement.style.setProperty('--streamedText', USER_CONFIG.videosAgeColorPickerStreamed);
  1880. document.documentElement.style.setProperty('--upComingVideo', USER_CONFIG.videosAgeColorPickerUpcoming);
  1881. document.documentElement.style.setProperty('--newlyVideo', USER_CONFIG.videosAgeColorPickerNewly);
  1882. document.documentElement.style.setProperty('--recentVideo', USER_CONFIG.videosAgeColorPickerRecent);
  1883. document.documentElement.style.setProperty('--latelyVideo', USER_CONFIG.videosAgeColorPickerLately);
  1884. document.documentElement.style.setProperty('--oldVideo', USER_CONFIG.videosOldOpacity);
  1885. }
  1886.  
  1887. // create and show the settings modal
  1888. function showSettingsModal() {
  1889. const existingModal = document.getElementById('yt-transcript-settings-modal');
  1890. if (existingModal) {
  1891. existingModal.style.display = 'flex';
  1892. document.body.style.overflow = 'hidden';
  1893. return;
  1894. }
  1895.  
  1896. // create modal elements
  1897. const modal = document.createElement('div');
  1898. modal.id = 'yt-transcript-settings-modal';
  1899. modal.classList.add('overlay');
  1900.  
  1901. const modalContent = document.createElement('div');
  1902. modalContent.classList.add('modal-content');
  1903.  
  1904. // modal header
  1905. const header = document.createElement('a');
  1906. header.href = 'https://github.com/TimMacy/YouTubeAlchemy';
  1907. header.target = '_blank';
  1908. header.innerText = 'YouTube Alchemy';
  1909. header.title = 'GitHub Repository for YouTube Alchemy';
  1910. header.classList.add('header');
  1911. modalContent.appendChild(header);
  1912.  
  1913. // create form elements for each setting
  1914. const form = document.createElement('form');
  1915. form.id = 'yt-transcript-settings-form';
  1916.  
  1917. // Button Icons
  1918. const iconsHeader = document.createElement('label');
  1919. iconsHeader.innerText = 'Button Icons:';
  1920. iconsHeader.classList.add('button-icons');
  1921. form.appendChild(iconsHeader);
  1922.  
  1923. const iconsContainer = document.createElement('div');
  1924. iconsContainer.classList.add('icons-container');
  1925.  
  1926. function createIconInputField(labelText, settingKey, settingValue, labelClass) {
  1927. const container = document.createElement('div');
  1928. container.classList.add('container-button');
  1929.  
  1930. const input = document.createElement('input');
  1931. const iconInputClass = `${settingKey}-input-field`;
  1932. input.type = 'text';
  1933. input.name = settingKey;
  1934. input.value = settingValue;
  1935. input.classList.add('container-button-input');
  1936. input.classList.add(iconInputClass);
  1937.  
  1938. const label = document.createElement('label');
  1939. label.innerText = labelText;
  1940. label.className = labelClass;
  1941. label.classList.add('container-button-label');
  1942.  
  1943. container.appendChild(input);
  1944. container.appendChild(label);
  1945.  
  1946. return container;
  1947. }
  1948.  
  1949. iconsContainer.appendChild(createIconInputField('NotebookLM', 'buttonIconNotebookLM', USER_CONFIG.buttonIcons.NotebookLM, 'label-NotebookLM'));
  1950. iconsContainer.appendChild(createIconInputField('ChatGPT', 'buttonIconChatGPT', USER_CONFIG.buttonIcons.ChatGPT, 'label-ChatGPT'));
  1951. iconsContainer.appendChild(createIconInputField('Download', 'buttonIconDownload', USER_CONFIG.buttonIcons.download, 'label-download'));
  1952. iconsContainer.appendChild(createIconInputField('Settings', 'buttonIconSettings', USER_CONFIG.buttonIcons.settings, 'label-settings'));
  1953.  
  1954. form.appendChild(iconsContainer);
  1955.  
  1956. // NotebookLM URL
  1957. form.appendChild(createInputField('NotebookLM URL (Copy transcript, then open the website):', 'targetNotebookLMUrl', USER_CONFIG.targetNotebookLMUrl, 'label-NotebookLM'));
  1958.  
  1959. // ChatGPT URL
  1960. form.appendChild(createInputField('ChatGPT URL (Copy transcript with the prompt, then open the website):', 'targetChatGPTUrl', USER_CONFIG.targetChatGPTUrl, 'label-ChatGPT'));
  1961.  
  1962. // SpacerTop10
  1963. const SpacerTop10 = document.createElement('div');
  1964. SpacerTop10.classList.add('spacer-10');
  1965. form.appendChild(SpacerTop10);
  1966.  
  1967. // File Naming Format
  1968. form.appendChild(createSelectField('Text File Naming Format:', 'label-download', 'fileNamingFormat', USER_CONFIG.fileNamingFormat, {
  1969. 'title-channel': 'Title - Channel.txt (default)',
  1970. 'channel-title': 'Channel - Title.txt',
  1971. 'date-title-channel': 'uploadDate - Title - Channel.txt',
  1972. 'date-channel-title': 'uploadDate - Channel - Title.txt',
  1973. }));
  1974.  
  1975. // YouTube Transcript Exporter
  1976. form.appendChild(createCheckboxField('Enable YouTube Transcript Exporter (default: yes)', 'YouTubeTranscriptExporter', USER_CONFIG.YouTubeTranscriptExporter));
  1977.  
  1978. // include Timestamps
  1979. form.appendChild(createCheckboxField('Include Timestamps in the Transcript (default: yes)', 'includeTimestamps', USER_CONFIG.includeTimestamps));
  1980.  
  1981. // include Chapter Headers
  1982. form.appendChild(createCheckboxField('Include Chapter Headers in the Transcript (default: yes)', 'includeChapterHeaders', USER_CONFIG.includeChapterHeaders));
  1983.  
  1984. // open in Same Tab
  1985. form.appendChild(createCheckboxField('Open Links in the Same Tab (default: yes)', 'openSameTab', USER_CONFIG.openSameTab));
  1986.  
  1987. // prevent Execution in Background Tabs
  1988. form.appendChild(createCheckboxField('Important for Chrome! (default: yes)', 'preventBackgroundExecution', USER_CONFIG.preventBackgroundExecution));
  1989.  
  1990. // info for Chrome
  1991. const description = document.createElement('small');
  1992. description.innerText = 'Prevents early script execution in background tabs.\nWhile this feature is superfluous in Safari, it is essential for Chrome.';
  1993. description.classList.add('chrome-info');
  1994. form.appendChild(description);
  1995.  
  1996. // extra settings buttons
  1997. const extraSettings = document.createElement('div');
  1998. extraSettings.classList.add('extra-button-container');
  1999.  
  2000. const buttonsLeft = document.createElement('button');
  2001. buttonsLeft.type = 'button';
  2002. buttonsLeft.innerText = 'Links in Header';
  2003. buttonsLeft.classList.add('btn-style-settings');
  2004. buttonsLeft.onclick = () => showSubPanel(createLinksInHeaderContent(), 'linksInHeader');
  2005.  
  2006. const customCSSButton = document.createElement('button');
  2007. customCSSButton.type = 'button';
  2008. customCSSButton.innerText = 'Features & CSS';
  2009. customCSSButton.classList.add('btn-style-settings');
  2010. customCSSButton.onclick = () => showSubPanel(createCustomCSSContent(), 'createcustomCSS');
  2011.  
  2012. const colorCodeVideos = document.createElement('button');
  2013. colorCodeVideos.type = 'button';
  2014. colorCodeVideos.innerText = 'Color Code Videos';
  2015. colorCodeVideos.classList.add('btn-style-settings');
  2016. colorCodeVideos.onclick = () => showSubPanel(createColorCodeVideosContent(), 'colorCodeVideos');
  2017.  
  2018. extraSettings.appendChild(buttonsLeft);
  2019. extraSettings.appendChild(customCSSButton);
  2020. extraSettings.appendChild(colorCodeVideos);
  2021.  
  2022. form.appendChild(extraSettings);
  2023.  
  2024. // ChatGPT Prompt
  2025. form.appendChild(createTextareaField('ChatGPT Prompt:', 'ChatGPTPrompt', USER_CONFIG.ChatGPTPrompt, 'label-ChatGPT'));
  2026.  
  2027. // action buttons container
  2028. const buttonContainer = document.createElement('div');
  2029. buttonContainer.classList.add('button-container-end');
  2030.  
  2031. // export and import button container
  2032. const exportImportContainer = document.createElement('div');
  2033. exportImportContainer.classList.add('button-container-backup');
  2034.  
  2035. const exportButton = document.createElement('button');
  2036. exportButton.type = 'button';
  2037. exportButton.innerText = 'Export Settings';
  2038. exportButton.classList.add('btn-style-settings');
  2039. exportButton.onclick = exportSettings;
  2040.  
  2041. const importButton = document.createElement('button');
  2042. importButton.type = 'button';
  2043. importButton.innerText = 'Import Settings';
  2044. importButton.classList.add('btn-style-settings');
  2045. importButton.onclick = importSettings;
  2046.  
  2047. // Copyright
  2048. const copyright = document.createElement('a');
  2049. copyright.href = 'https://github.com/TimMacy';
  2050. copyright.target = '_blank';
  2051. copyright.innerText = '© 2024 Tim Macy';
  2052. copyright.title = 'Copyright by Tim Macy';
  2053. copyright.classList.add('copyright');
  2054.  
  2055. const spacer = document.createElement('div');
  2056. spacer.style = 'flex: 1;';
  2057.  
  2058. // Save, Reset, and Cancel Buttons
  2059. const buttonContainerSettings = document.createElement('div');
  2060. buttonContainerSettings.classList.add('button-container-settings');
  2061.  
  2062. const saveButton = document.createElement('button');
  2063. saveButton.type = 'button';
  2064. saveButton.innerText = 'Save';
  2065. saveButton.classList.add('btn-style-settings');
  2066. saveButton.onclick = saveSettings;
  2067.  
  2068. const resetButton = document.createElement('button');
  2069. resetButton.type = 'button';
  2070. resetButton.innerText = 'Reset to Default';
  2071. resetButton.classList.add('btn-style-settings');
  2072. resetButton.onclick = async () => {
  2073. const userConfirmed = window.confirm("All settings will be reset to their default values.");
  2074. if (!userConfirmed) { return; }
  2075.  
  2076. try {
  2077. USER_CONFIG = { ...DEFAULT_CONFIG };
  2078. await GM.setValue('USER_CONFIG', USER_CONFIG);
  2079. showNotification('Settings have been reset to default!');
  2080. document.getElementById('yt-transcript-settings-modal').style.display = 'none';
  2081. setTimeout(() => { location.reload(); }, 1000);
  2082. } catch (error) {
  2083. showNotification('Error resetting settings to default!');
  2084. console.error("YouTubeAlchemy: Error resetting settings to default:", error);
  2085. }
  2086. };
  2087.  
  2088. const cancelButton = document.createElement('button');
  2089. cancelButton.type = 'button';
  2090. cancelButton.innerText = 'Cancel';
  2091. cancelButton.classList.add('btn-style-settings');
  2092. cancelButton.onclick = () => { modal.style.display = 'none'; document.body.style.overflow = ''; };
  2093.  
  2094. exportImportContainer.appendChild(exportButton);
  2095. exportImportContainer.appendChild(importButton);
  2096.  
  2097. buttonContainerSettings.appendChild(copyright);
  2098. buttonContainerSettings.appendChild(spacer);
  2099. buttonContainerSettings.appendChild(saveButton);
  2100. buttonContainerSettings.appendChild(resetButton);
  2101. buttonContainerSettings.appendChild(cancelButton);
  2102.  
  2103. buttonContainer.appendChild(exportImportContainer);
  2104. buttonContainer.appendChild(buttonContainerSettings);
  2105.  
  2106. form.appendChild(buttonContainer);
  2107. modalContent.appendChild(form);
  2108. modal.appendChild(modalContent);
  2109. document.body.appendChild(modal);
  2110. document.body.style.overflow = 'hidden';
  2111.  
  2112. // text area scroll on click
  2113. let animationTriggered = false;
  2114.  
  2115. document.querySelector('.chatgpt-prompt-textarea').addEventListener('click', function () {
  2116. if (animationTriggered) return;
  2117. animationTriggered = true;
  2118.  
  2119. const modalContent = this.closest('#yt-transcript-settings-form');
  2120. if (!modalContent) { animationTriggered = false; return; }
  2121.  
  2122. const textArea = this;
  2123. const buttons = modalContent.querySelector('.button-container-end');
  2124. const startHeight = 65;
  2125. const endHeight = 450;
  2126. const duration = 500;
  2127.  
  2128. const modalContentRect = modalContent.getBoundingClientRect();
  2129. const textAreaRect = textArea.getBoundingClientRect();
  2130. const buttonsRect = buttons.getBoundingClientRect();
  2131.  
  2132. const textAreaTop = textAreaRect.top - modalContentRect.top + modalContent.scrollTop;
  2133. const buttonsBottom = buttonsRect.bottom - modalContentRect.top + modalContent.scrollTop;
  2134. const contentHeightAfterExpansion = buttonsBottom + (endHeight - startHeight);
  2135. const modalVisibleHeight = modalContent.clientHeight;
  2136. const contentWillFit = contentHeightAfterExpansion <= modalVisibleHeight;
  2137. const maxScrollTop = textAreaTop;
  2138.  
  2139. let desiredScrollTop;
  2140.  
  2141. if (contentWillFit) { desiredScrollTop = contentHeightAfterExpansion - modalVisibleHeight;
  2142. } else { desiredScrollTop = buttonsBottom + (endHeight - startHeight) - modalVisibleHeight; }
  2143.  
  2144. const newScrollTop = Math.min(desiredScrollTop, maxScrollTop);
  2145. const startScrollTop = modalContent.scrollTop;
  2146. const scrollDistance = newScrollTop - startScrollTop;
  2147. const startTime = performance.now();
  2148.  
  2149. function animateScroll(currentTime) {
  2150. const elapsedTime = currentTime - startTime;
  2151. const progress = Math.min(elapsedTime / duration, 1);
  2152.  
  2153. const easeProgress = progress < 0.5
  2154. ? 2 * progress * progress
  2155. : -1 + (4 - 2 * progress) * progress;
  2156.  
  2157. const currentScrollTop = startScrollTop + scrollDistance * easeProgress;
  2158. modalContent.scrollTop = currentScrollTop;
  2159.  
  2160. if (progress < 1) { requestAnimationFrame(animateScroll);
  2161. } else { animationTriggered = false; }
  2162. }
  2163.  
  2164. requestAnimationFrame(animateScroll);
  2165. });
  2166.  
  2167. // close modal on overlay click
  2168. document.addEventListener('click', (event) => {
  2169. const mainModal = document.getElementById('yt-transcript-settings-modal');
  2170. const openSubPanel = document.querySelector('.sub-panel-overlay.active');
  2171.  
  2172. if (openSubPanel && event.target === openSubPanel) {
  2173. openSubPanel.classList.remove('active');
  2174. return;
  2175. }
  2176.  
  2177. if (mainModal && event.target === mainModal) {
  2178. mainModal.style.display = 'none';
  2179. document.body.style.overflow = '';
  2180. return;
  2181. }
  2182. });
  2183.  
  2184. // close modal with ESC key
  2185. const escKeyListener = function(event) {
  2186. if (event.key === 'Escape' && event.type === 'keydown') {
  2187. const openSubPanel = document.querySelector('.sub-panel-overlay.active');
  2188.  
  2189. if (openSubPanel) {
  2190. openSubPanel.classList.remove('active');
  2191. } else {
  2192. const modal = document.getElementById('yt-transcript-settings-modal');
  2193. if (modal) {
  2194. modal.style.display = 'none';
  2195. document.body.style.overflow = '';
  2196. }
  2197. }
  2198. }
  2199. };
  2200.  
  2201. window.addEventListener('keydown', escKeyListener);
  2202.  
  2203. document.addEventListener('yt-navigate-start', () => {
  2204. window.removeEventListener('keydown', escKeyListener);
  2205. });
  2206.  
  2207. // sub-panels
  2208. function showSubPanel(panelContent, panelId) {
  2209. let subPanelOverlay = document.querySelector(`.sub-panel-overlay[data-panel-id="${panelId}"]`);
  2210.  
  2211. if (!subPanelOverlay){
  2212. subPanelOverlay = document.createElement('div');
  2213. subPanelOverlay.classList.add('sub-panel-overlay');
  2214. subPanelOverlay.setAttribute('data-panel-id', panelId);
  2215.  
  2216. const subPanel = document.createElement('div');
  2217. subPanel.classList.add('sub-panel');
  2218.  
  2219. const closeButton = document.createElement('button');
  2220. closeButton.type = 'button';
  2221. closeButton.innerText = 'Close';
  2222. closeButton.classList.add('btn-style-settings');
  2223. closeButton.onclick = () => { subPanelOverlay.classList.remove('active'); };
  2224. subPanel.appendChild(closeButton);
  2225.  
  2226. if (panelContent) { subPanel.appendChild(panelContent); }
  2227.  
  2228. subPanelOverlay.appendChild(subPanel);
  2229. document.body.appendChild(subPanelOverlay);
  2230. }
  2231. subPanelOverlay.classList.add('active');
  2232. }
  2233.  
  2234. // links in header
  2235. function createLinksInHeaderContent() {
  2236. const form = document.createElement('form');
  2237. form.id = 'links-in-header-form';
  2238.  
  2239. const subPanelHeader = document.createElement('div');
  2240. subPanelHeader.classList.add('sub-panel-header');
  2241. subPanelHeader.textContent = 'Configure Links in Header';
  2242. form.appendChild(subPanelHeader);
  2243.  
  2244. const infoLinksHeader = document.createElement('small');
  2245. infoLinksHeader.innerText = "Up to seven links can be added next to the YouTube logo. An empty 'Link Text' field won't insert the link into the header.\nIf the navigation bar is hidden, a replacement icon will prepend the links, while retaining the default functionality of opening and closing the sidebar.";
  2246. infoLinksHeader.classList.add('chrome-info');
  2247. form.appendChild(infoLinksHeader);
  2248.  
  2249. const sidebarContainer = document.createElement('div');
  2250. sidebarContainer.classList.add('sidebar-container');
  2251.  
  2252. // hide left navigation bar and replacement icon
  2253. const checkboxField = createCheckboxField('Hide Navigation Bar', 'mButtonDisplay', USER_CONFIG.mButtonDisplay);
  2254. sidebarContainer.appendChild(checkboxField);
  2255.  
  2256. const inputField = createInputField('Navigation Bar Replacement Icon', 'mButtonText', USER_CONFIG.mButtonText, 'label-mButtonText');
  2257. sidebarContainer.appendChild(inputField);
  2258.  
  2259. form.appendChild(sidebarContainer);
  2260.  
  2261. // function to create a link input group
  2262. function createButtonInputGroup(linkNumber) {
  2263. const container = document.createElement('div');
  2264. container.classList.add('links-header-container');
  2265.  
  2266. // link text
  2267. const textField = createInputField(`Link ${linkNumber} Text`, `buttonLeft${linkNumber}Text`, USER_CONFIG[`buttonLeft${linkNumber}Text`], `label-buttonLeft${linkNumber}Text`);
  2268. container.appendChild(textField);
  2269.  
  2270. // link URL
  2271. const urlField = createInputField(`Link ${linkNumber} URL`, `buttonLeft${linkNumber}Url`, USER_CONFIG[`buttonLeft${linkNumber}Url`], `label-buttonLeft${linkNumber}Url`);
  2272. container.appendChild(urlField);
  2273.  
  2274. return container;
  2275. }
  2276.  
  2277. // create input groups for links 1 through 6
  2278. for (let i = 1; i <= 7; i++) {
  2279. form.appendChild(createButtonInputGroup(i));
  2280. }
  2281.  
  2282. return form;
  2283. }
  2284.  
  2285. // custom css
  2286. function createCustomCSSContent() {
  2287. const form = document.createElement('form');
  2288. form.id = 'custom-css-form';
  2289.  
  2290. const subPanelHeader = document.createElement('div');
  2291. subPanelHeader.classList.add('sub-panel-header');
  2292. subPanelHeader.textContent = 'Customize YouTube Appearance and Manage Features';
  2293. form.appendChild(subPanelHeader);
  2294.  
  2295. // dim watched videos
  2296. const videosWatchedContainer = createSliderInputField( 'Change Opacity of Watched Videos (default 0.5):', 'videosWatchedOpacity', USER_CONFIG.videosWatchedOpacity, '0', '1', '0.1' );
  2297. form.appendChild(videosWatchedContainer);
  2298.  
  2299. // title text transform
  2300. form.appendChild(createSelectField('Title Case:', 'label-Text-Transform', 'textTransform', USER_CONFIG.textTransform, {
  2301. 'uppercase': 'uppercase - THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.',
  2302. 'lowercase': 'lowercase - the quick brown fox jumps over the lazy dog.',
  2303. 'capitalize': 'capitalize - The Quick Brown Fox Jumps Over The Lazy Dog.',
  2304. 'normal-case': 'normal-case (default) - The quick brown fox jumps over the lazy dog.',
  2305. }));
  2306.  
  2307. // font size
  2308. const defaultFontSizeField = createNumberInputField('Font Size (default: 10)', 'defaultFontSize', USER_CONFIG.defaultFontSize);
  2309. form.appendChild(defaultFontSizeField);
  2310.  
  2311. // videos per row
  2312. const videosPerRow = createNumberInputField('Number of Videos per Row (default: 3)', 'videosPerRow', USER_CONFIG.videosPerRow);
  2313. form.appendChild(videosPerRow);
  2314.  
  2315. // features text
  2316. const featuresText = document.createElement('label');
  2317. featuresText.innerText = 'Features';
  2318. featuresText.classList.add('button-icons', 'features-text');
  2319. form.appendChild(featuresText);
  2320.  
  2321. // prevent autoplay
  2322. const preventAutoplay = createCheckboxField('Prevent Autoplay (default: no)', 'preventAutoplay', USER_CONFIG.preventAutoplay);
  2323. form.appendChild(preventAutoplay);
  2324.  
  2325. // auto open chapter panel
  2326. const autoOpenChapters = createCheckboxField('Automatically Open Chapter Panels (default: yes)', 'autoOpenChapters', USER_CONFIG.autoOpenChapters);
  2327. form.appendChild(autoOpenChapters);
  2328.  
  2329. // auto open transcript panel
  2330. const autoOpenTranscript = createCheckboxField('Automatically Open Transcript Panels (default: no)', 'autoOpenTranscript', USER_CONFIG.autoOpenTranscript);
  2331. form.appendChild(autoOpenTranscript);
  2332.  
  2333. // close chat window
  2334. const closeChatWindow = createCheckboxField('Auto Close Initial Chat Windows (default: yes)', 'closeChatWindow', USER_CONFIG.closeChatWindow);
  2335. form.appendChild(closeChatWindow);
  2336.  
  2337. // auto theater mode
  2338. const autoTheaterMode = createCheckboxField('Auto Theater Mode (default: no)', 'autoTheaterMode', USER_CONFIG.autoTheaterMode);
  2339. form.appendChild(autoTheaterMode);
  2340.  
  2341. // channel default videos page
  2342. const channelReindirizzare = createCheckboxField('Videos Tab as Default on Channel Page (default: no)', 'channelReindirizzare', USER_CONFIG.channelReindirizzare);
  2343. form.appendChild(channelReindirizzare);
  2344.  
  2345. // display remaining time
  2346. const displayRemainingTime = createCheckboxField('Display Remaining Time Under a Video, Adjusted for Playback Speed (default: yes)', 'displayRemainingTime', USER_CONFIG.displayRemainingTime);
  2347. form.appendChild(displayRemainingTime);
  2348.  
  2349. // persistent progress bar
  2350. const progressBar = createCheckboxField('Persistent Progress Bar with Chapter Markers and SponsorBlock Support (default: yes)', 'progressBar', USER_CONFIG.progressBar);
  2351. form.appendChild(progressBar);
  2352.  
  2353. // hide shorts
  2354. const hideShorts = createCheckboxField('Hide Shorts (default: no)', 'hideShorts', USER_CONFIG.hideShorts);
  2355. form.appendChild(hideShorts);
  2356.  
  2357. // features text
  2358. const featuresTextMain = document.createElement('label');
  2359. featuresTextMain.innerText = 'Layout Changes';
  2360. featuresTextMain.classList.add('button-icons', 'features-text');
  2361. form.appendChild(featuresTextMain);
  2362.  
  2363. // disable play on hover
  2364. const disablePlayOnHover = createCheckboxField('Disable Play on Hover (default: yes)', 'disablePlayOnHover', USER_CONFIG.disablePlayOnHover);
  2365. form.appendChild(disablePlayOnHover);
  2366.  
  2367. // square and compact search bar
  2368. const squareSearchBar = createCheckboxField('Square and Compact Search Bar (default: yes)', 'squareSearchBar', USER_CONFIG.squareSearchBar);
  2369. form.appendChild(squareSearchBar);
  2370.  
  2371. // square design
  2372. const squareDesign = createCheckboxField('Square Design (default: no)', 'squareDesign', USER_CONFIG.squareDesign);
  2373. form.appendChild(squareDesign);
  2374.  
  2375. // compact layout
  2376. const compactLayout = createCheckboxField('Compact Layout (default: no)', 'compactLayout', USER_CONFIG.compactLayout);
  2377. form.appendChild(compactLayout);
  2378.  
  2379. // features text
  2380. const featuresCSS = document.createElement('label');
  2381. featuresCSS.innerText = 'CSS to Change or Hide UI Elements';
  2382. featuresCSS.classList.add('button-icons', 'features-text');
  2383. form.appendChild(featuresCSS);
  2384.  
  2385. // hide voice search button
  2386. const hideVoiceSearch = createCheckboxField('Hide Voice Search Button in the Header (default: no)', 'hideVoiceSearch', USER_CONFIG.hideVoiceSearch);
  2387. form.appendChild(hideVoiceSearch);
  2388.  
  2389. // hide create button
  2390. const hideCreateButton = createCheckboxField('Hide Create Button in the Header (default: no)', 'hideCreateButton', USER_CONFIG.hideCreateButton);
  2391. form.appendChild(hideCreateButton);
  2392.  
  2393. // hide YouTube brand text within the header
  2394. const hideBrandText = createCheckboxField('Hide YouTube Brand Text in the Header (default: no)', 'hideBrandText', USER_CONFIG.hideBrandText);
  2395. form.appendChild(hideBrandText);
  2396.  
  2397. // small subscribed button
  2398. const smallSubscribeButton = createCheckboxField('Small Subscribed Button under a Video (default: no)', 'smallSubscribeButton', USER_CONFIG.smallSubscribeButton);
  2399. form.appendChild(smallSubscribeButton);
  2400.  
  2401. // hide join button
  2402. const hideJoinButton = createCheckboxField('Hide Join Button under a Video and on Channel Page (default: no)', 'hideJoinButton', USER_CONFIG.hideJoinButton);
  2403. form.appendChild(hideJoinButton);
  2404.  
  2405. // display full title
  2406. const displayFullTitle = createCheckboxField('Display Full Titles (default: yes)', 'displayFullTitle', USER_CONFIG.displayFullTitle);
  2407. form.appendChild(displayFullTitle);
  2408.  
  2409. // color picker progress bar
  2410. const progressbarColorContainer = document.createElement('div');
  2411. progressbarColorContainer.classList.add('videos-age-container');
  2412.  
  2413. function createLabelColorPair(labelText, configKey) {
  2414. const row = document.createElement('div');
  2415. row.classList.add('videos-age-row');
  2416.  
  2417. const colorPicker = document.createElement('input');
  2418. colorPicker.type = 'color';
  2419. colorPicker.value = USER_CONFIG[configKey];
  2420. colorPicker.name = configKey;
  2421. row.appendChild(colorPicker);
  2422.  
  2423. const label = document.createElement('span');
  2424. label.classList.add('label-style-settings');
  2425. label.innerText = labelText;
  2426. row.appendChild(label);
  2427.  
  2428. progressbarColorContainer.appendChild(row);
  2429. }
  2430.  
  2431. createLabelColorPair('Progress Bar Color', 'progressbarColorPicker');
  2432.  
  2433. form.appendChild(progressbarColorContainer);
  2434.  
  2435. // hide video scrubber
  2436. const removeScrubber = createCheckboxField('Hide Video Scrubber (default: no)', 'removeScrubber', USER_CONFIG.removeScrubber);
  2437. form.appendChild(removeScrubber);
  2438.  
  2439. // hide video end cards
  2440. const hideEndCards = createCheckboxField('Hide Video End Cards (default: no)', 'hideEndCards', USER_CONFIG.hideEndCards);
  2441. form.appendChild(hideEndCards);
  2442.  
  2443. // hide end screen
  2444. const hideEndscreen = createCheckboxField('Hide End Screens (suggestions at video end) (default: no)', 'hideEndscreen', USER_CONFIG.hideEndscreen);
  2445. form.appendChild(hideEndscreen);
  2446.  
  2447. // hide play next button
  2448. const hidePlayNextButton = createCheckboxField('Hide "Play Next" Button (default: no)', 'hidePlayNextButton', USER_CONFIG.hidePlayNextButton);
  2449. form.appendChild(hidePlayNextButton);
  2450.  
  2451. // hide share button
  2452. const hideShareButton = createCheckboxField('Hide Share Button under a Video (default: no)', 'hideShareButton', USER_CONFIG.hideShareButton);
  2453. form.appendChild(hideShareButton);
  2454.  
  2455. // hide add comment
  2456. const hideAddComment = createCheckboxField('Hide "Add Comment" Textfield (default: no)', 'hideAddComment', USER_CONFIG.hideAddComment);
  2457. form.appendChild(hideAddComment);
  2458.  
  2459. // hide reply comment button
  2460. const hideReplyButton = createCheckboxField('Hide Comment "Reply" Button (default: no)', 'hideReplyButton', USER_CONFIG.hideReplyButton);
  2461. form.appendChild(hideReplyButton);
  2462.  
  2463. // hide news on home
  2464. const hideNewsHome = createCheckboxField('Hide Breaking News on Home (default: no)', 'hideNewsHome', USER_CONFIG.hideNewsHome);
  2465. form.appendChild(hideNewsHome);
  2466.  
  2467. // hide playlists on home
  2468. const hidePlaylistsHome = createCheckboxField('Hide Playlists on Home (default: no)', 'hidePlaylistsHome', USER_CONFIG.hidePlaylistsHome);
  2469. form.appendChild(hidePlaylistsHome);
  2470.  
  2471. // hide mini player
  2472. const hideMiniPlayer = createCheckboxField('Hide Mini Player (default: no)', 'hideMiniPlayer', USER_CONFIG.hideMiniPlayer);
  2473. form.appendChild(hideMiniPlayer);
  2474.  
  2475. // hide right sidebar search
  2476. const hideRightSidebarSearch = createCheckboxField('Hide Right Sidebar on Search Page (default: no)', 'hideRightSidebarSearch', USER_CONFIG.hideRightSidebarSearch);
  2477. form.appendChild(hideRightSidebarSearch);
  2478.  
  2479. // hide watched videos globally
  2480. const videosHideWatchedGlobal = createCheckboxField('Hide Watched Videos Globally (default: no)', 'videosHideWatchedGlobal', USER_CONFIG.videosHideWatchedGlobal);
  2481. form.appendChild(videosHideWatchedGlobal);
  2482.  
  2483. return form;
  2484. }
  2485.  
  2486. // color code videos
  2487. function createColorCodeVideosContent() {
  2488. const form = document.createElement('form');
  2489. form.id = 'color-code-videos-form';
  2490.  
  2491. const subPanelHeader = document.createElement('div');
  2492. subPanelHeader.classList.add('sub-panel-header');
  2493. subPanelHeader.textContent = 'Configure Color Codes for Videos on Home';
  2494. form.appendChild(subPanelHeader);
  2495.  
  2496. const infoColorCodeVideos = document.createElement('small');
  2497. infoColorCodeVideos.innerText = "These settings only apply to the Home Page: YouTube.com.";
  2498. infoColorCodeVideos.classList.add('chrome-info');
  2499. form.appendChild(infoColorCodeVideos);
  2500.  
  2501. // activate color code videos
  2502. const checkboxField = createCheckboxField('Activate Color Code Videos (default: yes)', 'colorCodeVideosEnabled', USER_CONFIG.colorCodeVideosEnabled );
  2503. form.appendChild(checkboxField);
  2504.  
  2505. const checkboxFieldWatched = createCheckboxField('Hide Watched Videos (Only on Home) (default: no)', 'videosHideWatched', USER_CONFIG.videosHideWatched );
  2506. form.appendChild(checkboxFieldWatched);
  2507.  
  2508. // opacity picker for old videos
  2509. const videosOldContainer = createSliderInputField( 'Change opacity of videos uploaded more than 6 months ago:', 'videosOldOpacity', USER_CONFIG.videosOldOpacity, '0', '1', '0.1' );
  2510. form.appendChild(videosOldContainer);
  2511.  
  2512. // color pickers for different video ages
  2513. const videosAgeContainer = document.createElement('div');
  2514. videosAgeContainer.classList.add('videos-age-container');
  2515.  
  2516. function createLabelColorPair(labelText, configKey) {
  2517. const row = document.createElement('div');
  2518. row.classList.add('videos-age-row');
  2519.  
  2520. const label = document.createElement('span');
  2521. label.classList.add('label-style-settings');
  2522. label.innerText = labelText;
  2523. row.appendChild(label);
  2524.  
  2525. const colorPicker = document.createElement('input');
  2526. colorPicker.type = 'color';
  2527. colorPicker.value = USER_CONFIG[configKey];
  2528. colorPicker.name = configKey;
  2529. row.appendChild(colorPicker);
  2530.  
  2531. videosAgeContainer.appendChild(row);
  2532. }
  2533.  
  2534. createLabelColorPair('Videos uploaded within the last 24 hours:', 'videosAgeColorPickerNewly');
  2535. createLabelColorPair('Videos uploaded within the past week:', 'videosAgeColorPickerRecent');
  2536. createLabelColorPair('Videos uploaded within the past month:', 'videosAgeColorPickerLately');
  2537. createLabelColorPair('Videos currently live:', 'videosAgeColorPickerLive');
  2538. createLabelColorPair('The word "streamed" from Videos that were live:', 'videosAgeColorPickerStreamed');
  2539. createLabelColorPair('Scheduled videos and upcoming live streams:', 'videosAgeColorPickerUpcoming');
  2540.  
  2541. form.appendChild(videosAgeContainer);
  2542.  
  2543. return form;
  2544. }
  2545. }
  2546.  
  2547. // helper function to create input fields
  2548. function createInputField(labelText, settingKey, settingValue, labelClass) {
  2549. const container = document.createElement('div');
  2550. container.classList.add('url-container');
  2551.  
  2552. const label = document.createElement('label');
  2553. label.innerText = labelText;
  2554. label.className = labelClass;
  2555. label.classList.add('label-style-settings');
  2556. container.appendChild(label);
  2557.  
  2558. const input = document.createElement('input');
  2559. const fieldInputClass = `input-field-${settingKey}`;
  2560. input.type = 'text';
  2561. input.name = settingKey;
  2562. input.value = settingValue;
  2563. input.classList.add('input-field-url');
  2564. input.classList.add(fieldInputClass);
  2565. container.appendChild(input);
  2566.  
  2567. return container;
  2568. }
  2569.  
  2570. // helper function to create select fields
  2571. function createSelectField(labelText, labelClass, settingKey, settingValue, options) {
  2572. const container = document.createElement('div');
  2573. container.classList.add('file-naming-container');
  2574.  
  2575. const label = document.createElement('label');
  2576. label.innerText = labelText;
  2577. label.className = labelClass;
  2578. label.classList.add('label-style-settings');
  2579. container.appendChild(label);
  2580.  
  2581. const select = document.createElement('div');
  2582. select.classList.add('select-file-naming');
  2583. select.innerText = options[settingValue];
  2584. select.setAttribute('tabindex', '0');
  2585. container.appendChild(select);
  2586.  
  2587. const hiddenSelect = document.createElement('select');
  2588. hiddenSelect.name = settingKey;
  2589. hiddenSelect.classList.add('hidden-select');
  2590. for (const [value, text] of Object.entries(options)) {
  2591. const option = document.createElement('option');
  2592. option.value = value;
  2593. option.text = text;
  2594. if (value === settingValue) {
  2595. option.selected = true;
  2596. }
  2597. hiddenSelect.appendChild(option);
  2598. }
  2599. container.appendChild(hiddenSelect);
  2600.  
  2601. const dropdownList = document.createElement('div');
  2602. dropdownList.classList.add('dropdown-list');
  2603. container.appendChild(dropdownList);
  2604.  
  2605. for (const [value, text] of Object.entries(options)) {
  2606. const item = document.createElement('div');
  2607. item.classList.add('dropdown-item');
  2608. item.innerText = text;
  2609. item.dataset.value = value;
  2610.  
  2611. if (value === settingValue) { item.classList.add('dropdown-item-selected'); }
  2612.  
  2613. item.addEventListener('click', () => {
  2614. const previouslySelected = dropdownList.querySelector('.dropdown-item-selected');
  2615. if (previouslySelected) {
  2616. previouslySelected.classList.remove('dropdown-item-selected');
  2617. }
  2618.  
  2619. item.classList.add('dropdown-item-selected');
  2620. select.innerText = text;
  2621. hiddenSelect.value = value;
  2622. dropdownList.classList.remove('show');
  2623. });
  2624.  
  2625. dropdownList.appendChild(item);
  2626. }
  2627.  
  2628. // open dropdown
  2629. select.addEventListener('click', (event) => {
  2630. event.stopPropagation();
  2631. dropdownList.classList.toggle('show');
  2632. });
  2633.  
  2634. // close dropdown
  2635. document.addEventListener('click', () => {
  2636. dropdownList.classList.remove('show');
  2637. });
  2638.  
  2639. return container;
  2640. }
  2641.  
  2642. // helper function to create checkbox fields
  2643. function createCheckboxField(labelText, settingKey, settingValue) {
  2644. const container = document.createElement('div');
  2645. container.classList.add('checkbox-container');
  2646.  
  2647. const label = document.createElement('label');
  2648. label.classList.add('checkbox-label');
  2649.  
  2650. const checkbox = document.createElement('input');
  2651. checkbox.type = 'checkbox';
  2652. checkbox.name = settingKey;
  2653. checkbox.checked = settingValue;
  2654. checkbox.classList.add('checkbox-field');
  2655. label.appendChild(checkbox);
  2656.  
  2657. const span = document.createElement('span');
  2658. span.innerText = labelText;
  2659. label.appendChild(span);
  2660.  
  2661. container.appendChild(label);
  2662. return container;
  2663. }
  2664.  
  2665. // helper function to create a number input fields
  2666. function createNumberInputField(labelText, settingKey, settingValue) {
  2667. const container = document.createElement('div');
  2668. container.classList.add('number-input-container');
  2669.  
  2670. const label = document.createElement('label');
  2671. label.classList.add('number-input-label');
  2672.  
  2673. const numberInput = document.createElement('input');
  2674. numberInput.type = 'number';
  2675. numberInput.name = settingKey;
  2676. numberInput.value = settingValue;
  2677. numberInput.min = 1;
  2678. numberInput.max = 20;
  2679. numberInput.step = 1;
  2680. numberInput.classList.add('number-input-field');
  2681. label.appendChild(numberInput);
  2682.  
  2683. const span = document.createElement('span');
  2684. span.innerText = labelText;
  2685. label.appendChild(span);
  2686.  
  2687. container.appendChild(label);
  2688. return container;
  2689. }
  2690.  
  2691. // helper function to create a slider fields
  2692. function createSliderInputField(labelText, settingKey, settingValue, min, max, step) {
  2693. const container = document.createElement('div');
  2694. container.classList.add('videos-old-container');
  2695.  
  2696. const label = document.createElement('span');
  2697. label.classList.add('label-style-settings');
  2698. label.innerText = labelText;
  2699. container.appendChild(label);
  2700.  
  2701. const sliderContainer = document.createElement('div');
  2702. sliderContainer.classList.add('slider-container');
  2703.  
  2704. const leftLabel = document.createElement('span');
  2705. leftLabel.innerText = min;
  2706. sliderContainer.appendChild(leftLabel);
  2707.  
  2708. const slider = document.createElement('input');
  2709. slider.type = 'range';
  2710. slider.min = min;
  2711. slider.max = max;
  2712. slider.step = step;
  2713. slider.value = settingValue;
  2714. slider.name = settingKey;
  2715. sliderContainer.appendChild(slider);
  2716.  
  2717. const rightLabel = document.createElement('span');
  2718. rightLabel.innerText = max;
  2719. sliderContainer.appendChild(rightLabel);
  2720.  
  2721. const currentValue = document.createElement('span');
  2722. currentValue.innerText = `(${parseFloat(slider.value).toFixed(1)})`;
  2723. sliderContainer.appendChild(currentValue);
  2724.  
  2725. container.appendChild(sliderContainer);
  2726.  
  2727. slider.addEventListener('input', (e) => {
  2728. const value = parseFloat(e.target.value).toFixed(1);
  2729. currentValue.innerText = `(${value})`;
  2730. });
  2731.  
  2732. return container;
  2733. }
  2734.  
  2735. // helper function to create a textarea fields
  2736. function createTextareaField(labelText, settingKey, settingValue, labelClass) {
  2737. const container = document.createElement('chatgpt-prompt');
  2738.  
  2739. const label = document.createElement('label');
  2740. label.innerText = labelText;
  2741. label.className = labelClass;
  2742. label.classList.add('label-style-settings');
  2743. container.appendChild(label);
  2744.  
  2745. const textarea = document.createElement('textarea');
  2746. textarea.name = settingKey;
  2747. textarea.value = settingValue;
  2748. textarea.classList.add('chatgpt-prompt-textarea');
  2749. container.appendChild(textarea);
  2750.  
  2751. return container;
  2752. }
  2753.  
  2754. // function to save settings
  2755. async function saveSettings() {
  2756. const form = document.getElementById('yt-transcript-settings-form');
  2757. const subPanelLinks = document.getElementById('links-in-header-form');
  2758. const subPanelCustomCSS = document.getElementById('custom-css-form');
  2759. const subPanelColor = document.getElementById('color-code-videos-form');
  2760.  
  2761. // function to ensure secure URLs
  2762. function normalizeUrl(url) {
  2763. url = url.trim();
  2764. if (/^https?:\/\//i.test(url)) {
  2765. url = url.replace(/^http:\/\//i, 'https://');
  2766. } else { url = 'https://' + url; }
  2767. return url;
  2768. }
  2769.  
  2770. // validate ChatGPT URL
  2771. let targetChatGPTUrl = form.elements.targetChatGPTUrl.value.trim();
  2772. if (targetChatGPTUrl !== '') {
  2773. USER_CONFIG.targetChatGPTUrl = normalizeUrl(targetChatGPTUrl);
  2774. } else { delete USER_CONFIG.targetChatGPTUrl; }
  2775.  
  2776. // validate NotebookLM URL
  2777. let targetNotebookLMUrl = form.elements.targetNotebookLMUrl.value.trim();
  2778. if (targetNotebookLMUrl !== '') {
  2779. USER_CONFIG.targetNotebookLMUrl = normalizeUrl(targetNotebookLMUrl);
  2780. } else { delete USER_CONFIG.targetNotebookLMUrl; }
  2781.  
  2782. // save other settings
  2783. USER_CONFIG.YouTubeTranscriptExporter = form.elements.YouTubeTranscriptExporter.checked;
  2784. USER_CONFIG.fileNamingFormat = form.elements.fileNamingFormat.value;
  2785. USER_CONFIG.includeTimestamps = form.elements.includeTimestamps.checked;
  2786. USER_CONFIG.includeChapterHeaders = form.elements.includeChapterHeaders.checked;
  2787. USER_CONFIG.openSameTab = form.elements.openSameTab.checked;
  2788. USER_CONFIG.preventBackgroundExecution = form.elements.preventBackgroundExecution.checked;
  2789. USER_CONFIG.ChatGPTPrompt = form.elements.ChatGPTPrompt.value;
  2790.  
  2791. // initialize buttonIcons if not already
  2792. USER_CONFIG.buttonIcons = USER_CONFIG.buttonIcons || {};
  2793.  
  2794. // save button icons, removing empty values to use defaults
  2795. const buttonIconDownload = form.elements.buttonIconDownload.value.trim();
  2796. const buttonIconChatGPT = form.elements.buttonIconChatGPT.value.trim();
  2797. const buttonIconNotebookLM = form.elements.buttonIconNotebookLM.value.trim();
  2798. const buttonIconSettings = form.elements.buttonIconSettings.value.trim();
  2799.  
  2800. USER_CONFIG.buttonIcons.download = buttonIconDownload;
  2801. USER_CONFIG.buttonIcons.ChatGPT = buttonIconChatGPT;
  2802. USER_CONFIG.buttonIcons.NotebookLM = buttonIconNotebookLM;
  2803. if (buttonIconSettings !== '') { USER_CONFIG.buttonIcons.settings = buttonIconSettings; } else { delete USER_CONFIG.buttonIcons.settings; }
  2804.  
  2805. // save sub panels - links in header
  2806. if (subPanelLinks) {
  2807. USER_CONFIG.buttonLeft1Text = subPanelLinks.elements.buttonLeft1Text.value;
  2808. USER_CONFIG.buttonLeft1Url = subPanelLinks.elements.buttonLeft1Url.value;
  2809. USER_CONFIG.buttonLeft2Text = subPanelLinks.elements.buttonLeft2Text.value;
  2810. USER_CONFIG.buttonLeft2Url = subPanelLinks.elements.buttonLeft2Url.value;
  2811. USER_CONFIG.buttonLeft3Text = subPanelLinks.elements.buttonLeft3Text.value;
  2812. USER_CONFIG.buttonLeft3Url = subPanelLinks.elements.buttonLeft3Url.value;
  2813. USER_CONFIG.buttonLeft4Text = subPanelLinks.elements.buttonLeft4Text.value;
  2814. USER_CONFIG.buttonLeft4Url = subPanelLinks.elements.buttonLeft4Url.value;
  2815. USER_CONFIG.buttonLeft5Text = subPanelLinks.elements.buttonLeft5Text.value;
  2816. USER_CONFIG.buttonLeft5Url = subPanelLinks.elements.buttonLeft5Url.value;
  2817. USER_CONFIG.buttonLeft6Text = subPanelLinks.elements.buttonLeft6Text.value;
  2818. USER_CONFIG.buttonLeft6Url = subPanelLinks.elements.buttonLeft6Url.value;
  2819. USER_CONFIG.buttonLeft7Text = subPanelLinks.elements.buttonLeft7Text.value;
  2820. USER_CONFIG.buttonLeft7Url = subPanelLinks.elements.buttonLeft7Url.value;
  2821. USER_CONFIG.mButtonText = subPanelLinks.elements.mButtonText.value;
  2822. USER_CONFIG.mButtonDisplay = subPanelLinks.elements.mButtonDisplay.checked;
  2823. }
  2824.  
  2825. // save sub panels - custom css
  2826. if (subPanelCustomCSS) {
  2827. USER_CONFIG.textTransform = subPanelCustomCSS.elements.textTransform.value;
  2828. USER_CONFIG.defaultFontSize = parseFloat(subPanelCustomCSS.elements.defaultFontSize.value);
  2829. USER_CONFIG.videosWatchedOpacity = parseFloat(subPanelCustomCSS.elements.videosWatchedOpacity.value);
  2830. USER_CONFIG.videosHideWatchedGlobal = subPanelCustomCSS.elements.videosHideWatchedGlobal.checked;
  2831. USER_CONFIG.videosPerRow = parseInt(subPanelCustomCSS.elements.videosPerRow.value);
  2832. USER_CONFIG.autoOpenChapters = subPanelCustomCSS.elements.autoOpenChapters.checked;
  2833. USER_CONFIG.autoOpenTranscript = subPanelCustomCSS.elements.autoOpenTranscript.checked;
  2834. USER_CONFIG.displayRemainingTime = subPanelCustomCSS.elements.displayRemainingTime.checked;
  2835. USER_CONFIG.progressBar = subPanelCustomCSS.elements.progressBar.checked;
  2836. USER_CONFIG.hideShorts = subPanelCustomCSS.elements.hideShorts.checked;
  2837. USER_CONFIG.hideVoiceSearch = subPanelCustomCSS.elements.hideVoiceSearch.checked;
  2838. USER_CONFIG.hideCreateButton = subPanelCustomCSS.elements.hideCreateButton.checked;
  2839. USER_CONFIG.hideRightSidebarSearch = subPanelCustomCSS.elements.hideRightSidebarSearch.checked;
  2840. USER_CONFIG.hideBrandText = subPanelCustomCSS.elements.hideBrandText.checked;
  2841. USER_CONFIG.disablePlayOnHover = subPanelCustomCSS.elements.disablePlayOnHover.checked;
  2842. USER_CONFIG.preventAutoplay = subPanelCustomCSS.elements.preventAutoplay.checked;
  2843. USER_CONFIG.hideEndCards = subPanelCustomCSS.elements.hideEndCards.checked;
  2844. USER_CONFIG.hideEndscreen = subPanelCustomCSS.elements.hideEndscreen.checked;
  2845. USER_CONFIG.hideJoinButton = subPanelCustomCSS.elements.hideJoinButton.checked;
  2846. USER_CONFIG.hidePlayNextButton = subPanelCustomCSS.elements.hidePlayNextButton.checked;
  2847. USER_CONFIG.smallSubscribeButton = subPanelCustomCSS.elements.smallSubscribeButton.checked;
  2848. USER_CONFIG.hideShareButton = subPanelCustomCSS.elements.hideShareButton.checked;
  2849. USER_CONFIG.hideAddComment = subPanelCustomCSS.elements.hideAddComment.checked;
  2850. USER_CONFIG.hideReplyButton = subPanelCustomCSS.elements.hideReplyButton.checked;
  2851. USER_CONFIG.hidePlaylistsHome = subPanelCustomCSS.elements.hidePlaylistsHome.checked;
  2852. USER_CONFIG.hideNewsHome = subPanelCustomCSS.elements.hideNewsHome.checked;
  2853. USER_CONFIG.progressbarColorPicker = subPanelCustomCSS.elements.progressbarColorPicker.value;
  2854. USER_CONFIG.removeScrubber = subPanelCustomCSS.elements.removeScrubber.checked;
  2855. USER_CONFIG.autoTheaterMode = subPanelCustomCSS.elements.autoTheaterMode.checked;
  2856. USER_CONFIG.channelReindirizzare = subPanelCustomCSS.elements.channelReindirizzare.checked;
  2857. USER_CONFIG.hideMiniPlayer = subPanelCustomCSS.elements.hideMiniPlayer.checked;
  2858. USER_CONFIG.closeChatWindow = subPanelCustomCSS.elements.closeChatWindow.checked;
  2859. USER_CONFIG.displayFullTitle = subPanelCustomCSS.elements.displayFullTitle.checked;
  2860. USER_CONFIG.squareSearchBar = subPanelCustomCSS.elements.squareSearchBar.checked;
  2861. USER_CONFIG.squareDesign = subPanelCustomCSS.elements.squareDesign.checked;
  2862. USER_CONFIG.compactLayout = subPanelCustomCSS.elements.compactLayout.checked;
  2863. }
  2864.  
  2865. // save sub panels - color code videos
  2866. if (subPanelColor) {
  2867. USER_CONFIG.colorCodeVideosEnabled = subPanelColor.elements.colorCodeVideosEnabled.checked;
  2868. USER_CONFIG.videosHideWatched = subPanelColor.elements.videosHideWatched.checked;
  2869. USER_CONFIG.videosOldOpacity = parseFloat(subPanelColor.elements.videosOldOpacity.value);
  2870. USER_CONFIG.videosAgeColorPickerNewly = subPanelColor.elements.videosAgeColorPickerNewly.value;
  2871. USER_CONFIG.videosAgeColorPickerRecent = subPanelColor.elements.videosAgeColorPickerRecent.value;
  2872. USER_CONFIG.videosAgeColorPickerLately = subPanelColor.elements.videosAgeColorPickerLately.value;
  2873. USER_CONFIG.videosAgeColorPickerLive = subPanelColor.elements.videosAgeColorPickerLive.value;
  2874. USER_CONFIG.videosAgeColorPickerStreamed = subPanelColor.elements.videosAgeColorPickerStreamed.value;
  2875. USER_CONFIG.videosAgeColorPickerUpcoming = subPanelColor.elements.videosAgeColorPickerUpcoming.value;
  2876. }
  2877.  
  2878. // save updated config
  2879. try {
  2880. await GM.setValue('USER_CONFIG', USER_CONFIG);
  2881.  
  2882. // close modal
  2883. document.getElementById('yt-transcript-settings-modal').style.display = 'none';
  2884. showNotification('Settings have been updated!');
  2885. setTimeout(() => { location.reload(); }, 1000);
  2886. } catch (error) {
  2887. showNotification('Error saving new user config!');
  2888. console.error("YouTubeAlchemy: Error saving user configuration:", error);
  2889. }
  2890. }
  2891.  
  2892. // export and import settings
  2893. async function exportSettings() {
  2894. try {
  2895. const scriptVersion = GM.info.script.version;
  2896. const settingsString = JSON.stringify(USER_CONFIG, null, 2);
  2897. const blob = new Blob([settingsString], { type: 'application/json' });
  2898. const url = URL.createObjectURL(blob);
  2899.  
  2900. const a = document.createElement('a');
  2901. a.href = url;
  2902. a.download = `YouTube-Alchemy_v${scriptVersion}_Backup_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
  2903. document.body.appendChild(a);
  2904. a.click();
  2905. document.body.removeChild(a);
  2906. URL.revokeObjectURL(url);
  2907.  
  2908. showNotification('Settings have been exported.');
  2909. } catch (error) {
  2910. showNotification("Error exporting settings!");
  2911. console.error("YouTubeAlchemy: Error exporting user settings:", error);
  2912. }
  2913. }
  2914.  
  2915. let fileInputSettings;
  2916. async function importSettings() {
  2917. const handleFile = (e) => {
  2918. const file = e.target.files[0];
  2919. if (!file) return;
  2920.  
  2921. const reader = new FileReader();
  2922. reader.onload = (event) => {
  2923. const fileContent = event.target.result;
  2924. try {
  2925. const importedConfig = JSON.parse(fileContent);
  2926. if (typeof importedConfig === 'object' && importedConfig !== null) {
  2927. USER_CONFIG = { ...DEFAULT_CONFIG, ...importedConfig };
  2928. GM.setValue('USER_CONFIG', USER_CONFIG);
  2929. showNotification('Settings have been imported.');
  2930. setTimeout(() => {
  2931. location.reload();
  2932. }, 1000);
  2933. } else {
  2934. showNotification('Invalid JSON format!');
  2935. }
  2936. } catch (error) {
  2937. showNotification('Invalid JSON format!');
  2938. }
  2939. };
  2940. reader.readAsText(file);
  2941. };
  2942.  
  2943. const createOrResetFileInput = () => {
  2944. if (!fileInputSettings) {
  2945. fileInputSettings = document.createElement('input');
  2946. fileInputSettings.type = 'file';
  2947. fileInputSettings.accept = 'application/json';
  2948. fileInputSettings.id = 'fileInputSettings';
  2949. fileInputSettings.style.display = 'none';
  2950. fileInputSettings.addEventListener('change', handleFile);
  2951. document.body.appendChild(fileInputSettings);
  2952. } else {
  2953. fileInputSettings.value = '';
  2954. }
  2955. };
  2956.  
  2957. createOrResetFileInput();
  2958. fileInputSettings.click();
  2959. }
  2960.  
  2961. // function to display a notification for settings change or reset
  2962. function showNotification(message) {
  2963. const overlay = document.createElement('div');
  2964. overlay.classList.add('overlay');
  2965.  
  2966. const modal = document.createElement('div');
  2967. modal.classList.add('notification');
  2968. modal.innerText = message;
  2969.  
  2970. overlay.appendChild(modal);
  2971. document.body.appendChild(overlay);
  2972.  
  2973. setTimeout(() => { overlay.remove(); }, 1000);
  2974. }
  2975.  
  2976. // function to add the YouTube Transcript Exporter buttons
  2977. function buttonLocation(buttons, callback) {
  2978. const masthead = document.querySelector('#end');
  2979. if (masthead) {
  2980. buttons.forEach(({ id, text, clickHandler, tooltip }) => {
  2981.  
  2982. // button wrapper
  2983. const buttonWrapper = document.createElement('div');
  2984. buttonWrapper.classList.add('button-wrapper');
  2985.  
  2986. // buttons
  2987. const button = document.createElement('button');
  2988. button.id = id;
  2989. button.innerText = text;
  2990. button.classList.add('button-style');
  2991. if (id === 'transcript-settings-button') {
  2992. button.classList.add('button-style-settings'); }
  2993.  
  2994. button.addEventListener('click', clickHandler);
  2995.  
  2996. // tooltip div
  2997. const tooltipDiv = document.createElement('div');
  2998. tooltipDiv.innerText = tooltip;
  2999. tooltipDiv.classList.add('button-tooltip');
  3000.  
  3001. // tooltip arrow
  3002. const arrowDiv = document.createElement('div');
  3003. arrowDiv.classList.add('button-tooltip-arrow');
  3004. tooltipDiv.appendChild(arrowDiv);
  3005.  
  3006. // show and hide tooltip on hover
  3007. let tooltipTimeout;
  3008. button.addEventListener('mouseenter', () => {
  3009. tooltipTimeout = setTimeout(() => {
  3010. tooltipDiv.style.visibility = 'visible';
  3011. tooltipDiv.style.opacity = '1';
  3012. }, 700);
  3013. });
  3014.  
  3015. button.addEventListener('mouseleave', () => {
  3016. clearTimeout(tooltipTimeout);
  3017. tooltipDiv.style.visibility = 'hidden';
  3018. tooltipDiv.style.opacity = '0';
  3019. });
  3020.  
  3021. // append button elements
  3022. buttonWrapper.appendChild(button);
  3023. buttonWrapper.appendChild(tooltipDiv);
  3024. masthead.prepend(buttonWrapper);
  3025. });
  3026. } else {
  3027. const observer = new MutationObserver((mutations, obs) => {
  3028. const masthead = document.querySelector('#end');
  3029. if (masthead) {
  3030. obs.disconnect();
  3031. if (callback) callback();
  3032. }
  3033. });
  3034. observer.observe(document.body, {
  3035. childList: true,
  3036. subtree: true
  3037. });
  3038. }
  3039. }
  3040.  
  3041. function addButton() {
  3042. if (document.querySelector('.button-wrapper')) return;
  3043.  
  3044. const buttons = [
  3045. { id: 'transcript-settings-button', text: USER_CONFIG.buttonIcons.settings, clickHandler: showSettingsModal, tooltip: 'YouTube Alchemy Settings', ariaLabel: 'YouTube Alchemy Settings.' },
  3046. { id: 'transcript-download-button', text: USER_CONFIG.buttonIcons.download, clickHandler: handleDownloadClick, tooltip: 'Download Transcript as a Text File', ariaLabel: 'Download Transcript as a Text File.' },
  3047. { id: 'transcript-ChatGPT-button', text: USER_CONFIG.buttonIcons.ChatGPT, clickHandler: handleChatGPTClick, tooltip: 'Copy Transcript with a Prompt and Open ChatGPT', ariaLabel: 'Copy Transcript to Clipboard with a Prompt and Open ChatGPT.' },
  3048. { id: 'transcript-NotebookLM-button', text: USER_CONFIG.buttonIcons.NotebookLM, clickHandler: handleNotebookLMClick, tooltip: 'Copy Transcript and Open NotebookLM', ariaLabel: 'Copy Transcript to Clipboard and Open NotebookLM.' }
  3049. ];
  3050.  
  3051. const buttonsToAdd = buttons.filter(button => button.id === 'transcript-settings-button' || (button.text && button.text.trim() !== ''));
  3052.  
  3053. buttonLocation(buttonsToAdd, addButton);
  3054. }
  3055.  
  3056. function addSettingsButton() {
  3057. if (document.querySelector('.button-wrapper')) return;
  3058.  
  3059. const buttons = [ { id: 'transcript-settings-button', text: USER_CONFIG.buttonIcons.settings, clickHandler: showSettingsModal, tooltip: 'YouTube Alchemy Settings', ariaLabel: 'YouTube Alchemy Settings.' }, ];
  3060.  
  3061. buttonLocation(buttons, addSettingsButton);
  3062. }
  3063.  
  3064. // functions to handle the button clicks
  3065. function handleChatGPTClick() { handleTranscriptAction(function() { selectAndCopyTranscript('ChatGPT'); }); }
  3066. function handleNotebookLMClick() { handleTranscriptAction(function() { selectAndCopyTranscript('NotebookLM'); }); }
  3067. function handleDownloadClick() { handleTranscriptAction(downloadTranscriptAsText); }
  3068.  
  3069. // function to check for a transcript
  3070. function handleTranscriptAction(callback) {
  3071.  
  3072. // check if the transcript button exists or at least the div
  3073. const transcriptButton = document.querySelector('#button-container button[aria-label="Show transcript"]');
  3074. if (transcriptButton) {
  3075. } else {
  3076. const transcriptSection = document.querySelector('ytd-video-description-transcript-section-renderer');
  3077. if (transcriptSection) {
  3078. } else {
  3079. alert('Transcript unavailable or cannot be found.\nEnsure the "Show transcript" button exists.\nReload this page to try again.');
  3080. console.log("YouTubeAlchemy: Transcript button not found. Subtitles/closed captions unavailable or language unsupported. Reload this page to try again.");
  3081. return;
  3082. }
  3083. }
  3084.  
  3085. // check if the transcript has loaded
  3086. const transcriptItems = document.querySelectorAll('ytd-transcript-segment-list-renderer ytd-transcript-segment-renderer');
  3087. if (transcriptItems.length > 0) {
  3088. callback();
  3089. } else {
  3090. alert('Transcript has not loaded successfully.\nReload this page to try again.');
  3091. console.log("YouTubeAlchemy: Transcript has not loaded.");
  3092. return;
  3093. }
  3094. }
  3095.  
  3096. // function to get video information
  3097. function getVideoInfo() {
  3098. //const ytTitle = document.querySelector('#title yt-formatted-string')?.textContent.trim() || 'N/A';
  3099. const ytTitle = document.querySelector('div#title h1 > yt-formatted-string')?.textContent.trim() || 'N/A';
  3100. //const channelName = document.querySelector('ytd-channel-name#channel-name yt-formatted-string#text a')?.textContent.trim() || 'N/A';
  3101. const channelName = document.querySelector( 'ytd-video-owner-renderer ytd-channel-name#channel-name yt-formatted-string#text a' )?.textContent.trim() || 'N/A';
  3102. const uploadDate = document.querySelector('ytd-video-primary-info-renderer #info-strings yt-formatted-string')?.textContent.trim() || 'N/A';
  3103. const videoURL = window.location.href;
  3104.  
  3105. return { ytTitle, channelName, uploadDate, videoURL };
  3106. }
  3107.  
  3108. // function to get the transcript text
  3109. function getTranscriptText() {
  3110. const transcriptContainer = document.querySelector('ytd-transcript-segment-list-renderer #segments-container');
  3111. if (!transcriptContainer) {
  3112. //console.error("YouTubeAlchemy: Transcript container not found.");
  3113. return '';
  3114. }
  3115.  
  3116. const transcriptElements = transcriptContainer.children;
  3117. let transcriptLines = [];
  3118.  
  3119. Array.from(transcriptElements).forEach(element => {
  3120. if (element.tagName === 'YTD-TRANSCRIPT-SECTION-HEADER-RENDERER') {
  3121.  
  3122. // chapter header segment
  3123. if (USER_CONFIG.includeChapterHeaders) {
  3124. const chapterTitleElement = element.querySelector('h2 > span');
  3125. if (chapterTitleElement) {
  3126. const chapterTitle = chapterTitleElement.textContent.trim();
  3127. transcriptLines.push(`\nChapter: ${chapterTitle}`);
  3128. }
  3129. }
  3130. } else if (element.tagName === 'YTD-TRANSCRIPT-SEGMENT-RENDERER') {
  3131.  
  3132. // transcript segment
  3133. const timeElement = element.querySelector('.segment-timestamp');
  3134. const textElement = element.querySelector('.segment-text');
  3135. if (timeElement && textElement) {
  3136. const time = timeElement.textContent.trim();
  3137. const text = textElement.innerText.trim();
  3138. if (USER_CONFIG.includeTimestamps) {
  3139. transcriptLines.push(`${time} ${text}`);
  3140. } else { transcriptLines.push(`${text}`); }
  3141. }
  3142. }
  3143. });
  3144.  
  3145. return transcriptLines.join('\n');
  3146. }
  3147.  
  3148. // function to select and copy the transcript into the clipboard
  3149. function selectAndCopyTranscript(target) {
  3150. const transcriptText = getTranscriptText();
  3151. const { ytTitle, channelName, uploadDate, videoURL } = getVideoInfo();
  3152.  
  3153. let finalText = '';
  3154. let targetUrl = '';
  3155.  
  3156. if (target === 'ChatGPT') {
  3157. finalText = `YouTube Transcript:\n${transcriptText.trimStart()}\n\n\nAdditional Information about the YouTube Video:\nTitle: ${ytTitle}\nChannel: ${channelName}\nUpload Date: ${uploadDate}\nURL: ${videoURL}\n\n\nTask Instructions:\n${USER_CONFIG.ChatGPTPrompt}`;
  3158. targetUrl = USER_CONFIG.targetChatGPTUrl;
  3159. } else if (target === 'NotebookLM') {
  3160. finalText = `Information about the YouTube Video:\nTitle: ${ytTitle}\nChannel: ${channelName}\nUpload Date: ${uploadDate}\nURL: ${videoURL}\n\n\nYouTube Transcript:\n${transcriptText.trimStart()}`;
  3161. targetUrl = USER_CONFIG.targetNotebookLMUrl;
  3162. }
  3163.  
  3164. navigator.clipboard.writeText(finalText).then(() => {
  3165. showNotification('Transcript copied. Opening website . . .');
  3166. if (USER_CONFIG.openSameTab) { window.open(targetUrl, '_self');
  3167. } else { window.open(targetUrl, '_blank'); }
  3168. });
  3169. }
  3170.  
  3171. // function to get the formatted transcript with video details for the text file
  3172. function getFormattedTranscript() {
  3173. const transcriptText = getTranscriptText();
  3174. const { ytTitle, channelName, uploadDate, videoURL } = getVideoInfo();
  3175.  
  3176. return `Information about the YouTube Video:\nTitle: ${ytTitle}\nChannel: ${channelName}\nUpload Date: ${uploadDate}\nURL: ${videoURL}\n\n\nYouTube Transcript:\n${transcriptText.trimStart()}`;
  3177. }
  3178.  
  3179. // function to download the transcript as a text file
  3180. function downloadTranscriptAsText() {
  3181. const finalText = getFormattedTranscript();
  3182. const { ytTitle, channelName, uploadDate } = getVideoInfo();
  3183. const blob = new Blob([finalText], { type: 'text/plain' });
  3184.  
  3185. const sanitize = str => str.replace(/[<>:"/\\|?*]+/g, '');
  3186. const uploadDateFormatted = new Date(uploadDate).toLocaleDateString("en-CA");
  3187.  
  3188. // naming of text file based on user setting
  3189. let fileName = '';
  3190. switch (USER_CONFIG.fileNamingFormat) {
  3191. case 'title-channel': fileName = `${sanitize(ytTitle)} - ${sanitize(channelName)}.txt`; break;
  3192. case 'channel-title': fileName = `${sanitize(channelName)} - ${sanitize(ytTitle)}.txt`; break;
  3193. case 'date-title-channel': fileName = `${sanitize(uploadDateFormatted)} - ${sanitize(ytTitle)} - ${sanitize(channelName)}.txt`; break;
  3194. case 'date-channel-title': fileName = `${sanitize(uploadDateFormatted)} - ${sanitize(channelName)} - ${sanitize(ytTitle)}.txt`; break;
  3195. default: fileName = `${sanitize(ytTitle)} - ${sanitize(channelName)}.txt`;
  3196. }
  3197.  
  3198. const url = URL.createObjectURL(blob);
  3199.  
  3200. // create a temporary anchor element to trigger the download
  3201. const a = document.createElement('a');
  3202. a.href = url;
  3203. a.download = fileName;
  3204. document.body.appendChild(a);
  3205. a.click();
  3206.  
  3207. // clean up
  3208. document.body.removeChild(a);
  3209. URL.revokeObjectURL(url);
  3210. showNotification('File has been downloaded.');
  3211. }
  3212.  
  3213.  
  3214. // function to preload the transcript
  3215. function preLoadTranscript() {
  3216. return new Promise((resolve, reject) => {
  3217. if (isLiveVideo) {
  3218. showNotificationError("Live Stream, No Transcript");
  3219. reject();
  3220. return;
  3221. }
  3222.  
  3223. if (!hasTranscriptPanel) {
  3224. showNotificationError("Transcript Not Available");
  3225. reject();
  3226. return;
  3227. }
  3228.  
  3229. const masthead = document.querySelector("#end");
  3230. const notification = document.createElement("div");
  3231. notification.classList.add("notification-error", "loading");
  3232. const textSpan = document.createElement("span");
  3233. textSpan.textContent = "Transcript Is Loading";
  3234. notification.appendChild(textSpan);
  3235. masthead.prepend(notification);
  3236.  
  3237. if (!USER_CONFIG.autoOpenTranscript) transcriptPanel.classList.add("transcript-preload");
  3238. if (!USER_CONFIG.autoOpenTranscript) transcriptPanel.setAttribute("visibility", "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED");
  3239.  
  3240. let loaded = false;
  3241.  
  3242. const observer = new MutationObserver(() => {
  3243. const transcriptItems = transcriptPanel.querySelectorAll("ytd-transcript-segment-renderer");
  3244. if (transcriptItems.length > 0) {
  3245. loaded = true;
  3246. cleanup(false);
  3247. clearTimeout(fallbackTimer);
  3248. observer.disconnect();
  3249. resolve();
  3250. }
  3251. });
  3252.  
  3253. observer.observe(transcriptPanel, { childList: true, subtree: true });
  3254.  
  3255. const fallbackTimer = setTimeout(() => {
  3256. if (!loaded) {
  3257. console.error("YouTubeAlchemy: The transcript took too long to load. Reload this page to try again.");
  3258. observer.disconnect();
  3259. cleanup(true);
  3260. reject();
  3261. }
  3262. }, 6000);
  3263.  
  3264. function cleanup(failed) {
  3265. notification.remove();
  3266. if (!USER_CONFIG.autoOpenTranscript) transcriptPanel.classList.remove("transcript-preload");
  3267. if (!USER_CONFIG.autoOpenTranscript) transcriptPanel.setAttribute("visibility", "ENGAGEMENT_PANEL_VISIBILITY_HIDDEN");
  3268. if (failed) { showNotificationError("Transcript Failed to Load"); }
  3269. }
  3270. });
  3271. }
  3272.  
  3273. // function to display a notification if transcript cannot be found
  3274. function showNotificationError(message) {
  3275. const masthead = document.querySelector('#end');
  3276. const notification = document.createElement('div');
  3277. notification.textContent = message;
  3278. notification.classList.add('notification-error');
  3279.  
  3280. masthead.prepend(notification);
  3281.  
  3282. if (document.visibilityState === 'hidden') {
  3283. document.addEventListener('visibilitychange', function handleVisibilityChange() {
  3284. if (document.visibilityState === 'visible') {
  3285. document.removeEventListener('visibilitychange', handleVisibilityChange);
  3286. setTimeout(() => notification.remove(), 3000);
  3287. }
  3288. });
  3289. } else { setTimeout(() => notification.remove(), 3000); }
  3290. }
  3291.  
  3292. // helper function to switch theater mode
  3293. function toggleTheaterMode() {
  3294. const event = new KeyboardEvent('keydown', {
  3295. key: 'T',
  3296. code: 'KeyT',
  3297. keyCode: 84,
  3298. bubbles: true,
  3299. cancelable: true
  3300. });
  3301.  
  3302. document.dispatchEvent(event);
  3303. }
  3304.  
  3305. // function to display the remaining time based on playback speed
  3306. function remainingTime() {
  3307. const STREAM_SELECTOR = '.video-stream.html5-main-video';
  3308. const CONTAINER_SELECTOR = '#columns #primary #below';
  3309. const FULLSCREEN_CONTAINER_SELECTOR = '#movie_player > div.ytp-chrome-bottom';
  3310.  
  3311. // function to format seconds
  3312. function formatTime(seconds) {
  3313. if (!isFinite(seconds) || seconds < 0) seconds = 0;
  3314. const h = Math.floor(seconds / 3600);
  3315. const m = Math.floor((seconds % 3600) / 60);
  3316. const s = Math.floor(seconds % 60);
  3317. return h > 0
  3318. ? `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
  3319. : `${m}:${s.toString().padStart(2, '0')}`;
  3320. }
  3321.  
  3322. const element = document.createElement('div');
  3323. element.classList.add('remaining-time-container');
  3324.  
  3325. const textNode = document.createTextNode('');
  3326. element.appendChild(textNode);
  3327.  
  3328. const initialContainer = document.querySelector(CONTAINER_SELECTOR);
  3329. if (initialContainer) { initialContainer.prepend(element); }
  3330.  
  3331. // hide for live stream
  3332. if (isLiveVideo) {
  3333. element.classList.add('live');
  3334. } else {
  3335. element.classList.remove('live');
  3336. }
  3337.  
  3338. // dynamic placement based on fullscreen mode
  3339. const updateContainer = () => {
  3340. const container = document.querySelector(CONTAINER_SELECTOR);
  3341. const fullscreenContainer = document.querySelector(FULLSCREEN_CONTAINER_SELECTOR);
  3342. const remainingTimeContainer = document.querySelector('.remaining-time-container');
  3343.  
  3344. if (document.fullscreenElement && fullscreenContainer) {
  3345. if (remainingTimeContainer && remainingTimeContainer.parentNode !== fullscreenContainer) {
  3346. fullscreenContainer.appendChild(remainingTimeContainer);
  3347. }
  3348. } else if (container) {
  3349. if (getComputedStyle(container).position === 'static') {
  3350. container.style.position = 'relative';
  3351. }
  3352. if (remainingTimeContainer && remainingTimeContainer.parentNode !== container) {
  3353. container.prepend(remainingTimeContainer);
  3354. }
  3355. }
  3356. };
  3357.  
  3358. document.addEventListener('fullscreenchange', () => { setTimeout(updateContainer, 250); });
  3359. updateContainer();
  3360.  
  3361. // time updates
  3362. const video = document.querySelector(STREAM_SELECTOR);
  3363. if (video) {
  3364. video.ontimeupdate = () => {
  3365. const duration = video.duration;
  3366. const currentTime = video.currentTime;
  3367. const playbackRate = video.playbackRate || 1;
  3368. const remaining = (duration - currentTime) / playbackRate;
  3369. const watchedPercent = duration ? Math.round((currentTime / duration) * 100) + '%' : '0%';
  3370. const totalFormatted = formatTime(duration);
  3371. const elapsedFormatted = formatTime(currentTime);
  3372. const remainingFormatted = formatTime(remaining);
  3373.  
  3374. textNode.data = `total: ${totalFormatted} | elapsed: ${elapsedFormatted} watched: ${watchedPercent} remaining: ${remainingFormatted} (${playbackRate}x)`;
  3375. };
  3376. }
  3377. }
  3378.  
  3379. // function to keep the progress bar visible with chapters container
  3380. function keepProgressBarVisible() {
  3381. document.documentElement.classList.add('progressBar');
  3382.  
  3383. const player = document.querySelector('.html5-video-player');
  3384. const video = document.querySelector('video[src]');
  3385. const chaptersContainer = player && player.querySelector('.ytp-chapters-container');
  3386. const progressBarContainer = player && player.querySelector('.ytp-progress-bar-container');
  3387.  
  3388. if (!player || !video ) { console.error("YouTubeAlchemy: ProgressBar: A Required querySelector Not Found."); return; }
  3389. if (!progressBarContainer) { console.error("YouTubeAlchemy: ProgressBar: Progress Bar Container Not Found."); return; }
  3390.  
  3391. const bar = document.createElement('div');
  3392. bar.id = 'progress-bar-bar';
  3393.  
  3394. const progress = document.createElement('div');
  3395. progress.id = 'progress-bar-progress';
  3396.  
  3397. const buffer = document.createElement('div');
  3398. buffer.id = 'progress-bar-buffer';
  3399.  
  3400. const startDiv = document.createElement('div');
  3401. startDiv.id = 'progress-bar-start';
  3402.  
  3403. const endDiv = document.createElement('div');
  3404. endDiv.id = 'progress-bar-end';
  3405.  
  3406. player.appendChild(bar);
  3407. bar.appendChild(buffer);
  3408. bar.appendChild(progress);
  3409. player.appendChild(startDiv);
  3410. player.appendChild(endDiv);
  3411.  
  3412. progress.style.transform = 'scaleX(0)';
  3413.  
  3414. // live stream check
  3415. if (isLiveVideo) {
  3416. bar.classList.remove('active');
  3417. startDiv.classList.remove('active');
  3418. endDiv.classList.remove('active');
  3419. } else {
  3420. bar.classList.add('active');
  3421. startDiv.classList.add('active');
  3422. endDiv.classList.add('active');
  3423. }
  3424.  
  3425. function animateProgress() {
  3426. const fraction = video.currentTime / video.duration;
  3427. progress.style.transform = `scaleX(${fraction})`;
  3428. requestAnimationFrame(animateProgress);
  3429. }
  3430.  
  3431. requestAnimationFrame(animateProgress);
  3432.  
  3433. function renderBuffer() {
  3434. for (let i = video.buffered.length - 1; i >= 0; i--) {
  3435. if (video.currentTime < video.buffered.start(i)) continue;
  3436. buffer.style.transform = `scaleX(${video.buffered.end(i) / video.duration})`;
  3437. break;
  3438. }
  3439. }
  3440.  
  3441. video.addEventListener('progress', renderBuffer);
  3442. video.addEventListener('seeking', renderBuffer);
  3443.  
  3444. // chapters container
  3445. let cachedMaskImage = null;
  3446.  
  3447. function updateLayout() {
  3448. const initialWidth = progressBarContainer.getBoundingClientRect().width;
  3449.  
  3450. let attempts = 0;
  3451. const maxAttempts = 6;
  3452.  
  3453. const waitForSizeChange = new Promise((resolve) => {
  3454. const intervalId = setInterval(() => {
  3455. const currentWidth = progressBarContainer.getBoundingClientRect().width;
  3456.  
  3457. if (currentWidth !== initialWidth) { clearInterval(intervalId); resolve(); }
  3458. else if (++attempts >= maxAttempts) { clearInterval(intervalId); resolve(); }
  3459. }, 250);
  3460. });
  3461.  
  3462. waitForSizeChange.then(() => {
  3463. const playerRect = player.getBoundingClientRect();
  3464. const progressBarRect = progressBarContainer.getBoundingClientRect();
  3465. const progressBarWidth = progressBarRect.width;
  3466.  
  3467. bar.style.position = 'absolute';
  3468. bar.style.left = (progressBarRect.left - playerRect.left) + 'px';
  3469. bar.style.width = progressBarWidth + 'px';
  3470.  
  3471. if (chaptersContainer) {
  3472. const chapters = chaptersContainer.querySelectorAll('.ytp-chapter-hover-container');
  3473. if (chapters.length) {
  3474. const svgWidth = 100;
  3475. const svgHeight = 10;
  3476. let rects = '';
  3477.  
  3478. chapters.forEach((chapter) => {
  3479. const rect = chapter.getBoundingClientRect();
  3480. const startPx = rect.left - progressBarRect.left;
  3481. const chapterWidth = rect.width;
  3482.  
  3483. const startPerc = (startPx / progressBarWidth) * svgWidth;
  3484. const widthPerc = (chapterWidth / progressBarWidth) * svgWidth;
  3485.  
  3486. rects += `<rect x="${startPerc}" y="0" width="${widthPerc}" height="${svgHeight}" fill="white"/>`;
  3487. });
  3488.  
  3489. const svg = `<svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">${rects}</svg>`;
  3490. const encoded = encodeURIComponent(svg).replace(/%20/g, ' ');
  3491. cachedMaskImage = `url("data:image/svg+xml;utf8,${encoded}")`;
  3492.  
  3493. bar.style.maskImage = cachedMaskImage;
  3494. bar.style.webkitMaskImage = cachedMaskImage;
  3495. bar.style.maskRepeat = 'no-repeat';
  3496. bar.style.webkitMaskRepeat = 'no-repeat';
  3497. bar.style.maskSize = '100% 100%';
  3498. bar.style.webkitMaskSize = '100% 100%';
  3499. } else {
  3500. if (cachedMaskImage) {
  3501. bar.style.maskImage = '';
  3502. bar.style.webkitMaskImage = '';
  3503. cachedMaskImage = null;
  3504. }
  3505. }
  3506. }
  3507. });
  3508. }
  3509.  
  3510. // handle layout changes
  3511. document.addEventListener('yt-set-theater-mode-enabled', () => { updateLayout(); });
  3512. window.addEventListener('resize', () => { updateLayout(); });
  3513.  
  3514. // initialization
  3515. renderBuffer();
  3516. updateLayout();
  3517. }
  3518.  
  3519. // close live chat initially
  3520. function closeLiveChat() {
  3521. const chatFrame = document.querySelector('ytd-live-chat-frame');
  3522. if (!chatFrame) return;
  3523.  
  3524. const retryInterval = 250;
  3525. const maxRetries = 12;
  3526. let iframeAttempts = 0;
  3527. let buttonAttempts = 0;
  3528.  
  3529. function tryCloseChat() {
  3530. const iframe = document.querySelector('#chatframe');
  3531. if (!iframe?.contentWindow?.document) {
  3532. if (iframeAttempts < maxRetries) {
  3533. iframeAttempts++;
  3534. setTimeout(tryCloseChat, retryInterval);
  3535. } else {
  3536. document.body.classList.remove('CentAnni-close-live-chat');
  3537. initialRun = false;
  3538. }
  3539. return;
  3540. }
  3541.  
  3542. const button = iframe.contentWindow.document.querySelector('#close-button button');
  3543. if (!button) {
  3544. if (buttonAttempts < maxRetries) {
  3545. buttonAttempts++;
  3546. setTimeout(tryCloseChat, retryInterval);
  3547. } else {
  3548. document.body.classList.remove('CentAnni-close-live-chat');
  3549. initialRun = false;
  3550. }
  3551. return;
  3552. }
  3553.  
  3554. button.click();
  3555. setTimeout(() => { document.body.classList.remove('CentAnni-close-live-chat'); }, 250);
  3556. initialRun = false;
  3557. }
  3558.  
  3559. tryCloseChat();
  3560. }
  3561.  
  3562. // function to automatically open the chapter panel
  3563. function openChapters() {
  3564. if (hasChapterPanel)
  3565. chapterPanel.setAttribute('visibility', 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED');
  3566. }
  3567.  
  3568. // function to automatically open the transcript panel
  3569. function openTranscript() {
  3570. if (hasTranscriptPanel)
  3571. transcriptPanel.setAttribute('visibility', 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED');
  3572. }
  3573.  
  3574. // sidebar and links in header
  3575. function buttonsLeftHeader() {
  3576. function openSidebar() {
  3577. const guideButton = document.querySelector('#guide-button button');
  3578. if (guideButton) {
  3579. guideButton.click();
  3580. }
  3581. }
  3582.  
  3583. // create sidebar button
  3584. function createButton(text, onClick) {
  3585. const btn = document.createElement('button');
  3586. btn.textContent = text;
  3587. btn.classList.add('buttons-left');
  3588. btn.addEventListener('click', (e) => {
  3589. e.preventDefault();
  3590. onClick();
  3591. });
  3592. return btn;
  3593. }
  3594.  
  3595. // create links
  3596. function createLink(text, url) {
  3597. const link = document.createElement('a');
  3598. link.textContent = text;
  3599. link.classList.add('buttons-left');
  3600. link.href = url;
  3601. return link;
  3602. }
  3603.  
  3604. const masthead = document.querySelector('ytd-masthead'); if (!masthead) return;
  3605. const container = masthead.querySelector('#container #start'); if (!container) return;
  3606.  
  3607. const isHideSidebarChecked = USER_CONFIG.mButtonDisplay;
  3608.  
  3609. if (container.querySelector('.buttons-left')) { return; }
  3610.  
  3611. // adding the buttons
  3612. const buttonsConfig = [
  3613. { type: 'button', text: USER_CONFIG.mButtonText, onClick: openSidebar },
  3614. { type: 'link', text: USER_CONFIG.buttonLeft1Text, url: USER_CONFIG.buttonLeft1Url },
  3615. { type: 'link', text: USER_CONFIG.buttonLeft2Text, url: USER_CONFIG.buttonLeft2Url },
  3616. { type: 'link', text: USER_CONFIG.buttonLeft3Text, url: USER_CONFIG.buttonLeft3Url },
  3617. { type: 'link', text: USER_CONFIG.buttonLeft4Text, url: USER_CONFIG.buttonLeft4Url },
  3618. { type: 'link', text: USER_CONFIG.buttonLeft5Text, url: USER_CONFIG.buttonLeft5Url },
  3619. { type: 'link', text: USER_CONFIG.buttonLeft6Text, url: USER_CONFIG.buttonLeft6Url },
  3620. { type: 'link', text: USER_CONFIG.buttonLeft7Text, url: USER_CONFIG.buttonLeft7Url },
  3621. ];
  3622.  
  3623. buttonsConfig.forEach(config => {
  3624. if (config.text && config.text.trim() !== '') {
  3625. let element;
  3626. if (config.type === 'button') {
  3627. if (isHideSidebarChecked) {
  3628. element = createButton(config.text, config.onClick);
  3629. if (config.text === DEFAULT_CONFIG.mButtonText) {
  3630. element.style.display = 'inline-block';
  3631. element.style.fontSize = '25px';
  3632. element.style.margin = '0';
  3633. element.style.padding = '0 0 5px 0';
  3634. element.style.transform = 'scaleX(1.25)';
  3635. }
  3636. }
  3637. } else if (config.type === 'link') {
  3638. element = createLink(config.text, config.url);
  3639. }
  3640. if (element) {
  3641. container.appendChild(element);
  3642. }
  3643. }
  3644. });
  3645. }
  3646.  
  3647. // color code videos on home
  3648. function homeColorCodeVideos() {
  3649. // define age categories
  3650. const categories = {
  3651. live: ['watching'],
  3652. streamed: ['Streamed'],
  3653. upcoming: ['waiting', 'scheduled for'],
  3654. newly: ['1 day ago', 'hours ago', 'hour ago', 'minutes ago', 'minute ago', 'seconds ago', 'second ago'],
  3655. recent: ['1 week ago', '7 days ago', '6 days ago', '5 days ago', '4 days ago', '3 days ago', '2 days ago'],
  3656. lately: ['1 month ago', 'weeks ago', '14 days ago', '13 days ago', '12 days ago', '11 days ago', '10 days ago', '9 days ago', '8 days ago'],
  3657. old: ['years ago', '1 year ago', '12 months ago', '11 months ago', '10 months ago', '9 months ago', '8 months ago', '7 months ago']
  3658. };
  3659.  
  3660. function processVideos() {
  3661. document.querySelectorAll('[class*="ytd-video-meta-block"]').forEach(el => {
  3662. const textContent = el.textContent.trim().toLowerCase();
  3663. for (const [className, ages] of Object.entries(categories)) {
  3664. if (ages.some(age => textContent.includes(age.toLowerCase()))) {
  3665. const videoContainer = el.closest('ytd-rich-item-renderer');
  3666. if (videoContainer && !videoContainer.classList.contains(`CentAnni-style-${className}-video`)) {
  3667. videoContainer.classList.add(`CentAnni-style-${className}-video`);
  3668. }
  3669. }
  3670. }
  3671. });
  3672.  
  3673. document.querySelectorAll('span.ytd-video-meta-block').forEach(el => {
  3674. const text = el.textContent;
  3675. const videoContainer = el.closest('ytd-rich-item-renderer');
  3676. if (!videoContainer) return;
  3677.  
  3678. if (/Scheduled for/i.test(text) && !videoContainer.classList.contains('CentAnni-style-upcoming-video')) {
  3679. videoContainer.classList.add('CentAnni-style-upcoming-video');
  3680. }
  3681.  
  3682. if (/Streamed/i.test(text) && !el.querySelector('.CentAnni-style-streamed-text')) {
  3683. el.childNodes.forEach(node => {
  3684. if (node.nodeType === Node.TEXT_NODE && /Streamed/i.test(node.nodeValue)) {
  3685. const span = document.createElement('span');
  3686. span.className = 'CentAnni-style-streamed-text';
  3687. span.textContent = node.nodeValue.match(/Streamed/i)[0];
  3688. const rest = document.createTextNode(node.nodeValue.replace(/Streamed/i, ''));
  3689. el.replaceChild(rest, node);
  3690. el.insertBefore(span, rest);
  3691. }
  3692. });
  3693. }
  3694. });
  3695. }
  3696.  
  3697. processVideos();
  3698.  
  3699. document.addEventListener('yt-service-request-sent', () => {
  3700. setTimeout(() => { processVideos(); }, 2000);
  3701. });
  3702. }
  3703.  
  3704. // initiate the script
  3705. let ultimoURL = null;
  3706. let lastVideoURL = null;
  3707. let lastVideoID = null;
  3708. let videoURL = null;
  3709. let videoID = null;
  3710. let isVideoPage = null;
  3711. let isLiveVideo = null;
  3712. let isLiveStream = null;
  3713. let isChannelHome = null;
  3714. let initialRun = null;
  3715. let chatEnabled = null;
  3716. let isTheaterMode = null;
  3717. let hasChapterPanel = null;
  3718. let chapterPanel = null;
  3719. let hasTranscriptPanel = null;
  3720. let transcriptPanel = null;
  3721. let hasChatPanel = null;
  3722. let isFullscreen = null;
  3723.  
  3724. async function initializeAlchemy() {
  3725. if (USER_CONFIG.preventBackgroundExecution) { await chromeUserWait(); }
  3726. if (USER_CONFIG.channelReindirizzare && isChannelHome) channelRedirect();
  3727. buttonsLeftHeader();
  3728.  
  3729. if (isVideoPage || isLiveStream) {
  3730. liveVideoCheck();
  3731. fullscreenCheck();
  3732. theaterModeCheck();
  3733. chapterPanelCheck();
  3734. transcriptPanelCheck();
  3735. if (USER_CONFIG.closeChatWindow) setTimeout(() => { chatWindowCheck(); }, 500);
  3736. if (USER_CONFIG.autoTheaterMode && !isTheaterMode) toggleTheaterMode();
  3737. if (USER_CONFIG.displayRemainingTime && !isLiveVideo &&!isLiveStream) remainingTime();
  3738. if (USER_CONFIG.progressBar && !isLiveVideo &&!isLiveStream) keepProgressBarVisible();
  3739. if (USER_CONFIG.autoOpenChapters && hasChapterPanel) openChapters();
  3740. if (USER_CONFIG.autoOpenTranscript && hasTranscriptPanel) openTranscript();
  3741.  
  3742. //YouTube Transcript Exporter
  3743. let transcriptLoaded = false;
  3744. if (USER_CONFIG.YouTubeTranscriptExporter) {
  3745. try { await preLoadTranscript(); transcriptLoaded = true; }
  3746. catch (error) { setTimeout(() => { addSettingsButton(); }, 3000); }
  3747. if (transcriptLoaded) { addButton(); }
  3748. } else { addSettingsButton(); }
  3749. //console.log("YouTubeAlchemy: YouTube Alchemy Initialized: On Video Page.");
  3750. } else {
  3751. addSettingsButton();
  3752. //console.log("YouTubeAlchemy: YouTube Alchemy Initialized: On Other Page.");
  3753. if (window.location.href !== "https://www.youtube.com/") return;
  3754. if (USER_CONFIG.colorCodeVideosEnabled) { homeColorCodeVideos(); }
  3755. }
  3756. }
  3757.  
  3758. // YouTube navigation handler
  3759. function handleYouTubeNavigation() {
  3760. //console.log("YouTubeAlchemy: Event Listner Arrived");
  3761. const currentVideoURL = window.location.href;
  3762. if (currentVideoURL !== ultimoURL) {
  3763. ultimoURL = currentVideoURL;
  3764. //console.log("YouTubeAlchemy: Only One Survived");
  3765. loadCSSsettings();
  3766. initialRun = true;
  3767. isVideoPage = /^https:\/\/.*\.youtube\.com\/watch\?v=/.test(currentVideoURL);
  3768. isLiveStream = /^https:\/\/.*\.youtube\.com\/live\/[a-zA-Z0-9_-]+/.test(currentVideoURL);
  3769. isChannelHome = /^https:\/\/.*\.youtube\.com\/@[a-zA-Z0-9_-]+$/.test(currentVideoURL);
  3770.  
  3771. if (isVideoPage || isLiveStream) {
  3772. lastVideoURL = videoURL;
  3773. lastVideoID = videoID;
  3774.  
  3775. videoURL = window.location.href;
  3776. const urlParams = new URLSearchParams(window.location.search);
  3777. videoID = urlParams.get('v');
  3778. }
  3779.  
  3780. setTimeout(() => { initializeAlchemy(); }, 500);
  3781. }
  3782. }
  3783.  
  3784. // theater mode check
  3785. function theaterModeCheck() {
  3786. const watchFlexy = document.querySelector('ytd-watch-flexy');
  3787.  
  3788. if (watchFlexy?.hasAttribute('theater')) {
  3789. isTheaterMode = true;
  3790. } else if (watchFlexy?.hasAttribute('default-layout')) {
  3791. isTheaterMode = false;
  3792. }
  3793. }
  3794.  
  3795. // fullscreen check
  3796. function fullscreenCheck() {
  3797. const player = document.getElementById('movie_player');
  3798. if (player && player.classList.contains('ytp-fullscreen')) {
  3799. isFullscreen = true;
  3800. } else {
  3801. isFullscreen = false;
  3802. }
  3803. }
  3804.  
  3805. // live stream check
  3806. function liveVideoCheck() {
  3807. const timeDisplay = document.querySelector('.ytp-time-display');
  3808. isLiveVideo = !!timeDisplay?.classList.contains('ytp-live');
  3809. }
  3810.  
  3811. // chapter panel check
  3812. function chapterPanelCheck() {
  3813. chapterPanel = document.querySelector( 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"]' ) || document.querySelector( 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-auto-chapters"]' );
  3814. hasChapterPanel = !!chapterPanel;
  3815. }
  3816.  
  3817. // transcript panel check
  3818. function transcriptPanelCheck() {
  3819. transcriptPanel = document.querySelector( 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-searchable-transcript"]' );
  3820. hasTranscriptPanel = !!transcriptPanel;
  3821. }
  3822.  
  3823. // check and close chat window
  3824. function chatWindowCheck() {
  3825. hasChatPanel = !!(document.querySelector('#chat') || document.querySelector('iframe#chatframe'));
  3826. const chatMessageElement = document.querySelector('#chat-container ytd-live-chat-frame #message');
  3827.  
  3828. if (chatMessageElement) {
  3829. const messageText = chatMessageElement.textContent.trim();
  3830. if (messageText === 'Chat is disabled for this live stream.') chatEnabled = false;
  3831. } else chatEnabled = true;
  3832.  
  3833. if (USER_CONFIG.closeChatWindow && initialRun && hasChatPanel && chatEnabled) closeLiveChat();
  3834. else document.body.classList.remove('CentAnni-close-live-chat');
  3835. }
  3836.  
  3837. // function to prevent autoplay
  3838. function pauseYouTubeVideo() {
  3839. document.removeEventListener('yt-player-updated', pauseYouTubeVideo);
  3840.  
  3841. if ( !/^https:\/\/.*\.youtube\.com\/watch\?v=/.test(window.location.href) || document.querySelector('.ytp-time-display')?.classList.contains('ytp-live') ) return;
  3842.  
  3843. const elements = {
  3844. player: document.getElementById('movie_player'),
  3845. video: document.querySelector('video'),
  3846. watchFlexy: document.querySelector('ytd-watch-flexy'),
  3847. thumbnailOverlayImage: document.querySelector('.ytp-cued-thumbnail-overlay-image'),
  3848. thumbnailOverlay: document.querySelector('.ytp-cued-thumbnail-overlay')
  3849. };
  3850.  
  3851. if (!elements.player || !elements.video || !elements.watchFlexy) return;
  3852.  
  3853. const urlParams = new URLSearchParams(window.location.search);
  3854. const vinteo = urlParams.get('v');
  3855.  
  3856. if (elements.watchFlexy.hasAttribute('default-layout')) {
  3857. toggleTheaterMode();
  3858. setTimeout(pauseVideo, 200);
  3859. } else if (elements.watchFlexy.hasAttribute('theater')) {
  3860. pauseVideo();
  3861. }
  3862.  
  3863. function pauseVideo() {
  3864. const { player, video, thumbnailOverlayImage, thumbnailOverlay } = elements;
  3865. if (player && video) {
  3866. video.pause();
  3867. player.classList.remove('playing-mode');
  3868. player.classList.add('unstarted-mode', 'paused-mode');
  3869.  
  3870. const playingHandler = () => {
  3871. if (thumbnailOverlayImage) {
  3872. thumbnailOverlayImage.removeAttribute('style');
  3873. thumbnailOverlayImage.style.display = 'none';
  3874. thumbnailOverlay.removeAttribute('style');
  3875. thumbnailOverlay.style.display = 'none';
  3876. }
  3877. video.removeEventListener('playing', playingHandler);
  3878. };
  3879. video.addEventListener('playing', playingHandler);
  3880. }
  3881.  
  3882. if (thumbnailOverlayImage && thumbnailOverlay && lastVideoID !== vinteo) {
  3883. thumbnailOverlay.style.cssText = 'display: block';
  3884. void thumbnailOverlayImage.offsetHeight;
  3885. thumbnailOverlayImage.style.cssText = `
  3886. display: block;
  3887. z-index: 10;
  3888. background-image: url("https://i.ytimg.com/vi/${vinteo}/maxresdefault.jpg");
  3889. `;}
  3890. }
  3891. }
  3892.  
  3893. // channel page redirect to videos
  3894. function channelRedirect() {
  3895. window.location.href = window.location.href.replace(/\/$/, '') + '/videos';
  3896. }
  3897.  
  3898. // reset function
  3899. function handleYTNavigation() {
  3900. if (USER_CONFIG.preventAutoplay) document.addEventListener('yt-player-updated', pauseYouTubeVideo);
  3901.  
  3902. document.querySelectorAll('.button-wrapper, .remaining-time-container, #progress-bar-bar, #progress-bar-start, #progress-bar-end, #yt-transcript-settings-modal, .sub-panel-overlay').forEach(el => el.remove());
  3903. }
  3904.  
  3905. // pause script until tab becomes visible
  3906. async function chromeUserWait() {
  3907. if (document.visibilityState !== 'visible') {
  3908. //console.log("YouTubeAlchemy: Waiting for this tab to become visible...");
  3909. return new Promise((resolve) => {
  3910. document.addEventListener('visibilitychange', function onVisibilityChange() {
  3911. if (document.visibilityState === 'visible') {
  3912. document.removeEventListener('visibilitychange', onVisibilityChange);
  3913. resolve();
  3914. }
  3915. });
  3916. });
  3917. }
  3918. }
  3919.  
  3920. // event listeners
  3921. document.addEventListener('yt-navigate-start', handleYTNavigation); // reset
  3922. document.addEventListener('yt-navigate-finish', handleYouTubeNavigation); // default
  3923. document.addEventListener('yt-page-data-updated', handleYouTubeNavigation); // backup
  3924. document.addEventListener('yt-page-data-fetched', handleYouTubeNavigation); // redundancy
  3925. document.addEventListener('fullscreenchange', fullscreenCheck); // fullscreen check
  3926. if (USER_CONFIG.preventAutoplay) document.addEventListener('yt-player-updated', pauseYouTubeVideo); // prevent autoplay
  3927. document.addEventListener('yt-set-theater-mode-enabled', theaterModeCheck); // theater mode check
  3928. document.addEventListener('yt-service-request-completed', handleYouTubeNavigation); // for chrome
  3929. })();