YouTube Transcript Exporter

Export YouTube transcripts to LLMs or download them as text files. Easy customizable via settings panels. Additional features: persistent progress bar with chapter markers; display remaining time based on playback speed; auto-open chapter panels; links in the header; custom CSS; hide nav bar; color-coded borders on the home page based on video age.

当前为 2025-01-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Transcript Exporter
  3. // @description Export YouTube transcripts to LLMs or download them as text files. Easy customizable via settings panels. Additional features: persistent progress bar with chapter markers; display remaining time based on playback speed; auto-open chapter panels; links in the header; custom CSS; hide nav bar; color-coded borders on the home page based on video age.
  4. // @author Tim Macy
  5. // @license GNU AFFERO GENERAL PUBLIC LICENSE-3.0
  6. // @version 7.3
  7. // @namespace TimMacy.YouTubeTranscriptExporter
  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/YouTubeTranscriptExporter
  15. // @supportURL https://github.com/TimMacy/YouTubeTranscriptExporter/issues
  16. // ==/UserScript==
  17.  
  18. (async function() {
  19. 'use strict';
  20. // CSS
  21. const styleSheet = document.createElement('style');
  22. styleSheet.textContent = `
  23. .overlay {
  24. position: fixed;
  25. z-index: 2053;
  26. left: 0;
  27. top: 0;
  28. width: 100%;
  29. height: 100%;
  30. display: flex;
  31. align-items: center;
  32. justify-content: center;
  33. background-color: rgba(0,0,0,0.5);
  34. backdrop-filter: blur(5px);
  35. }
  36.  
  37. .modal-content {
  38. z-index: 2077;
  39. background-color: rgba(17, 17, 17, 0.8);
  40. padding: 20px 0 20px 20px;
  41. border: 1px solid rgba(255, 255, 255, 0.25);
  42. border-radius: 8px;
  43. width: 420px;
  44. max-height: 90vh;
  45. font-family: "Roboto","Arial",sans-serif;
  46. font-size: 9px;
  47. line-height: 1.2;
  48. color: white;
  49. text-rendering: optimizeLegibility !important;
  50. -webkit-font-smoothing: antialiased !important;
  51. -moz-osx-font-smoothing: grayscale !important;
  52. }
  53.  
  54. #yt-transcript-settings-form {
  55. max-height: calc(90vh - 40px);
  56. overflow-y: auto;
  57. padding-right: 20px;
  58. }
  59.  
  60. .notification {
  61. background:hsl(0,0%,7%);
  62. padding: 20px 30px;
  63. border-radius: 8px;
  64. border: 1px solid hsl(0,0%,18.82%);
  65. max-width: 80%;
  66. text-align: center;
  67. font-family: "Roboto","Arial",sans-serif;
  68. font-size: 16px;
  69. color: white;
  70. }
  71.  
  72. .header {
  73. margin: 0 20px 20px 0;
  74. padding: 0;
  75. border: 0;
  76. text-align:center;
  77. text-decoration: none;
  78. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  79. font-size: 2.5em;
  80. line-height: 1em;
  81. font-weight: 700;
  82. text-overflow: ellipsis;
  83. white-space: normal;
  84. text-shadow: 0 0 20px black;
  85. cursor: pointer;
  86. background: transparent;
  87. display: block;
  88. background-image: linear-gradient(45deg, #FFFFFF, #F2F2F2, #E6E6E6, #D9D9D9, #CCCCCC);
  89. -webkit-background-clip: text;
  90. background-clip: text;
  91. color: transparent;
  92. transition: color .3s ease-in-out;
  93. -webkit-user-select: none;
  94. -moz-user-select: none;
  95. -ms-user-select: none;
  96. user-select: none;
  97. }
  98.  
  99. .header:hover { color: white; }
  100.  
  101. .label-style-settings {
  102. display: block;
  103. margin-bottom: 5px;
  104. font-family: "Roboto","Arial",sans-serif;
  105. font-size: 1.4em;
  106. line-height: 1.5em;
  107. font-weight: 500;
  108. }
  109.  
  110. .label-NotebookLM { color: hsl(134, 61%, 40%); }
  111. .label-ChatGPT { color: hsl(217, 91%, 59%); }
  112. .label-download { color: hsl(359, 88%, 57%); }
  113. .label-settings { color: hsl(0, 0%, 100%); }
  114. .input-field-targetNotebookLMUrl:focus { border: 1px solid hsl(134, 61%, 40%); }
  115. .input-field-targetChatGPTUrl:focus { border: 1px solid hsl(217, 91%, 59%); }
  116. .buttonIconNotebookLM-input-field:focus { border: 1px solid hsl(134, 61%, 40%); }
  117. .buttonIconChatGPT-input-field:focus { border: 1px solid hsl(217, 91%, 59%); }
  118. .buttonIconDownload-input-field:focus { border: 1px solid hsl(359, 88%, 57%); }
  119. .buttonIconSettings-input-field:focus, .links-header-container input:focus, .sidebar-container input:focus { border: 1px solid hsl(0, 0%, 100%); }
  120.  
  121. .input-field-targetNotebookLMUrl:hover,
  122. .input-field-targetChatGPTUrl:hover,
  123. .buttonIconNotebookLM-input-field:hover,
  124. .buttonIconChatGPT-input-field:hover,
  125. .buttonIconDownload-input-field:hover,
  126. .buttonIconSettings-input-field:hover,
  127. .select-file-naming:hover,
  128. .input-field-url:hover,
  129. .chatgpt-prompt-textarea:hover
  130. { background-color: hsl(0, 0%, 10.37%); }
  131.  
  132. .btn-style-settings {
  133. padding: 5px 10px;
  134. cursor: pointer;
  135. color: whitesmoke;
  136. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  137. font-size: 1.4em;
  138. line-height: 1.5em;
  139. font-weight: 400;
  140. background-color: hsl(0, 0%, 7%);
  141. border: 1px solid hsl(0, 0%, 18.82%);
  142. border-radius: 3px;
  143. transition: all 0.2s ease-out;
  144. }
  145.  
  146. .btn-style-settings:hover { color: white; background-color: rgba(255,255,255,0.2); border-color: transparent; }
  147. .button-icons { display: block; font-family: "Roboto","Arial",sans-serif; font-size: 1.4em; line-height: 1.5em; font-weight: 500; }
  148. .icons-container { display: flex; justify-content: space-between; margin-bottom: 20px; }
  149. .container-button { display: flex; flex-direction: column; align-items: center; margin: 5px 0 0 0; }
  150.  
  151. .container-button-input {
  152. width: 80px;
  153. padding: 8px;
  154. text-align: center;
  155. color: ghostwhite;
  156. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  157. font-size: 2em;
  158. line-height: 1.5em;
  159. font-weight: 400;
  160. transition: all .5s ease-in-out;
  161. outline: none;
  162. background-color: hsl(0,0%,7%);
  163. border: 1px solid hsl(0,0%,18.82%);
  164. border-radius: 1px;
  165. box-sizing: border-box;
  166. }
  167.  
  168. .container-button-label {
  169. margin-top: 5px;
  170. text-align: center;
  171. font-family: "Roboto","Arial",sans-serif;
  172. font-size: 1.4em;
  173. line-height: 1.5em;
  174. font-weight: 500;
  175. }
  176.  
  177. .container-button-input:focus { color: white; background-color: hsl(0, 0%, 10.37%); border-radius: 3px; }
  178. .spacer-top { height: 10px; }
  179.  
  180. .copyright {
  181. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  182. font-size: 1.4em;
  183. line-height: 1.5em;
  184. font-weight: 500;
  185. color: white;
  186. text-decoration: none;
  187. transition: color 0.2s ease-in-out;
  188. }
  189.  
  190. .copyright:hover { color: #369eff; }
  191. .url-container { margin-bottom: 10px; }
  192.  
  193. .input-field-url {
  194. width: 100%;
  195. padding: 8px;
  196. color: ghostwhite;
  197. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  198. font-size: 1.4em;
  199. line-height: 1.5em;
  200. font-weight: 400;
  201. transition: all .5s ease-in-out;
  202. outline: none;
  203. background-color:hsl(0,0%,7%);
  204. border: 1px solid hsl(0,0%,18.82%);
  205. border-radius: 1px;
  206. box-sizing: border-box;
  207. }
  208. .input-field-url:focus { color: white; background-color: hsl(0, 0%, 10.37%); border-radius: 3px; }
  209. .file-naming-container { position: relative; margin-bottom: 20px; }
  210.  
  211. .select-file-naming {
  212. width: 100%;
  213. padding: 8px;
  214. cursor:pointer;
  215. color: ghostwhite;
  216. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  217. font-size: 1.4em;
  218. line-height: 1.5em;
  219. font-weight: 400;
  220. transition: all .5s ease-in-out;
  221. outline: none;
  222. appearance: none;
  223. -webkit-appearance: none;
  224. background-color:hsl(0,0%,7%);
  225. border: 1px solid hsl(0,0%,18.82%);
  226. border-radius: 1px;
  227. box-sizing: border-box;
  228.  
  229. 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');
  230. background-repeat: no-repeat;
  231. background-position: right 10px center;
  232. background-size: 20px;
  233.  
  234. -webkit-user-select: none;
  235. -moz-user-select: none;
  236. -ms-user-select: none;]
  237. user-select: none;
  238. }
  239.  
  240. .hidden-select {
  241. position: absolute;
  242. visibility: hidden;
  243. opacity: 0;
  244. pointer-events: none;
  245. width: 100%;
  246. height: 100%;
  247. top: 0;
  248. left: 0;
  249. }
  250.  
  251. .dropdown-list {
  252. visibility: hidden;
  253. opacity: 0;
  254. position: absolute;
  255. z-index: 2100;
  256. top: 115%;
  257. left: 0;
  258. width: 100%;
  259. max-height: 200px;
  260. overflow-y: auto;
  261. background-color:hsl(0,0%,7%);
  262. border: 1px solid hsl(359,88%,57%);
  263. border-radius: 1px 1px 8px 8px;
  264. box-sizing: border-box;
  265. transition: opacity .5s ease-in-out, transform .5s ease-in-out;
  266. transform: translateY(-10px);
  267. }
  268.  
  269. .dropdown-list.show {
  270. visibility: visible;
  271. opacity: 1;
  272. transform: translateY(0);
  273. }
  274.  
  275. .dropdown-item {
  276. padding: 15px;
  277. cursor: pointer;
  278. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  279. font-size: 1.47em;
  280. line-height: 1em;
  281. font-weight: 400;
  282. color: lightgray;
  283. position: relative;
  284. transition: background-color .3s;
  285. padding-left: 1.6em;
  286. }
  287.  
  288. .dropdown-item:hover {
  289. color: ghostwhite;
  290. background-color: rgba(255, 255, 255, .05);
  291. }
  292.  
  293. .dropdown-item:hover::before {
  294. color: ghostwhite;
  295. font-weight: 600;
  296. }
  297.  
  298. .dropdown-item-selected {
  299. color: hsl(359,88%,57%);
  300. font-weight: 600;
  301. }
  302.  
  303. .dropdown-item-selected::before {
  304. content: '✓';
  305. position: absolute;
  306. left: 6px;
  307. color: hsl(359,88%,57%);
  308. }
  309.  
  310. .select-file-naming:focus {
  311. color: white;
  312. background-color: hsl(0, 0%, 10.37%);
  313. border-radius: 3px;
  314. border-color: hsl(359, 88%, 57%);
  315. }
  316.  
  317. .checkbox-label,
  318. .number-input-label span {
  319. display: flex;
  320. align-items: center;
  321. font-family: "Roboto","Arial",sans-serif;
  322. font-size: 1.4em;
  323. line-height: 1.5em;
  324. font-weight: 500;
  325. cursor: pointer;
  326. text-decoration: none;
  327. -webkit-user-select: none;
  328. -moz-user-select: none;
  329. -ms-user-select: none;
  330. user-select: none;
  331. }
  332.  
  333. .checkbox-container { margin-bottom: 5px; }
  334. .checkbox-label:hover { text-decoration: underline; }
  335. .checkbox-field { margin-right: 10px; }
  336.  
  337. .chrome-info {
  338. color: rgba(175, 175, 175, .9);
  339. font-family: "Roboto","Arial",sans-serif;
  340. font-size: 1.2em;
  341. line-height: 1.5em;
  342. font-weight: 400;
  343. display: block;
  344. margin:-5px 0px 5px 24px;
  345. pointer-events: none;
  346. cursor: default;
  347. }
  348.  
  349. .extra-button-container {
  350. display: flex;
  351. justify-content: center;
  352. gap: 5%;
  353. margin: 10px 0;
  354. }
  355.  
  356. .chatgpt-prompt-textarea {
  357. width: 100%;
  358. padding: 8px;
  359. height: 65px;
  360. transition: all .5s ease-in-out;
  361. outline: none;
  362. resize: none;
  363. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  364. font-size: 1.4em;
  365. line-height: 1.5em;
  366. font-weight: 400;
  367. color: ghostwhite;
  368. background-color:hsl(0,0%,7%);
  369. border: 1px solid hsl(0,0%,18.82%);
  370. border-radius: 1px;
  371. box-sizing: border-box;
  372. }
  373.  
  374. .chatgpt-prompt-textarea:focus {
  375. height: 432px;
  376. color: white;
  377. background-color: hsl(0, 0%, 10.37%);
  378. border: 1px solid hsl(217, 91%, 59%);
  379. border-radius: 3px;
  380. }
  381. .button-container-end {
  382. display: flex;
  383. flex-direction: column;
  384. gap: 10px;
  385. margin-top: 20px;
  386. padding-top: 10px;
  387. border: none;
  388. border-top: solid 1px transparent;
  389. border-image: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, .5), rgba(255, 255, 255, 0));
  390. border-image-slice: 1;
  391. -webkit-user-select: none;
  392. -moz-user-select: none;
  393. -ms-user-select: none;
  394. user-select: none;
  395. }
  396.  
  397. .button-container-backup {
  398. display: flex;
  399. justify-content: end;
  400. gap: 23.5px;
  401. }
  402.  
  403. .button-container-settings {
  404. display: flex;
  405. align-items: center;
  406. justify-content: end;
  407. gap: 10px;
  408. }
  409.  
  410. .button-wrapper {
  411. position: relative;
  412. margin-right: 8px;
  413. display: flex;
  414. background-color: transparent;
  415. text-rendering: optimizeLegibility !important;
  416. -webkit-font-smoothing: antialiased !important;
  417. -moz-osx-font-smoothing: grayscale !important;
  418. }
  419.  
  420. .button-wrapper:not(:has(.button-style-settings)):hover { background-color: rgba(255, 255, 255, 0.1); border-radius: 24px; }
  421. .button-wrapper:not(:has(.button-style-settings)):active { background-color: rgba(255, 255, 255, 0.2); border-radius: 24px; }
  422.  
  423. .button-style {
  424. width: 40px;
  425. height: 40px;
  426. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  427. font-size: 24px;
  428. display: inline-block;
  429. position: relative;
  430. box-sizing: border-box;
  431. vertical-align: middle;
  432. color: white;
  433. outline: none;
  434. background: transparent;
  435. margin: 0;
  436. border: none;
  437. padding: 0;
  438. cursor: pointer;
  439. -webkit-tap-highlight-color: rgba(0,0,0,0);
  440. -webkit-tap-highlight-color: transparent;
  441. }
  442.  
  443. .button-style-settings { width: 10px; color: rgb(170, 170, 170); }
  444. .button-style-settings:hover { color: white; }
  445.  
  446. .button-tooltip {
  447. visibility: hidden;
  448. background-color: black;
  449. color: white;
  450. text-align: center;
  451. border-radius: 3px;
  452. padding: 6px 8px;
  453. position: absolute;
  454. z-index: 2053;
  455. top: 120%;
  456. left: 50%;
  457. transform: translateX(-50%);
  458. opacity: 0;
  459. white-space: nowrap;
  460. font-size: 12px;
  461. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  462. border-top: solid 1px white;
  463. border-bottom: solid 1px black;
  464. border-left: solid 1px transparent;
  465. border-right: solid 1px transparent;
  466. border-image: linear-gradient(to bottom, white, black);
  467. border-image-slice: 1;
  468. }
  469.  
  470. .button-tooltip-arrow {
  471. position: absolute;
  472. top: -5px;
  473. left: 50%;
  474. transform: translateX(-50%) rotate(45deg);
  475. width: 10px;
  476. height: 10px;
  477. background: linear-gradient(135deg, white 0%, white 50%, black 50%, black 100%);
  478. z-index: -1;
  479. }
  480.  
  481. .remaining-time-container {
  482. position: relative;
  483. display: block;
  484. height: 0;
  485. top: 4px;
  486. text-align: right;
  487. font-family: "Roboto", "Arial", sans-serif;
  488. font-size: 1.4rem;
  489. font-weight: 500;
  490. line-height: 1em;
  491. color: ghostwhite;
  492. pointer-events: auto;
  493. cursor: auto;
  494. text-rendering: optimizeLegibility !important;
  495. -webkit-font-smoothing: antialiased !important;
  496. -moz-osx-font-smoothing: grayscale !important;
  497. }
  498. .remaining-time-container.live,
  499. #movie_player .remaining-time-container.live {
  500. display: none;
  501. pointer-events: none;
  502. cursor: default;
  503. }
  504.  
  505. #movie_player .remaining-time-container {
  506. position: absolute;
  507. z-index: 2053;
  508. bottom: 15px;
  509. left: 50%;
  510. transform: translateX(-50%);
  511. font-weight: 800 !important;
  512. font-size: 109%;
  513. display: inline-block;
  514. vertical-align: top;
  515. white-space: nowrap;
  516. line-height: 53px;
  517. color: #ddd;
  518. text-shadow: black 0 0 3px !important;
  519. }
  520.  
  521. .transcript-preload {
  522. position: fixed !important;
  523. top: var(--ytd-toolbar-height) !important;
  524. left: 50% !important;
  525. transform: translateX(-50%) !important;
  526. z-index: -1 !important;
  527. opacity: 0 !important;
  528. pointer-events: none !important;
  529. }
  530.  
  531. .notification-error {
  532. z-index: 2053;
  533. box-shadow: none;
  534. text-decoration: none;
  535. display: inline-block;
  536. background-color: hsl(0, 0%, 7%);
  537. padding: 10px 12px;
  538. margin: 0 8px;
  539. border: 1px solid hsl(0, 0%, 18.82%);
  540. border-radius: 5px;
  541. pointer-events: none;
  542. cursor: default;
  543. font-family: 'Roboto', Arial, sans-serif;
  544. color: rgba(255, 255, 255, 0.5);
  545. text-align: center;
  546. font-weight: 500;
  547. font-size: 14px;
  548. text-rendering: optimizeLegibility !important;
  549. -webkit-font-smoothing: antialiased !important;
  550. -moz-osx-font-smoothing: grayscale !important;
  551. }
  552.  
  553. .loading span::before {
  554. content: "Transcript Is Loading";
  555. position: absolute;
  556. inset: initial;
  557. color: rgba(255, 250, 250, .86);
  558. opacity: 0;
  559. -webkit-animation: pulse 1.5s infinite;
  560. animation: pulse 1.5s infinite;
  561. text-rendering: optimizeLegibility !important;
  562. -webkit-font-smoothing: antialiased !important;
  563. -moz-osx-font-smoothing: grayscale !important;
  564. }
  565.  
  566. @-webkit-keyframes pulse {
  567. 0% { opacity: 0; }
  568. 50% { opacity: .71; }
  569. 100% { opacity: 0; }
  570. }
  571.  
  572. @keyframes pulse {
  573. 0% { opacity: 0; }
  574. 50% { opacity: .71; }
  575. 100% { opacity: 0; }
  576. }
  577.  
  578. .buttons-left {
  579. font-family: "Roboto", "Arial", sans-serif;
  580. font-size: 14px;
  581. font-weight: 500;
  582. line-height: 1em;
  583. display: inline-block;
  584. position: relative;
  585. color: ghostwhite;
  586. text-decoration: none;
  587. cursor: pointer;
  588. margin: 0 8px;
  589. outline: none;
  590. background: transparent;
  591. border: none;
  592. text-align: center;
  593. text-rendering: optimizeLegibility !important;
  594. -webkit-font-smoothing: antialiased !important;
  595. -moz-osx-font-smoothing: grayscale !important;
  596. }
  597.  
  598. .buttons-left:hover { color: #ff0000; }
  599. .buttons-left:active { color:rgb(200, 25, 25); }
  600.  
  601. .sub-panel-overlay {
  602. position: fixed;
  603. z-index: 2100;
  604. left: 0;
  605. top: 0;
  606. width: 100%;
  607. height: 100%;
  608. display: flex;
  609. background-color: rgba(0,0,0,0.5);
  610. justify-content: center;
  611. align-items: center;
  612. backdrop-filter: blur(1px);
  613. }
  614.  
  615. .sub-panel {
  616. z-index: 2177;
  617. background-color: rgba(17, 17, 17, 0.8);
  618. padding: 20px;
  619. border: 1px solid rgba(255, 255, 255, 0.25);
  620. border-radius: 8px;
  621. width: 50vw;
  622. max-width: 70vw;
  623. max-height: 80vh;
  624. position: relative;
  625. display: flex;
  626. flex-direction: column;
  627. overflow-y: auto;
  628. color: whitesmoke;
  629. text-rendering: optimizeLegibility !important;
  630. -webkit-font-smoothing: antialiased !important;
  631. -moz-osx-font-smoothing: grayscale !important;
  632. }
  633.  
  634. .sub-panel button {
  635. position: sticky;
  636. top: 0;
  637. align-self: flex-end;
  638. box-shadow: 0 0 20px 10px black;
  639. z-index: 2078;
  640. }
  641.  
  642. .sub-panel-header {
  643. margin: -24px 60px 20px 0px;
  644. padding: 0px 0px 0px 0px;
  645. border: 0;
  646. text-align: left;
  647. text-decoration: none;
  648. font-family: -apple-system, "Roboto", "Arial", sans-serif;
  649. font-size: 2em;
  650. line-height: 1em;
  651. font-weight: 700;
  652. text-overflow: ellipsis;
  653. white-space: normal;
  654. text-shadow: 0 0 30px 20px black;
  655. cursor: auto;
  656. color: white;
  657. align-self: left;
  658. position: relative;
  659. z-index: 2180;
  660. }
  661.  
  662. #links-in-header-form .chrome-info {
  663. margin: -10px 80px 20px 0px;
  664. }
  665.  
  666. .links-header-container {
  667. display: flex;
  668. align-items: center;
  669. gap: 20px;
  670. }
  671.  
  672. .links-header-container label {
  673. color: whitesmoke;
  674. }
  675.  
  676. .links-header-container .url-container:first-child {
  677. flex: 1;
  678. }
  679.  
  680. .links-header-container .url-container:last-child {
  681. flex: 2;
  682. }
  683.  
  684. .sidebar-container {
  685. display: flex;
  686. align-items: center;
  687. margin: 10px 0 0 0;
  688. color: whitesmoke;
  689. justify-content: flex-start;
  690. gap: 20px;
  691. }
  692.  
  693. .sidebar-container .checkbox-container {
  694. margin-bottom: 0 !important;
  695. flex: 1;
  696. }
  697.  
  698. .sidebar-container .url-container {
  699. flex: 2;
  700. }
  701.  
  702. #color-code-videos-form .checkbox-container { margin: 20px 0 0 0; }
  703. #color-code-videos-form .label-style-settings { margin: 20px 0 10px 0; }
  704. #color-code-videos-form > div.videos-old-container > span { margin: 0; }
  705. #custom-css-form .checkbox-container { margin: 20px 0; }
  706.  
  707. #custom-css-form .file-naming-container {
  708. margin: 20px 0;
  709. display: flex;
  710. gap: 25px;
  711. align-content: center;
  712. }
  713.  
  714. #custom-css-form .label-style-settings {
  715. margin-bottom: 0;
  716. white-space: nowrap;
  717. align-content: center;
  718. }
  719.  
  720. input[type="range"] {
  721. -webkit-appearance: none; /* Remove default styling */
  722. appearance: none;
  723. width: 100%; /* Full width */
  724. height: 6px; /* Height of the track */
  725. background: #ccc; /* Track color */
  726. border-radius: 5px; /* Rounded corners for the track */
  727. outline: none; /* Remove outline */
  728. }
  729.  
  730. input[type="range"]::-moz-range-thumb,
  731. input[type="range"]::-webkit-slider-thumb {
  732. -webkit-appearance: none; /* Remove default styling */
  733. appearance: none;
  734. width: 16px; /* Width of the thumb */
  735. height: 16px; /* Height of the thumb */
  736. background: #007bff; /* Thumb color */
  737. border-radius: 50%; /* Make the thumb round */
  738. cursor: pointer; /* Add a pointer cursor */
  739. border: 2px solid #ffffff; /* Add a white border for visibility */
  740. }
  741.  
  742. input[type="range"]::-moz-range-track,
  743. input[type="range"]::-webkit-slider-runnable-track {
  744. background: #007bff; /* Track color */
  745. height: 6px; /* Height of the track */
  746. border-radius: 5px; /* Rounded corners */
  747. }
  748.  
  749. .videos-old-container {
  750. display: flex;
  751. align-items: center;
  752. gap: 25px;
  753. margin: 20px 0;
  754. }
  755.  
  756. .slider-container {
  757. display: flex;
  758. align-items: center;
  759. gap: 10px;
  760. flex: 1;
  761. }
  762.  
  763. .slider-container input[type="range"] {
  764. flex: 1;
  765. }
  766.  
  767. .videos-age-container {
  768. display: flex;
  769. flex-direction: column;
  770. align-items: center;
  771. gap: 25px;
  772. }
  773.  
  774. .videos-age-row {
  775. display: flex;
  776. justify-content: flex-start;
  777. align-items: center;
  778. width: 100%;
  779. gap: 10px;
  780. }
  781.  
  782. .videos-age-row span {
  783. text-align: right;
  784. flex: 1;
  785. max-width: 50%;
  786. }
  787.  
  788. .videos-age-row input {
  789. flex: 1;
  790. margin: 0 0 0 10px;
  791. max-width: 70px;
  792. height: 35px;
  793. cursor: pointer;
  794. }
  795.  
  796. .number-input-container {
  797. margin: 20px 0;
  798. }
  799.  
  800. .number-input-field {
  801. margin: 0 10px 0 0;
  802. align-items: center;
  803. font-size: 1.4em;
  804. line-height: 1.5em;
  805. font-weight: 700;
  806. cursor: auto;
  807. text-decoration: none;
  808. text-align: center;
  809. display: inline-block;
  810.  
  811. }
  812.  
  813. .number-input-label span {
  814. display: initial;
  815. cursor: auto;
  816. }
  817. `;
  818.  
  819. document.head.appendChild(styleSheet);
  820.  
  821. // CSS Progress Bar
  822. function ProgressBarCSS() {
  823. const progressBarCSS = document.createElement('style');
  824. progressBarCSS.textContent = `
  825. #ProgressBar-bar {
  826. width: 100%;
  827. height: 3px;
  828. background: rgba(255, 255, 255, 0.2);
  829. position: absolute;
  830. bottom: 0;
  831. opacity: 0;
  832. z-index: 50;
  833. }
  834.  
  835. #ProgressBar-progress, #ProgressBar-buffer {
  836. width: 100%;
  837. height: 3px;
  838. transform-origin: 0 0;
  839. position: absolute;
  840. }
  841.  
  842. #ProgressBar-progress {
  843. background: #f00;
  844. filter: none;
  845. z-index: 1;
  846. }
  847.  
  848. .ytp-autohide .ytp-chrome-bottom .ytp-load-progress, .ytp-autohide .ytp-chrome-bottom .ytp-play-progress { display: none !important; }
  849. .ytp-autohide .ytp-chrome-bottom { opacity: 1 !important; display: block !important; }
  850. .ytp-autohide .ytp-chrome-bottom .ytp-chrome-controls { opacity: 0 !important; }
  851. .ad-interrupting #ProgressBar-progress { background: transparent; }
  852. .ytp-ad-persistent-progress-bar-container { display: none; }
  853. #ProgressBar-buffer { background: rgba(255, 255, 255, 0.4); }
  854.  
  855. .ytp-autohide #ProgressBar-start.active,
  856. .ytp-autohide #ProgressBar-bar.active,
  857. .ytp-autohide #ProgressBar-end.active
  858. { opacity: 1; }
  859.  
  860. .ytp-autohide .ytp-chrome-bottom .ytp-progress-bar-container {
  861. bottom: 0px !important;
  862. opacity: 1 !important;
  863. height: 4px !important;
  864. transform: translateX(0px) !important;
  865. z-index: 100;
  866. }
  867.  
  868. .ytp-autohide .ytp-chrome-bottom .ytp-progress-bar,
  869. .ytp-autohide .ytp-chrome-bottom .ytp-progress-list {
  870. background: transparent !important;
  871. box-shadow: none !important;
  872. }
  873.  
  874. .ytp-autohide .ytp-chrome-bottom .previewbar {
  875. height: calc(100% + 1px) !important;
  876. bottom: -1px !important;
  877. margin-bottom: 0px !important;
  878. opacity: 1 !important;
  879. border: none !important;
  880. box-shadow: none !important;
  881. will-change: opacity, transform !important;
  882. }
  883.  
  884. #ProgressBar-start, #ProgressBar-end {
  885. position: absolute;
  886. height: 3px;
  887. width: 12px;
  888. bottom: 0;
  889. z-index: 2077;
  890. opacity: 0;
  891. pointer-events: none;
  892. }
  893.  
  894. :fullscreen #ProgressBar-start, :fullscreen #ProgressBar-end { width: 24px; }
  895. :-webkit-full-screen #ProgressBar-start, :-webkit-full-screen #ProgressBar-end { width: 24px; }
  896.  
  897. #ProgressBar-start {
  898. left: 0;
  899. background: #f00;
  900. }
  901.  
  902. #ProgressBar-end {
  903. right: 0;
  904. background: rgba(255, 255, 255, 0.2);
  905. }
  906. `;
  907.  
  908. document.head.appendChild(progressBarCSS);
  909. }
  910.  
  911. // customCSS
  912. function customCSS() {
  913. const customCSS = document.createElement('style');
  914. customCSS.textContent = `
  915. .yte-style-hide-default-sidebar {
  916. ytd-mini-guide-renderer.ytd-app { display: none !important; }
  917. ytd-app[mini-guide-visible] ytd-page-manager.ytd-app { margin-left: 0 !important; }
  918. #guide-button.ytd-masthead { display: none !important; }
  919. #contents.ytd-rich-grid-renderer { justify-content: center !important; }
  920. }
  921.  
  922. html {
  923. font-size: var(--fontSize) !important;
  924. font-family: "Roboto", Arial, sans-serif;
  925. }
  926.  
  927. #video-title.ytd-rich-grid-media {
  928. text-transform: var(--textTransform);
  929. }
  930.  
  931. ytd-compact-video-renderer ytd-thumbnail:has(ytd-thumbnail-overlay-resume-playback-renderer),
  932. ytd-rich-item-renderer ytd-thumbnail:has(ytd-thumbnail-overlay-resume-playback-renderer) {
  933. opacity: var(--watchedOpacity);
  934. }
  935.  
  936. .yte-style-hide-watched-videos-global {
  937. ytd-rich-item-renderer:has(ytd-thumbnail-overlay-resume-playback-renderer) {
  938. display: none;
  939. }
  940. }
  941.  
  942. .ytd-page-manager[page-subtype="home"],
  943. .ytd-page-manager[page-subtype="channels"],
  944. .ytd-page-manager[page-subtype="subscriptions"] {
  945. .style-scope.ytd-two-column-browse-results-renderer {
  946. --ytd-rich-grid-items-per-row: var(--itemsPerRow) !important;
  947. --ytd-rich-grid-posts-per-row: var(--itemsPerRow) !important;
  948. --ytd-rich-grid-slim-items-per-row: var(--itemsPerRowCalc);
  949. --ytd-rich-grid-game-cards-per-row: var(--itemsPerRowCalc);
  950. --ytd-rich-grid-mini-game-cards-per-row: var(--itemsPerRowCalc);
  951. }
  952. }
  953.  
  954. .yte-style-hide-voice-search {
  955. #voice-search-button.ytd-masthead { display: none; }
  956. }
  957.  
  958. .yte-style-hide-create-button {
  959. #buttons.ytd-masthead > .ytd-masthead:first-child { display: none; }
  960. }
  961.  
  962. .yte-style-hide-miniplayer {
  963. .miniplayer.ytd-miniplayer { display: none; }
  964. ytd-miniplayer { display: none; }
  965. }
  966.  
  967. .yte-style-sqaure-search-bar {
  968. #center.ytd-masthead { flex: 0 1 500px; }
  969. .YtSearchboxComponentInputBox { border: 1px solid hsl(0,0%,18.82%); border-radius: 0; }
  970. .YtSearchboxComponentSuggestionsContainer { border-radius: 0 0 10px 10px; }
  971. .YtSearchboxComponentSearchButtonDark { display: none; }
  972. .YtSearchboxComponentHost { margin: 0; }
  973.  
  974. .ytSearchboxComponentInputBox { border: 1px solid hsl(0,0%,18.82%); border-radius: 0; }
  975. .ytSearchboxComponentSuggestionsContainer { border-radius: 0 0 10px 10px; }
  976. .ytSearchboxComponentSearchButtonDark { display: none; }
  977. .ytSearchboxComponentHost { margin: 0; }
  978. }
  979.  
  980. .yte-style-sqaure-design {
  981. .ytd-page-manager[page-subtype="home"] {
  982. yt-chip-cloud-chip-renderer {
  983. border-radius: 3px;
  984. }
  985. }
  986.  
  987. .ytd-page-manager[page-subtype="channels"] {
  988. .yt-image-banner-view-model-wiz--inset,
  989. yt-chip-cloud-chip-renderer {
  990. border-radius: 0 !important;
  991. }
  992.  
  993. .yt-spec-button-shape-next--size-m {
  994. border-radius: 3px;
  995. }
  996. }
  997.  
  998. .yt-spec-button-shape-next--size-m.yt-spec-button-shape-next--segmented-start { border-radius: 3px 0 0 3px; }
  999. .yt-spec-button-shape-next--size-m.yt-spec-button-shape-next--segmented-end { border-radius: 0 3px 3px 0; }
  1000.  
  1001. ytd-engagement-panel-section-list-renderer[modern-panels]:not([live-chat-engagement-panel]),
  1002. .yt-spec-button-shape-next--size-s,
  1003. .yt-spec-button-shape-next--size-m,
  1004. #description.ytd-watch-metadata,
  1005. ytd-multi-page-menu-renderer,
  1006. yt-chip-cloud-chip-renderer,
  1007. .ytChipShapeChip {
  1008. border-radius: 3px;
  1009. }
  1010.  
  1011. ytd-menu-popup-renderer {
  1012. border-radius: 0 0 5px 5px;
  1013. }
  1014.  
  1015. ytd-macro-markers-list-item-renderer[rounded] #thumbnail.ytd-macro-markers-list-item-renderer,
  1016. ytd-thumbnail[size="medium"] a.ytd-thumbnail, ytd-thumbnail[size="medium"]::before,
  1017. ytd-thumbnail[size="large"] a.ytd-thumbnail, ytd-thumbnail[size="large"]::before,
  1018. ytd-watch-flexy[rounded-player-large][default-layout] #ytd-player.ytd-watch-flexy,
  1019. ytd-engagement-panel-section-list-renderer[modern-panels]:not([live-chat-engagement-panel]) {
  1020. border-radius: 0 !important;
  1021. }
  1022. }
  1023.  
  1024. .yte-style-compact-layout {
  1025. .ytd-page-manager[page-subtype="home"],
  1026. .ytd-page-manager[page-subtype="channels"],
  1027. .ytd-page-manager[page-subtype="subscriptions"] {
  1028. ytd-rich-section-renderer { display: none; }
  1029. #contents.ytd-rich-grid-renderer {
  1030. width: 100%;
  1031. max-width: 100%;
  1032. padding-top: 0;
  1033. display: flex;
  1034. flex-wrap: wrap;
  1035. justify-content: flex-start;
  1036. }
  1037. .style-scope.ytd-two-column-browse-results-renderer {
  1038. --ytd-rich-grid-item-max-width: 100vw;
  1039. --ytd-rich-grid-item-min-width: 310px;
  1040. --ytd-rich-grid-item-margin: 1px !important;
  1041. --ytd-rich-grid-content-offset-top: 56px;
  1042. }
  1043. ytd-rich-item-renderer[rendered-from-rich-grid][is-in-first-column] {
  1044. margin-left: 5px !important;
  1045. }
  1046. ytd-rich-item-renderer[rendered-from-rich-grid] {
  1047. margin: 5px 0 20px 5px !important;
  1048. }
  1049. #meta.ytd-rich-grid-media {
  1050. overflow-x: hidden;
  1051. padding-right: 6px;
  1052. }
  1053. #avatar-container.ytd-rich-grid-media {
  1054. margin:7px 6px 0px 6px;
  1055. }
  1056. h3.ytd-rich-grid-media {
  1057. margin: 7px 0 4px 0;
  1058. }
  1059. }
  1060. .ytd-page-manager[page-subtype="home"] {
  1061. ytd-menu-renderer.ytd-rich-grid-media {
  1062. position: absolute;
  1063. top: 50px;
  1064. right: auto;
  1065. left: 3px;
  1066. transform: rotate(90deg);
  1067. background-color: rgba(255,255,255,.1);
  1068. border-radius: 50%;
  1069. }
  1070. .title-badge.ytd-rich-grid-media, .video-badge.ytd-rich-grid-media {
  1071. position: absolute;
  1072. margin: 0px 10% 0 0;
  1073. right: 0;
  1074. top: 6em;
  1075. }
  1076. ytd-rich-item-renderer[rendered-from-rich-grid] {
  1077. margin: 5px 5px 20px 5px !important;
  1078. }
  1079.  
  1080. .style-scope.ytd-two-column-browse-results-renderer {
  1081. --ytd-rich-grid-item-margin: .5% !important;
  1082. }
  1083. }
  1084. .ytd-page-manager[page-subtype="channels"] {
  1085. 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 {
  1086. padding: 0 !important;
  1087. }
  1088. ytd-two-column-browse-results-renderer.grid-5-columns, .grid-5-columns.ytd-two-column-browse-results-renderer {
  1089. width: 100% !important;
  1090. }
  1091. ytd-rich-grid-renderer:not([is-default-grid]) #header.ytd-rich-grid-renderer {
  1092. transform: translateX(800px) translateY(-40px);
  1093. z-index: 2000;
  1094. }
  1095. ytd-feed-filter-chip-bar-renderer[component-style="FEED_FILTER_CHIP_BAR_STYLE_TYPE_CHANNEL_PAGE_GRID"] {
  1096. margin-bottom: -32px;
  1097. margin-top: 0;
  1098. }
  1099. .page-header-view-model-wiz__page-header-headline-image {
  1100. margin-left: 110px;
  1101. }
  1102. ytd-rich-section-renderer {
  1103. display: none;
  1104. }
  1105. ytd-menu-renderer.ytd-rich-grid-media {
  1106. position: absolute;
  1107. top: 4em;
  1108. right: 5%;
  1109. left:-auto;
  1110. transform: rotate(90deg);
  1111. background-color: rgba(255,255,255,.1);
  1112. border-radius: 50%;
  1113. }
  1114. .yt-tab-group-shape-wiz__slider,.yt-tab-shape-wiz__tab-bar {
  1115. display:none;
  1116. }
  1117. .yt-tab-shape-wiz__tab--tab-selected,.yt-tab-shape-wiz__tab:hover {
  1118. color:white;
  1119. }
  1120. #tabsContent > yt-tab-group-shape > div.yt-tab-group-shape-wiz__tabs > yt-tab-shape:nth-child(3) {
  1121. display:none!important;
  1122. }
  1123. .style-scope.ytd-two-column-browse-results-renderer {
  1124. --ytd-rich-grid-item-margin: .5% !important;
  1125. }
  1126. }
  1127. ytd-browse[page-subtype="channels"] #contentContainer {
  1128. padding-top: 0 !important;
  1129. }
  1130. ytd-browse[page-subtype="channels"] tp-yt-app-header {
  1131. position: static !important;
  1132. transform: none !important;
  1133. transition: none !important;
  1134. }
  1135. ytd-browse[page-subtype="channels"] tp-yt-app-header[fixed] {
  1136. position: static !important;
  1137. transform: none !important;
  1138. transition: none !important;
  1139. }
  1140. ytd-browse[page-subtype="channels"] tp-yt-app-header #page-header {
  1141. position: static !important;
  1142. transform: none !important;
  1143. }
  1144. .ytd-page-manager[page-subtype="subscriptions"] {
  1145. ytd-menu-renderer.ytd-rich-grid-media {
  1146. position: absolute;
  1147. top: 50px;
  1148. right: auto;
  1149. left: 3px;
  1150. transform: rotate(90deg);
  1151. background-color: rgba(255,255,255,.1);
  1152. border-radius: 50%;
  1153. }
  1154. .title-badge.ytd-rich-grid-media, .video-badge.ytd-rich-grid-media {
  1155. position: absolute;
  1156. margin: 0px 10% 0 0;
  1157. right: 0;
  1158. top: 6em;
  1159. }
  1160. }
  1161. .item.ytd-watch-metadata {
  1162. margin-top: 7px;
  1163. }
  1164. #subheader.ytd-engagement-panel-title-header-renderer:not(:empty) {
  1165. padding: 0 !important;
  1166. transform: translateX(110px) translateY(-44px);
  1167. background-color: transparent;
  1168. border-top: none;
  1169. }
  1170. #header.ytd-engagement-panel-title-header-renderer {
  1171. padding: 4px 7px 4px 7px;
  1172. }
  1173. #visibility-button.ytd-engagement-panel-title-header-renderer, #information-button.ytd-engagement-panel-title-header-renderer {
  1174. z-index: 1;
  1175. }
  1176. .ytChipShapeChip:hover {
  1177. background: rgba(255,255,255,0.2);
  1178. border-color: transparent;
  1179. }
  1180. .ytChipShapeActive:hover {
  1181. background-color: #f1f1f1;
  1182. color: #0f0f0f;
  1183. }
  1184. ytd-engagement-panel-title-header-renderer {
  1185. height: 54px;
  1186. }
  1187. .yt-spec-button-shape-next--icon-only-default {
  1188. width: 35px;
  1189. height: 35px;
  1190. }
  1191. }
  1192.  
  1193. .ytd-page-manager[page-subtype="home"] {
  1194. .yte-style-live-video, .yte-style-upcoming-video, .yte-style-newly-video, .yte-style-recent-video, .yte-style-lately-video, .yte-style-old-video { outline: 2px solid; }
  1195.  
  1196. .yte-style-live-video { outline-color: var(--liveVideo);}
  1197. .yte-style-streamed-text { color: var(--streamedText);}
  1198. .yte-style-upcoming-video { outline-color: var(--upComingVideo);}
  1199. .yte-style-newly-video { outline-color: var(--newlyVideo);}
  1200. .yte-style-recent-video { outline-color: var(--recentVideo);}
  1201. .yte-style-lately-video { outline-color: var(--latelyVideo);}
  1202. .yte-style-old-video { opacity: var(--oldVideo);}
  1203. }
  1204.  
  1205. .yte-style-hide-watched-videos {
  1206. .ytd-page-manager[page-subtype="home"] {
  1207. ytd-rich-item-renderer:has(ytd-thumbnail-overlay-resume-playback-renderer) {
  1208. display: none;
  1209. }
  1210. }
  1211. }
  1212. `;
  1213.  
  1214. document.head.appendChild(customCSS);
  1215. }
  1216.  
  1217. // default configuration
  1218. const DEFAULT_CONFIG = {
  1219. targetChatGPTUrl: 'https://ChatGPT.com/',
  1220. targetNotebookLMUrl: 'https://NotebookLM.Google.com/',
  1221. fileNamingFormat: 'title-channel',
  1222. includeTimestamps: true,
  1223. includeChapterHeaders: true,
  1224. openSameTab:true,
  1225. autoOpenChapters: true,
  1226. DisplayRemainingTime: true,
  1227. ProgressBar: true,
  1228. preventBackgroundExecution: true,
  1229. 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 the YouTuber's 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 question from the transcript. Respond only in chat. Do not open a canvas. Ask for permission 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 were 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.`,
  1230. buttonIcons: {
  1231. settings: '⋮',
  1232. download: '↓',
  1233. ChatGPT: '💬',
  1234. NotebookLM: '🎧'
  1235. },
  1236. buttonLeft1Text: 'ABC News',
  1237. buttonLeft1Url: 'https://www.youtube.com/@ABCNews/videos',
  1238. buttonLeft2Text: 'CNN',
  1239. buttonLeft2Url: 'https://www.youtube.com/@CNN/videos',
  1240. buttonLeft3Text: '',
  1241. buttonLeft3Url: 'https://www.youtube.com/@BBCNews/videos',
  1242. buttonLeft4Text: '',
  1243. buttonLeft4Url: 'https://www.youtube.com/@FoxNews/videos',
  1244. buttonLeft5Text: '',
  1245. buttonLeft5Url: 'https://www.youtube.com/@NBCNews/videos',
  1246. buttonLeft6Text: '',
  1247. buttonLeft6Url: 'https://www.youtube.com/@Formula1/videos',
  1248. buttonLeft7Text: '',
  1249. buttonLeft7Url: 'https://www.youtube.com/@SkySportsF1/videos',
  1250. mButtonText: '☰',
  1251. mButtonDisplay: false,
  1252. colorCodeVideosEnabled: true,
  1253. videosHideWatched: false,
  1254. videosOldOpacity: 0.5,
  1255. videosAgeColorPickerNewly: '#FFFF00',
  1256. videosAgeColorPickerRecent: '#FFA500',
  1257. videosAgeColorPickerLately: '#0000FF',
  1258. videosAgeColorPickerLive: '#FF0000',
  1259. videosAgeColorPickerStreamed: '#FF0000',
  1260. videosAgeColorPickerUpcoming: '#32CD32',
  1261. textTransform: 'normal-case',
  1262. defaultFontSize: 10,
  1263. videosWatchedOpacity: 0.5,
  1264. videosPerRow: 3,
  1265. videosHideWatchedGlobal: false,
  1266. hideVoiceSearch: false,
  1267. hideCreateButton: false,
  1268. hideMiniPlayer: false,
  1269. squareSearchBar: true,
  1270. squareDesign: false,
  1271. compactLayout: false,
  1272. };
  1273.  
  1274. // load user configuration or use defaults
  1275. let storedConfig = {};
  1276. try {
  1277. storedConfig = await GM.getValue('USER_CONFIG', {});
  1278. } catch (error) {
  1279. showNotification('Error loading user save!');
  1280. console.error("YTE: Error loading user configuration:", error);
  1281. }
  1282.  
  1283. let USER_CONFIG = {
  1284. ...DEFAULT_CONFIG,
  1285. ...storedConfig,
  1286. buttonIcons: {
  1287. ...DEFAULT_CONFIG.buttonIcons,
  1288. ...storedConfig.buttonIcons
  1289. }
  1290. };
  1291.  
  1292. // ensure CSS settings load immediately
  1293. function loadCSSsettings() {
  1294. const body = document.querySelector('body');
  1295.  
  1296. // buttonsLeft - sidebar visibility
  1297. if (USER_CONFIG.mButtonDisplay) { body.classList.add('yte-style-hide-default-sidebar'); } else { body.classList.remove('yte-style-hide-default-sidebar'); }
  1298.  
  1299. // custom css
  1300. document.documentElement.style.setProperty('--textTransform', USER_CONFIG.textTransform);
  1301. document.documentElement.style.setProperty('--fontSize', `${USER_CONFIG.defaultFontSize}px`);
  1302. document.documentElement.style.setProperty('--watchedOpacity', USER_CONFIG.videosWatchedOpacity);
  1303. document.documentElement.style.setProperty('--itemsPerRow', USER_CONFIG.videosPerRow);
  1304. document.documentElement.style.setProperty('--itemsPerRowCalc', USER_CONFIG.videosPerRow + 2);
  1305. if (USER_CONFIG.videosHideWatchedGlobal) { body.classList.add('yte-style-hide-watched-videos-global'); } else { body.classList.remove('yte-style-hide-watched-videos-global'); }
  1306. if (USER_CONFIG.hideVoiceSearch) { body.classList.add('yte-style-hide-voice-search'); } else { body.classList.remove('yte-style-hide-voice-search'); }
  1307. if (USER_CONFIG.hideCreateButton) { body.classList.add('yte-style-hide-create-button'); } else { body.classList.remove('yte-style-hide-create-button'); }
  1308. if (USER_CONFIG.hideMiniPlayer) { body.classList.add('yte-style-hide-miniplayer'); } else { body.classList.remove('yte-style-hide-miniplayer'); }
  1309. if (USER_CONFIG.squareSearchBar) { body.classList.add('yte-style-sqaure-search-bar'); } else { body.classList.remove('yte-style-sqaure-search-bar'); }
  1310. if (USER_CONFIG.squareDesign) { body.classList.add('yte-style-sqaure-design'); } else { body.classList.remove('yte-style-sqaure-design'); }
  1311. if (USER_CONFIG.compactLayout) { body.classList.add('yte-style-compact-layout'); } else { body.classList.remove('yte-style-compact-layout'); }
  1312.  
  1313. // color code videos
  1314. if (USER_CONFIG.videosHideWatched) { body.classList.add('yte-style-hide-watched-videos'); } else { body.classList.remove('yte-style-hide-watched-videos'); }
  1315. document.documentElement.style.setProperty('--liveVideo', USER_CONFIG.videosAgeColorPickerLive);
  1316. document.documentElement.style.setProperty('--streamedText', USER_CONFIG.videosAgeColorPickerStreamed);
  1317. document.documentElement.style.setProperty('--upComingVideo', USER_CONFIG.videosAgeColorPickerUpcoming);
  1318. document.documentElement.style.setProperty('--newlyVideo', USER_CONFIG.videosAgeColorPickerNewly);
  1319. document.documentElement.style.setProperty('--recentVideo', USER_CONFIG.videosAgeColorPickerRecent);
  1320. document.documentElement.style.setProperty('--latelyVideo', USER_CONFIG.videosAgeColorPickerLately);
  1321. document.documentElement.style.setProperty('--oldVideo', USER_CONFIG.videosOldOpacity);
  1322. }
  1323.  
  1324. // create and show the settings modal
  1325. function showSettingsModal() {
  1326. const existingModal = document.getElementById('yt-transcript-settings-modal');
  1327. if (existingModal) {
  1328. existingModal.style.display = 'flex';
  1329. document.body.style.overflow = 'hidden';
  1330. return;
  1331. }
  1332.  
  1333. // create modal elements
  1334. const modal = document.createElement('div');
  1335. modal.id = 'yt-transcript-settings-modal';
  1336. modal.classList.add('overlay');
  1337.  
  1338. const modalContent = document.createElement('div');
  1339. modalContent.classList.add('modal-content');
  1340.  
  1341. // modal header
  1342. const header = document.createElement('a');
  1343. header.href = 'https://github.com/TimMacy/YouTubeTranscriptExporter';
  1344. header.target = '_blank';
  1345. header.innerText = 'YouTube Transcript Exporter';
  1346. header.title = 'GitHub Repository for YouTube Transcript Exporter';
  1347. header.classList.add('header');
  1348. modalContent.appendChild(header);
  1349.  
  1350. // create form elements for each setting
  1351. const form = document.createElement('form');
  1352. form.id = 'yt-transcript-settings-form';
  1353.  
  1354. // Button Icons
  1355. const iconsHeader = document.createElement('label');
  1356. iconsHeader.innerText = 'Button Icons:';
  1357. iconsHeader.classList.add('button-icons');
  1358. form.appendChild(iconsHeader);
  1359.  
  1360. const iconsContainer = document.createElement('div');
  1361. iconsContainer.classList.add('icons-container');
  1362.  
  1363. function createIconInputField(labelText, settingKey, settingValue, labelClass) {
  1364. const container = document.createElement('div');
  1365. container.classList.add('container-button');
  1366.  
  1367. const input = document.createElement('input');
  1368. const iconInputClass = `${settingKey}-input-field`;
  1369. input.type = 'text';
  1370. input.name = settingKey;
  1371. input.value = settingValue;
  1372. input.classList.add('container-button-input');
  1373. input.classList.add(iconInputClass);
  1374.  
  1375. const label = document.createElement('label');
  1376. label.innerText = labelText;
  1377. label.className = labelClass;
  1378. label.classList.add('container-button-label');
  1379.  
  1380. container.appendChild(input);
  1381. container.appendChild(label);
  1382.  
  1383. return container;
  1384. }
  1385.  
  1386. iconsContainer.appendChild(createIconInputField('NotebookLM', 'buttonIconNotebookLM', USER_CONFIG.buttonIcons.NotebookLM, 'label-NotebookLM'));
  1387. iconsContainer.appendChild(createIconInputField('ChatGPT', 'buttonIconChatGPT', USER_CONFIG.buttonIcons.ChatGPT, 'label-ChatGPT'));
  1388. iconsContainer.appendChild(createIconInputField('Download', 'buttonIconDownload', USER_CONFIG.buttonIcons.download, 'label-download'));
  1389. iconsContainer.appendChild(createIconInputField('Settings', 'buttonIconSettings', USER_CONFIG.buttonIcons.settings, 'label-settings'));
  1390.  
  1391. form.appendChild(iconsContainer);
  1392.  
  1393. // NotebookLM URL
  1394. form.appendChild(createInputField('NotebookLM URL (Copy transcript, then open the website):', 'targetNotebookLMUrl', USER_CONFIG.targetNotebookLMUrl, 'label-NotebookLM'));
  1395.  
  1396. // ChatGPT URL
  1397. form.appendChild(createInputField('ChatGPT URL (Copy transcript with the prompt, then open the website):', 'targetChatGPTUrl', USER_CONFIG.targetChatGPTUrl, 'label-ChatGPT'));
  1398.  
  1399. // Spacer-Top
  1400. const spacerTop = document.createElement('div');
  1401. spacerTop.classList.add('spacer-top');
  1402. form.appendChild(spacerTop);
  1403.  
  1404. // File Naming Format
  1405. form.appendChild(createSelectField('Text File Naming Format:', 'label-download', 'fileNamingFormat', USER_CONFIG.fileNamingFormat, {
  1406. 'title-channel': 'Title - Channel.txt (default)',
  1407. 'channel-title': 'Channel - Title.txt',
  1408. 'date-title-channel': 'uploadDate - Title - Channel.txt',
  1409. 'date-channel-title': 'uploadDate - Channel - Title.txt',
  1410. }));
  1411.  
  1412. // include Timestamps
  1413. form.appendChild(createCheckboxField('Include Timestamps in the Transcript (default: yes)', 'includeTimestamps', USER_CONFIG.includeTimestamps));
  1414.  
  1415. // include Chapter Headers
  1416. form.appendChild(createCheckboxField('Include Chapter Headers in the Transcript (default: yes)', 'includeChapterHeaders', USER_CONFIG.includeChapterHeaders));
  1417.  
  1418. // open in Same Tab
  1419. form.appendChild(createCheckboxField('Open Links in the Same Tab (default: yes)', 'openSameTab', USER_CONFIG.openSameTab));
  1420.  
  1421. // Auto Open Chapter Panel
  1422. form.appendChild(createCheckboxField('Automatically Open the Chapter Panel (default: yes)', 'autoOpenChapters', USER_CONFIG.autoOpenChapters));
  1423.  
  1424. // Display Remaining Time
  1425. form.appendChild(createCheckboxField('Display Remaining Time Under the Video (default: yes)', 'DisplayRemainingTime', USER_CONFIG.DisplayRemainingTime));
  1426.  
  1427. // keep Progress Bar Visible
  1428. form.appendChild(createCheckboxField('Keep the Progress Bar Visible (default: yes)', 'ProgressBar', USER_CONFIG.ProgressBar));
  1429.  
  1430. // prevent Execution in Background Tabs
  1431. form.appendChild(createCheckboxField('Important for Chrome! (default: yes)', 'preventBackgroundExecution', USER_CONFIG.preventBackgroundExecution));
  1432.  
  1433. // info for Chrome
  1434. const description = document.createElement('small');
  1435. description.innerText = 'Prevents early script execution in background tabs.\nWhile this feature is superfluous in Safari, it is essential for Chrome.';
  1436. description.classList.add('chrome-info');
  1437. form.appendChild(description);
  1438.  
  1439. // extra settings buttons
  1440. const extraSettings = document.createElement('div');
  1441. extraSettings.classList.add('extra-button-container');
  1442.  
  1443. const buttonsLeft = document.createElement('button');
  1444. buttonsLeft.type = 'button';
  1445. buttonsLeft.innerText = 'Links in Header';
  1446. buttonsLeft.classList.add('btn-style-settings');
  1447. buttonsLeft.onclick = () => showSubPanel(createLinksInHeaderContent(), 'linksInHeader');
  1448.  
  1449. const customCSSButton = document.createElement('button');
  1450. customCSSButton.type = 'button';
  1451. customCSSButton.innerText = 'Customize CSS';
  1452. customCSSButton.classList.add('btn-style-settings');
  1453. customCSSButton.onclick = () => showSubPanel(createCustomCSSContent(), 'createcustomCSS');
  1454.  
  1455. const ColorCodeVideos = document.createElement('button');
  1456. ColorCodeVideos.type = 'button';
  1457. ColorCodeVideos.innerText = 'Color Code Videos';
  1458. ColorCodeVideos.classList.add('btn-style-settings');
  1459. ColorCodeVideos.onclick = () => showSubPanel(createColorCodeVideosContent(), 'colorCodeVideos');
  1460.  
  1461. extraSettings.appendChild(buttonsLeft);
  1462. extraSettings.appendChild(customCSSButton);
  1463. extraSettings.appendChild(ColorCodeVideos);
  1464.  
  1465. form.appendChild(extraSettings);
  1466.  
  1467. // ChatGPT Prompt
  1468. form.appendChild(createTextareaField('ChatGPT Prompt:', 'ChatGPTPrompt', USER_CONFIG.ChatGPTPrompt, 'label-ChatGPT'));
  1469. // action buttons container
  1470. const buttonContainer = document.createElement('div');
  1471. buttonContainer.classList.add('button-container-end');
  1472.  
  1473. // export and import button container
  1474. const exportImportContainer = document.createElement('div');
  1475. exportImportContainer.classList.add('button-container-backup');
  1476.  
  1477. const exportButton = document.createElement('button');
  1478. exportButton.type = 'button';
  1479. exportButton.innerText = 'Export Settings';
  1480. exportButton.classList.add('btn-style-settings');
  1481. exportButton.onclick = exportSettings;
  1482.  
  1483. const importButton = document.createElement('button');
  1484. importButton.type = 'button';
  1485. importButton.innerText = 'Import Settings';
  1486. importButton.classList.add('btn-style-settings');
  1487. importButton.onclick = importSettings;
  1488.  
  1489. // Copyright
  1490. const copyright = document.createElement('a');
  1491. copyright.href = 'https://github.com/TimMacy';
  1492. copyright.target = '_blank';
  1493. copyright.innerText = '© 2024 Tim Macy';
  1494. copyright.title = 'Copyright by Tim Macy';
  1495. copyright.classList.add('copyright');
  1496.  
  1497. const spacer = document.createElement('div');
  1498. spacer.style = 'flex: 1;';
  1499.  
  1500. // Save, Reset, and Cancel Buttons
  1501. const buttonContainerSettings = document.createElement('div');
  1502. buttonContainerSettings.classList.add('button-container-settings');
  1503.  
  1504. const saveButton = document.createElement('button');
  1505. saveButton.type = 'button';
  1506. saveButton.innerText = 'Save';
  1507. saveButton.classList.add('btn-style-settings');
  1508. saveButton.onclick = saveSettings;
  1509.  
  1510. const resetButton = document.createElement('button');
  1511. resetButton.type = 'button';
  1512. resetButton.innerText = 'Reset to Default';
  1513. resetButton.classList.add('btn-style-settings');
  1514. resetButton.onclick = async () => {
  1515. const userConfirmed = window.confirm("All settings will be reset to their default values.");
  1516. if (!userConfirmed) { return; }
  1517. try {
  1518. USER_CONFIG = { ...DEFAULT_CONFIG };
  1519. await GM.setValue('USER_CONFIG', USER_CONFIG);
  1520. showNotification('Settings have been reset to default!');
  1521. document.getElementById('yt-transcript-settings-modal').style.display = 'none';
  1522. setTimeout(() => { location.reload(); }, 1000);
  1523. } catch (error) {
  1524. showNotification('Error resetting settings to default!');
  1525. console.error("YTE: Error resetting settings to default:", error);
  1526. }
  1527. };
  1528.  
  1529. const cancelButton = document.createElement('button');
  1530. cancelButton.type = 'button';
  1531. cancelButton.innerText = 'Cancel';
  1532. cancelButton.classList.add('btn-style-settings');
  1533. cancelButton.onclick = () => { modal.style.display = 'none'; document.body.style.overflow = ''; };
  1534.  
  1535. exportImportContainer.appendChild(exportButton);
  1536. exportImportContainer.appendChild(importButton);
  1537.  
  1538. buttonContainerSettings.appendChild(copyright);
  1539. buttonContainerSettings.appendChild(spacer);
  1540. buttonContainerSettings.appendChild(saveButton);
  1541. buttonContainerSettings.appendChild(resetButton);
  1542. buttonContainerSettings.appendChild(cancelButton);
  1543.  
  1544. buttonContainer.appendChild(exportImportContainer);
  1545. buttonContainer.appendChild(buttonContainerSettings);
  1546.  
  1547. form.appendChild(buttonContainer);
  1548. modalContent.appendChild(form);
  1549. modal.appendChild(modalContent);
  1550. document.body.appendChild(modal);
  1551. document.body.style.overflow = 'hidden';
  1552.  
  1553. // text area scroll on click
  1554. let animationTriggered = false;
  1555.  
  1556. document.querySelector('.chatgpt-prompt-textarea').addEventListener('click', function () {
  1557. if (animationTriggered) return;
  1558. animationTriggered = true;
  1559.  
  1560. const modalContent = this.closest('#yt-transcript-settings-form');
  1561. if (!modalContent) { animationTriggered = false; return; }
  1562.  
  1563. const textArea = this;
  1564. const buttons = modalContent.querySelector('.button-container-end');
  1565. const startHeight = 65;
  1566. const endHeight = 432;
  1567. const duration = 500;
  1568.  
  1569. const modalContentRect = modalContent.getBoundingClientRect();
  1570. const textAreaRect = textArea.getBoundingClientRect();
  1571. const buttonsRect = buttons.getBoundingClientRect();
  1572.  
  1573. const textAreaTop = textAreaRect.top - modalContentRect.top + modalContent.scrollTop;
  1574. const buttonsBottom = buttonsRect.bottom - modalContentRect.top + modalContent.scrollTop;
  1575. const contentHeightAfterExpansion = buttonsBottom + (endHeight - startHeight);
  1576. const modalVisibleHeight = modalContent.clientHeight;
  1577. const contentWillFit = contentHeightAfterExpansion <= modalVisibleHeight;
  1578. const maxScrollTop = textAreaTop;
  1579.  
  1580. let desiredScrollTop;
  1581.  
  1582. if (contentWillFit) { desiredScrollTop = contentHeightAfterExpansion - modalVisibleHeight;
  1583. } else { desiredScrollTop = buttonsBottom + (endHeight - startHeight) - modalVisibleHeight; }
  1584.  
  1585. const newScrollTop = Math.min(desiredScrollTop, maxScrollTop);
  1586. const startScrollTop = modalContent.scrollTop;
  1587. const scrollDistance = newScrollTop - startScrollTop;
  1588. const startTime = performance.now();
  1589.  
  1590. function animateScroll(currentTime) {
  1591. const elapsedTime = currentTime - startTime;
  1592. const progress = Math.min(elapsedTime / duration, 1);
  1593.  
  1594. const easeProgress = progress < 0.5
  1595. ? 2 * progress * progress
  1596. : -1 + (4 - 2 * progress) * progress;
  1597.  
  1598. const currentScrollTop = startScrollTop + scrollDistance * easeProgress;
  1599. modalContent.scrollTop = currentScrollTop;
  1600.  
  1601. if (progress < 1) { requestAnimationFrame(animateScroll);
  1602. } else { animationTriggered = false; }
  1603. }
  1604.  
  1605. requestAnimationFrame(animateScroll);
  1606. });
  1607.  
  1608. // close modal on overlay click
  1609. modal.addEventListener('click', (event) => {
  1610. if (event.target === modal) {
  1611. modal.style.display = 'none';
  1612. document.body.style.overflow = '';
  1613. }
  1614. });
  1615.  
  1616. // close modal with ESC key
  1617. window.addEventListener('keydown', function (event) {
  1618. if (event.key === 'Escape') {
  1619. modal.style.display = 'none';
  1620. document.body.style.overflow = '';
  1621. }
  1622. });
  1623.  
  1624. // sub-panels
  1625. function showSubPanel(panelContent, panelId) {
  1626. let subPanelOverlay = document.querySelector(`.sub-panel-overlay[data-panel-id="${panelId}"]`);
  1627.  
  1628. if (subPanelOverlay) { subPanelOverlay.style.display = 'flex'; }
  1629. else {
  1630. subPanelOverlay = document.createElement('div');
  1631. subPanelOverlay.classList.add('sub-panel-overlay');
  1632. subPanelOverlay.setAttribute('data-panel-id', panelId);
  1633.  
  1634. const subPanel = document.createElement('div');
  1635. subPanel.classList.add('sub-panel');
  1636.  
  1637. const closeButton = document.createElement('button');
  1638. closeButton.type = 'button';
  1639. closeButton.innerText = 'Close';
  1640. closeButton.classList.add('btn-style-settings');
  1641. closeButton.onclick = () => { subPanelOverlay.style.display = 'none'; };
  1642. subPanel.appendChild(closeButton);
  1643.  
  1644. if (panelContent) { subPanel.appendChild(panelContent); }
  1645. subPanelOverlay.appendChild(subPanel);
  1646. document.body.appendChild(subPanelOverlay);
  1647. }
  1648. }
  1649.  
  1650. // links in header
  1651. function createLinksInHeaderContent() {
  1652. const form = document.createElement('form');
  1653. form.id = 'links-in-header-form';
  1654.  
  1655. const subPanelHeader = document.createElement('div');
  1656. subPanelHeader.classList.add('sub-panel-header');
  1657. subPanelHeader.textContent = 'Configure the links on the left side of the YouTube header';
  1658. form.appendChild(subPanelHeader);
  1659.  
  1660. const infoLinksHeader = document.createElement('small');
  1661. infoLinksHeader.innerText = "Up to seven links can be added next to the logo. An empty 'Link Text' field won't insert the link into the header.\nIf the left navigation bar is hidden, a replacement icon will prepend the links, while retaining the default functionality of opening and closing the sidebar.";
  1662. infoLinksHeader.classList.add('chrome-info');
  1663. form.appendChild(infoLinksHeader);
  1664.  
  1665. const sidebarContainer = document.createElement('div');
  1666. sidebarContainer.classList.add('sidebar-container');
  1667.  
  1668. // hide left navigation bar and replacement icon
  1669. const checkboxField = createCheckboxField('Hide Left Navigation Bar', 'mButtonDisplay', USER_CONFIG.mButtonDisplay);
  1670. sidebarContainer.appendChild(checkboxField);
  1671.  
  1672. const inputField = createInputField('Left Navigation Bar Replacement Icon', 'mButtonText', USER_CONFIG.mButtonText, 'label-mButtonText');
  1673. sidebarContainer.appendChild(inputField);
  1674.  
  1675. form.appendChild(sidebarContainer);
  1676.  
  1677. // function to create a link input group
  1678. function createButtonInputGroup(linkNumber) {
  1679. const container = document.createElement('div');
  1680. container.classList.add('links-header-container');
  1681.  
  1682. // link text
  1683. const textField = createInputField(`Link ${linkNumber} Text`, `buttonLeft${linkNumber}Text`, USER_CONFIG[`buttonLeft${linkNumber}Text`], `label-buttonLeft${linkNumber}Text`);
  1684. container.appendChild(textField);
  1685.  
  1686. // link URL
  1687. const urlField = createInputField(`Link ${linkNumber} URL`, `buttonLeft${linkNumber}Url`, USER_CONFIG[`buttonLeft${linkNumber}Url`], `label-buttonLeft${linkNumber}Url`);
  1688. container.appendChild(urlField);
  1689.  
  1690. return container;
  1691. }
  1692.  
  1693. // create input groups for links 1 through 6
  1694. for (let i = 1; i <= 7; i++) {
  1695. form.appendChild(createButtonInputGroup(i));
  1696. }
  1697.  
  1698. return form;
  1699. }
  1700.  
  1701. // custom css
  1702. function createCustomCSSContent() {
  1703. const form = document.createElement('form');
  1704. form.id = 'custom-css-form';
  1705.  
  1706. const subPanelHeader = document.createElement('div');
  1707. subPanelHeader.classList.add('sub-panel-header');
  1708. subPanelHeader.textContent = 'Customize YouTube Appearance';
  1709. form.appendChild(subPanelHeader);
  1710.  
  1711. // dim watched videos
  1712. const videosWatchedContainer = createSliderInputField( 'Change Opacity of Watched Videos (default 0.5):', 'videosWatchedOpacity', USER_CONFIG.videosWatchedOpacity, '0', '1', '0.1' );
  1713. form.appendChild(videosWatchedContainer);
  1714.  
  1715. // text transform
  1716. form.appendChild(createSelectField('Text Transform:', 'label-Text-Transform', 'textTransform', USER_CONFIG.textTransform, {
  1717. 'uppercase': 'uppercase - THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.',
  1718. 'lowercase': 'lowercase - the quick brown fox jumps over the lazy dog.',
  1719. 'capitalize': 'capitalize - The Quick Brown Fox Jumps Over The Lazy Dog.',
  1720. 'normal-case': 'normal-case (default) - The quick brown fox jumps over the lazy dog.',
  1721. }));
  1722.  
  1723. // font size
  1724. const defaultFontSizeField = createNumberInputField('Font Size (default: 10px)', 'defaultFontSize', USER_CONFIG.defaultFontSize);
  1725. form.appendChild(defaultFontSizeField);
  1726.  
  1727. // videos per row
  1728. const videosPerRow = createNumberInputField('Number of Videos per Row (default: 3)', 'videosPerRow', USER_CONFIG.videosPerRow);
  1729. form.appendChild(videosPerRow);
  1730.  
  1731. // hide voice search button
  1732. const hideVoiceSearch = createCheckboxField('Hide Voice Search Button in the Header (default: no)', 'hideVoiceSearch', USER_CONFIG.hideVoiceSearch);
  1733. form.appendChild(hideVoiceSearch);
  1734.  
  1735. // hide create button
  1736. const hideCreateButton = createCheckboxField('Hide Create Button in the Header (default: no)', 'hideCreateButton', USER_CONFIG.hideCreateButton);
  1737. form.appendChild(hideCreateButton);
  1738.  
  1739. // hide watched videos globally
  1740. const videosHideWatchedGlobal = createCheckboxField('Hide Watched Videos Globally (default: no)', 'videosHideWatchedGlobal', USER_CONFIG.videosHideWatchedGlobal);
  1741. form.appendChild(videosHideWatchedGlobal);
  1742.  
  1743. // hide miniplayer
  1744. const hideMiniplayer = createCheckboxField('Hide Miniplayer (default: no)', 'hideMiniplayer', USER_CONFIG.hideMiniplayer);
  1745. form.appendChild(hideMiniplayer);
  1746.  
  1747. // square and compact search bar
  1748. const squareSearchBar = createCheckboxField('Square and Compact Search Bar (default: yes)', 'squareSearchBar', USER_CONFIG.squareSearchBar);
  1749. form.appendChild(squareSearchBar);
  1750.  
  1751. // square design
  1752. const squareDesign = createCheckboxField('Square Design (default: no)', 'squareDesign', USER_CONFIG.squareDesign);
  1753. form.appendChild(squareDesign);
  1754.  
  1755. // compact layout
  1756. const compactLayout = createCheckboxField('Compact Layout (default: no)', 'compactLayout', USER_CONFIG.compactLayout);
  1757. form.appendChild(compactLayout);
  1758.  
  1759. return form;
  1760. }
  1761.  
  1762. // color code videos
  1763. function createColorCodeVideosContent() {
  1764. const form = document.createElement('form');
  1765. form.id = 'color-code-videos-form';
  1766.  
  1767. const subPanelHeader = document.createElement('div');
  1768. subPanelHeader.classList.add('sub-panel-header');
  1769. subPanelHeader.textContent = 'Configure Color Codes for Videos on Home';
  1770. form.appendChild(subPanelHeader);
  1771.  
  1772. // activate color code videos
  1773. const checkboxField = createCheckboxField('Activate Color Code Videos (default: yes)', 'colorCodeVideosEnabled', USER_CONFIG.colorCodeVideosEnabled );
  1774. form.appendChild(checkboxField);
  1775.  
  1776. const checkboxFieldWatched = createCheckboxField('Hide Watched Videos (Only on Home) (default: no)', 'videosHideWatched', USER_CONFIG.videosHideWatched );
  1777. form.appendChild(checkboxFieldWatched);
  1778.  
  1779. // opacity picker for old videos
  1780. const videosOldContainer = createSliderInputField( 'Change opacity of videos uploaded more than 6 months ago:', 'videosOldOpacity', USER_CONFIG.videosOldOpacity, '0', '1', '0.1' );
  1781. form.appendChild(videosOldContainer);
  1782.  
  1783. // color pickers for different video ages
  1784. const videosAgeContainer = document.createElement('div');
  1785. videosAgeContainer.classList.add('videos-age-container');
  1786.  
  1787. function createLabelColorPair(labelText, configKey) {
  1788. const row = document.createElement('div');
  1789. row.classList.add('videos-age-row');
  1790.  
  1791. const label = document.createElement('span');
  1792. label.classList.add('label-style-settings');
  1793. label.innerText = labelText;
  1794. row.appendChild(label);
  1795.  
  1796. const colorPicker = document.createElement('input');
  1797. colorPicker.type = 'color';
  1798. colorPicker.value = USER_CONFIG[configKey];
  1799. colorPicker.name = configKey;
  1800. row.appendChild(colorPicker);
  1801.  
  1802. videosAgeContainer.appendChild(row);
  1803. }
  1804.  
  1805. createLabelColorPair('Videos uploaded within the last 24 hours:', 'videosAgeColorPickerNewly');
  1806. createLabelColorPair('Videos uploaded within the past week:', 'videosAgeColorPickerRecent');
  1807. createLabelColorPair('Videos uploaded within the past month:', 'videosAgeColorPickerLately');
  1808. createLabelColorPair('Videos currently live:', 'videosAgeColorPickerLive');
  1809. createLabelColorPair('The word "streamed" from Videos that were live:', 'videosAgeColorPickerStreamed');
  1810. createLabelColorPair('Scheduled videos and upcoming live streams:', 'videosAgeColorPickerUpcoming');
  1811.  
  1812. form.appendChild(videosAgeContainer);
  1813.  
  1814. return form;
  1815. }
  1816. }
  1817.  
  1818. // helper function to create input fields
  1819. function createInputField(labelText, settingKey, settingValue, labelClass) {
  1820. const container = document.createElement('div');
  1821. container.classList.add('url-container');
  1822.  
  1823. const label = document.createElement('label');
  1824. label.innerText = labelText;
  1825. label.className = labelClass;
  1826. label.classList.add('label-style-settings');
  1827. container.appendChild(label);
  1828.  
  1829. const input = document.createElement('input');
  1830. const fieldInputClass = `input-field-${settingKey}`;
  1831. input.type = 'text';
  1832. input.name = settingKey;
  1833. input.value = settingValue;
  1834. input.classList.add('input-field-url');
  1835. input.classList.add(fieldInputClass);
  1836. container.appendChild(input);
  1837.  
  1838. return container;
  1839. }
  1840.  
  1841. // helper function to create select fields
  1842. function createSelectField(labelText, labelClass, settingKey, settingValue, options) {
  1843. const container = document.createElement('div');
  1844. container.classList.add('file-naming-container');
  1845. const label = document.createElement('label');
  1846. label.innerText = labelText;
  1847. label.className = labelClass;
  1848. label.classList.add('label-style-settings');
  1849. container.appendChild(label);
  1850. const select = document.createElement('div');
  1851. select.classList.add('select-file-naming');
  1852. select.innerText = options[settingValue];
  1853. select.setAttribute('tabindex', '0');
  1854. container.appendChild(select);
  1855.  
  1856. const hiddenSelect = document.createElement('select');
  1857. hiddenSelect.name = settingKey;
  1858. hiddenSelect.classList.add('hidden-select');
  1859. for (const [value, text] of Object.entries(options)) {
  1860. const option = document.createElement('option');
  1861. option.value = value;
  1862. option.text = text;
  1863. if (value === settingValue) {
  1864. option.selected = true;
  1865. }
  1866. hiddenSelect.appendChild(option);
  1867. }
  1868. container.appendChild(hiddenSelect);
  1869. const dropdownList = document.createElement('div');
  1870. dropdownList.classList.add('dropdown-list');
  1871. container.appendChild(dropdownList);
  1872. for (const [value, text] of Object.entries(options)) {
  1873. const item = document.createElement('div');
  1874. item.classList.add('dropdown-item');
  1875. item.innerText = text;
  1876. item.dataset.value = value;
  1877. if (value === settingValue) { item.classList.add('dropdown-item-selected'); }
  1878.  
  1879. item.addEventListener('click', () => {
  1880. const previouslySelected = dropdownList.querySelector('.dropdown-item-selected');
  1881. if (previouslySelected) {
  1882. previouslySelected.classList.remove('dropdown-item-selected');
  1883. }
  1884. item.classList.add('dropdown-item-selected');
  1885. select.innerText = text;
  1886. hiddenSelect.value = value;
  1887. dropdownList.classList.remove('show');
  1888. });
  1889. dropdownList.appendChild(item);
  1890. }
  1891.  
  1892. // open dropdown
  1893. select.addEventListener('click', (event) => {
  1894. event.stopPropagation();
  1895. dropdownList.classList.toggle('show');
  1896. });
  1897.  
  1898. // close dropdown
  1899. document.addEventListener('click', () => {
  1900. dropdownList.classList.remove('show');
  1901. });
  1902. return container;
  1903. }
  1904.  
  1905. // helper function to create checkbox fields
  1906. function createCheckboxField(labelText, settingKey, settingValue) {
  1907. const container = document.createElement('div');
  1908. container.classList.add('checkbox-container');
  1909.  
  1910. const label = document.createElement('label');
  1911. label.classList.add('checkbox-label');
  1912.  
  1913. const checkbox = document.createElement('input');
  1914. checkbox.type = 'checkbox';
  1915. checkbox.name = settingKey;
  1916. checkbox.checked = settingValue;
  1917. checkbox.classList.add('checkbox-field');
  1918. label.appendChild(checkbox);
  1919.  
  1920. const span = document.createElement('span');
  1921. span.innerText = labelText;
  1922. label.appendChild(span);
  1923.  
  1924. container.appendChild(label);
  1925. return container;
  1926. }
  1927.  
  1928. // helper function to create a number input fields
  1929. function createNumberInputField(labelText, settingKey, settingValue) {
  1930. const container = document.createElement('div');
  1931. container.classList.add('number-input-container');
  1932.  
  1933. const label = document.createElement('label');
  1934. label.classList.add('number-input-label');
  1935.  
  1936. const numberInput = document.createElement('input');
  1937. numberInput.type = 'number';
  1938. numberInput.name = settingKey;
  1939. numberInput.value = settingValue;
  1940. numberInput.min = 1;
  1941. numberInput.max = 20;
  1942. numberInput.step = 1;
  1943. numberInput.classList.add('number-input-field');
  1944. label.appendChild(numberInput);
  1945.  
  1946. const span = document.createElement('span');
  1947. span.innerText = labelText;
  1948. label.appendChild(span);
  1949.  
  1950. container.appendChild(label);
  1951. return container;
  1952. }
  1953.  
  1954. // helper function to create a slider fields
  1955. function createSliderInputField(labelText, settingKey, settingValue, min, max, step) {
  1956. const container = document.createElement('div');
  1957. container.classList.add('videos-old-container');
  1958.  
  1959. const label = document.createElement('span');
  1960. label.classList.add('label-style-settings');
  1961. label.innerText = labelText;
  1962. container.appendChild(label);
  1963.  
  1964. const sliderContainer = document.createElement('div');
  1965. sliderContainer.classList.add('slider-container');
  1966.  
  1967. const leftLabel = document.createElement('span');
  1968. leftLabel.innerText = min;
  1969. sliderContainer.appendChild(leftLabel);
  1970.  
  1971. const slider = document.createElement('input');
  1972. slider.type = 'range';
  1973. slider.min = min;
  1974. slider.max = max;
  1975. slider.step = step;
  1976. slider.value = settingValue;
  1977. slider.name = settingKey;
  1978. sliderContainer.appendChild(slider);
  1979.  
  1980. const rightLabel = document.createElement('span');
  1981. rightLabel.innerText = max;
  1982. sliderContainer.appendChild(rightLabel);
  1983.  
  1984. const currentValue = document.createElement('span');
  1985. currentValue.innerText = `(${parseFloat(slider.value).toFixed(1)})`;
  1986. sliderContainer.appendChild(currentValue);
  1987.  
  1988. container.appendChild(sliderContainer);
  1989.  
  1990. slider.addEventListener('input', (e) => {
  1991. const value = parseFloat(e.target.value).toFixed(1);
  1992. currentValue.innerText = `(${value})`;
  1993. });
  1994.  
  1995. return container;
  1996. }
  1997.  
  1998. // helper function to create a textarea fields
  1999. function createTextareaField(labelText, settingKey, settingValue, labelClass) {
  2000. const container = document.createElement('chatgpt-prompt');
  2001. const label = document.createElement('label');
  2002. label.innerText = labelText;
  2003. label.className = labelClass;
  2004. label.classList.add('label-style-settings');
  2005. container.appendChild(label);
  2006. const textarea = document.createElement('textarea');
  2007. textarea.name = settingKey;
  2008. textarea.value = settingValue;
  2009. textarea.classList.add('chatgpt-prompt-textarea');
  2010. container.appendChild(textarea);
  2011. return container;
  2012. }
  2013.  
  2014. // function to save settings
  2015. async function saveSettings() {
  2016. const form = document.getElementById('yt-transcript-settings-form');
  2017. const subPanelLinks = document.getElementById('links-in-header-form');
  2018. const subPanelCustomCSS = document.getElementById('custom-css-form');
  2019. const subPanelColor = document.getElementById('color-code-videos-form');
  2020.  
  2021. // function to ensure secure URLs
  2022. function normalizeUrl(url) {
  2023. url = url.trim();
  2024. if (/^https?:\/\//i.test(url)) {
  2025. url = url.replace(/^http:\/\//i, 'https://');
  2026. } else { url = 'https://' + url; }
  2027. return url;
  2028. }
  2029.  
  2030. // validate ChatGPT URL
  2031. let targetChatGPTUrl = form.elements.targetChatGPTUrl.value.trim();
  2032. if (targetChatGPTUrl !== '') {
  2033. USER_CONFIG.targetChatGPTUrl = normalizeUrl(targetChatGPTUrl);
  2034. } else { delete USER_CONFIG.targetChatGPTUrl; }
  2035.  
  2036. // validate NotebookLM URL
  2037. let targetNotebookLMUrl = form.elements.targetNotebookLMUrl.value.trim();
  2038. if (targetNotebookLMUrl !== '') {
  2039. USER_CONFIG.targetNotebookLMUrl = normalizeUrl(targetNotebookLMUrl);
  2040. } else { delete USER_CONFIG.targetNotebookLMUrl; }
  2041. // save other settings
  2042. USER_CONFIG.fileNamingFormat = form.elements.fileNamingFormat.value;
  2043. USER_CONFIG.includeTimestamps = form.elements.includeTimestamps.checked;
  2044. USER_CONFIG.includeChapterHeaders = form.elements.includeChapterHeaders.checked;
  2045. USER_CONFIG.openSameTab = form.elements.openSameTab.checked;
  2046. USER_CONFIG.autoOpenChapters = form.elements.autoOpenChapters.checked;
  2047. USER_CONFIG.DisplayRemainingTime = form.elements.DisplayRemainingTime.checked;
  2048. USER_CONFIG.ProgressBar = form.elements.ProgressBar.checked;
  2049. USER_CONFIG.preventBackgroundExecution = form.elements.preventBackgroundExecution.checked;
  2050. USER_CONFIG.ChatGPTPrompt = form.elements.ChatGPTPrompt.value;
  2051. // initialize buttonIcons if not already
  2052. USER_CONFIG.buttonIcons = USER_CONFIG.buttonIcons || {};
  2053. // save button icons, removing empty values to use defaults
  2054. const buttonIconDownload = form.elements.buttonIconDownload.value.trim();
  2055. const buttonIconChatGPT = form.elements.buttonIconChatGPT.value.trim();
  2056. const buttonIconNotebookLM = form.elements.buttonIconNotebookLM.value.trim();
  2057. const buttonIconSettings = form.elements.buttonIconSettings.value.trim();
  2058. if (buttonIconDownload !== '') { USER_CONFIG.buttonIcons.download = buttonIconDownload; } else { delete USER_CONFIG.buttonIcons.download; }
  2059. if (buttonIconChatGPT !== '') { USER_CONFIG.buttonIcons.ChatGPT = buttonIconChatGPT; } else { delete USER_CONFIG.buttonIcons.ChatGPT; }
  2060. if (buttonIconNotebookLM !== '') { USER_CONFIG.buttonIcons.NotebookLM = buttonIconNotebookLM; } else { delete USER_CONFIG.buttonIcons.NotebookLM; }
  2061. if (buttonIconSettings !== '') { USER_CONFIG.buttonIcons.settings = buttonIconSettings; } else { delete USER_CONFIG.buttonIcons.settings; }
  2062.  
  2063. // save sub panels - links in header
  2064. if (subPanelLinks) {
  2065. USER_CONFIG.buttonLeft1Text = subPanelLinks.elements.buttonLeft1Text.value;
  2066. USER_CONFIG.buttonLeft1Url = subPanelLinks.elements.buttonLeft1Url.value;
  2067. USER_CONFIG.buttonLeft2Text = subPanelLinks.elements.buttonLeft2Text.value;
  2068. USER_CONFIG.buttonLeft2Url = subPanelLinks.elements.buttonLeft2Url.value;
  2069. USER_CONFIG.buttonLeft3Text = subPanelLinks.elements.buttonLeft3Text.value;
  2070. USER_CONFIG.buttonLeft3Url = subPanelLinks.elements.buttonLeft3Url.value;
  2071. USER_CONFIG.buttonLeft4Text = subPanelLinks.elements.buttonLeft4Text.value;
  2072. USER_CONFIG.buttonLeft4Url = subPanelLinks.elements.buttonLeft4Url.value;
  2073. USER_CONFIG.buttonLeft5Text = subPanelLinks.elements.buttonLeft5Text.value;
  2074. USER_CONFIG.buttonLeft5Url = subPanelLinks.elements.buttonLeft5Url.value;
  2075. USER_CONFIG.buttonLeft6Text = subPanelLinks.elements.buttonLeft6Text.value;
  2076. USER_CONFIG.buttonLeft6Url = subPanelLinks.elements.buttonLeft6Url.value;
  2077. USER_CONFIG.buttonLeft7Text = subPanelLinks.elements.buttonLeft7Text.value;
  2078. USER_CONFIG.buttonLeft7Url = subPanelLinks.elements.buttonLeft7Url.value;
  2079. USER_CONFIG.mButtonText = subPanelLinks.elements.mButtonText.value;
  2080. USER_CONFIG.mButtonDisplay = subPanelLinks.elements.mButtonDisplay.checked;
  2081. }
  2082.  
  2083. // save sub panels - custom css
  2084. if (subPanelCustomCSS) {
  2085. USER_CONFIG.textTransform = subPanelCustomCSS.elements.textTransform.value;
  2086. USER_CONFIG.defaultFontSize = parseFloat(subPanelCustomCSS.elements.defaultFontSize.value);
  2087. USER_CONFIG.videosWatchedOpacity = parseFloat(subPanelCustomCSS.elements.videosWatchedOpacity.value);
  2088. USER_CONFIG.videosHideWatchedGlobal = subPanelCustomCSS.elements.videosHideWatchedGlobal.checked;
  2089. USER_CONFIG.videosPerRow = parseInt(subPanelCustomCSS.elements.videosPerRow.value);
  2090. USER_CONFIG.hideVoiceSearch = subPanelCustomCSS.elements.hideVoiceSearch.checked;
  2091. USER_CONFIG.hideCreateButton = subPanelCustomCSS.elements.hideCreateButton.checked;
  2092. USER_CONFIG.hideMiniplayer = subPanelCustomCSS.elements.hideMiniplayer.checked;
  2093. USER_CONFIG.squareSearchBar = subPanelCustomCSS.elements.squareSearchBar.checked;
  2094. USER_CONFIG.squareDesign = subPanelCustomCSS.elements.squareDesign.checked;
  2095. USER_CONFIG.compactLayout = subPanelCustomCSS.elements.compactLayout.checked;
  2096. }
  2097.  
  2098. // save sub panels - color code videos
  2099. if (subPanelColor) {
  2100. USER_CONFIG.colorCodeVideosEnabled = subPanelColor.elements.colorCodeVideosEnabled.checked;
  2101. USER_CONFIG.videosHideWatched = subPanelColor.elements.videosHideWatched.checked;
  2102. USER_CONFIG.videosOldOpacity = parseFloat(subPanelColor.elements.videosOldOpacity.value);
  2103. USER_CONFIG.videosAgeColorPickerNewly = subPanelColor.elements.videosAgeColorPickerNewly.value;
  2104. USER_CONFIG.videosAgeColorPickerRecent = subPanelColor.elements.videosAgeColorPickerRecent.value;
  2105. USER_CONFIG.videosAgeColorPickerLately = subPanelColor.elements.videosAgeColorPickerLately.value;
  2106. USER_CONFIG.videosAgeColorPickerLive = subPanelColor.elements.videosAgeColorPickerLive.value;
  2107. USER_CONFIG.videosAgeColorPickerStreamed = subPanelColor.elements.videosAgeColorPickerStreamed.value;
  2108. USER_CONFIG.videosAgeColorPickerUpcoming = subPanelColor.elements.videosAgeColorPickerUpcoming.value;
  2109. }
  2110.  
  2111. // save updated config
  2112. try {
  2113. await GM.setValue('USER_CONFIG', USER_CONFIG);
  2114. // close modal
  2115. document.getElementById('yt-transcript-settings-modal').style.display = 'none';
  2116. showNotification('Settings have been updated!');
  2117. setTimeout(() => { location.reload(); }, 1000);
  2118. } catch (error) {
  2119. showNotification('Error saving new user config!');
  2120. console.error("YTE: Error saving user configuration:", error);
  2121. }
  2122. }
  2123.  
  2124. // export and import settings
  2125. async function exportSettings() {
  2126. try {
  2127. const scriptVersion = GM.info.script.version;
  2128. const settingsString = JSON.stringify(USER_CONFIG, null, 2);
  2129. const blob = new Blob([settingsString], { type: 'application/json' });
  2130. const url = URL.createObjectURL(blob);
  2131.  
  2132. const a = document.createElement('a');
  2133. a.href = url;
  2134. a.download = `YouTube-Transcript-Exporter_v${scriptVersion}_Backup_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
  2135. document.body.appendChild(a);
  2136. a.click();
  2137. document.body.removeChild(a);
  2138. URL.revokeObjectURL(url);
  2139.  
  2140. showNotification('Settings have been exported.');
  2141. } catch (error) {
  2142. showNotification("Error exporting settings!");
  2143. console.error("YTE: Error exporting user settings:", error);
  2144. }
  2145. }
  2146.  
  2147. let fileInputSettings;
  2148. async function importSettings() {
  2149. const handleFile = (e) => {
  2150. const file = e.target.files[0];
  2151. if (!file) return;
  2152.  
  2153. const reader = new FileReader();
  2154. reader.onload = (event) => {
  2155. const fileContent = event.target.result;
  2156. try {
  2157. const importedConfig = JSON.parse(fileContent);
  2158. if (typeof importedConfig === 'object' && importedConfig !== null) {
  2159. USER_CONFIG = { ...DEFAULT_CONFIG, ...importedConfig };
  2160. GM.setValue('USER_CONFIG', USER_CONFIG);
  2161. showNotification('Settings have been imported.');
  2162. setTimeout(() => {
  2163. location.reload();
  2164. }, 1000);
  2165. } else {
  2166. showNotification('Invalid JSON format!');
  2167. }
  2168. } catch (error) {
  2169. showNotification('Invalid JSON format!');
  2170. }
  2171. };
  2172. reader.readAsText(file);
  2173. };
  2174.  
  2175. const createOrResetFileInput = () => {
  2176. if (!fileInputSettings) {
  2177. fileInputSettings = document.createElement('input');
  2178. fileInputSettings.type = 'file';
  2179. fileInputSettings.accept = 'application/json';
  2180. fileInputSettings.id = 'fileInputSettings';
  2181. fileInputSettings.style.display = 'none';
  2182. fileInputSettings.addEventListener('change', handleFile);
  2183. document.body.appendChild(fileInputSettings);
  2184. } else {
  2185. fileInputSettings.value = '';
  2186. }
  2187. };
  2188.  
  2189. createOrResetFileInput();
  2190. fileInputSettings.click();
  2191. }
  2192.  
  2193. // function to display a notification for settings change or reset
  2194. function showNotification(message) {
  2195. const overlay = document.createElement('div');
  2196. overlay.classList.add('overlay');
  2197.  
  2198. const modal = document.createElement('div');
  2199. modal.classList.add('notification');
  2200. modal.innerText = message;
  2201.  
  2202. overlay.appendChild(modal);
  2203. document.body.appendChild(overlay);
  2204.  
  2205. setTimeout(() => { overlay.remove(); }, 1000);
  2206. }
  2207.  
  2208. // function to add the YouTube Transcript Exporter buttons
  2209. function buttonLocation(buttons, callback) {
  2210. const masthead = document.querySelector('#end');
  2211. if (masthead) {
  2212. buttons.forEach(({ id, text, clickHandler, tooltip }) => {
  2213. // button wrapper
  2214. const buttonWrapper = document.createElement('div');
  2215. buttonWrapper.classList.add('button-wrapper');
  2216. // buttons
  2217. const button = document.createElement('button');
  2218. button.id = id;
  2219. button.innerText = text;
  2220. button.classList.add('button-style');
  2221. if (id === 'transcript-settings-button') {
  2222. button.classList.add('button-style-settings'); }
  2223. button.addEventListener('click', clickHandler);
  2224. // tooltip div
  2225. const tooltipDiv = document.createElement('div');
  2226. tooltipDiv.innerText = tooltip;
  2227. tooltipDiv.classList.add('button-tooltip');
  2228. // tooltip arrow
  2229. const arrowDiv = document.createElement('div');
  2230. arrowDiv.classList.add('button-tooltip-arrow');
  2231. tooltipDiv.appendChild(arrowDiv);
  2232. // show and hide tooltip on hover
  2233. let tooltipTimeout;
  2234. button.addEventListener('mouseenter', () => {
  2235. tooltipTimeout = setTimeout(() => {
  2236. tooltipDiv.style.visibility = 'visible';
  2237. tooltipDiv.style.opacity = '1';
  2238. }, 700);
  2239. });
  2240. button.addEventListener('mouseleave', () => {
  2241. clearTimeout(tooltipTimeout);
  2242. tooltipDiv.style.visibility = 'hidden';
  2243. tooltipDiv.style.opacity = '0';
  2244. });
  2245. // append button elements
  2246. buttonWrapper.appendChild(button);
  2247. buttonWrapper.appendChild(tooltipDiv);
  2248. masthead.prepend(buttonWrapper);
  2249. });
  2250. } else {
  2251. const observer = new MutationObserver((mutations, obs) => {
  2252. const masthead = document.querySelector('#end');
  2253. if (masthead) {
  2254. obs.disconnect();
  2255. if (callback) callback();
  2256. }
  2257. });
  2258. observer.observe(document.body, {
  2259. childList: true,
  2260. subtree: true
  2261. });
  2262. }
  2263. }
  2264. function addButton() {
  2265. if (document.querySelector('.button-wrapper')) return;
  2266.  
  2267. const buttons = [
  2268. { id: 'transcript-settings-button', text: USER_CONFIG.buttonIcons.settings, clickHandler: showSettingsModal, tooltip: 'YouTube Transcript Exporter Settings', ariaLabel: 'YouTube Transcript Exporter Settings.' },
  2269. { 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.' },
  2270. { 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.' },
  2271. { 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.' }
  2272. ];
  2273. buttonLocation(buttons, addButton);
  2274. }
  2275.  
  2276. function addSettingsButton() {
  2277.  
  2278. if (document.querySelector('.button-wrapper')) return;
  2279.  
  2280. const buttons = [ { id: 'transcript-settings-button', text: USER_CONFIG.buttonIcons.settings, clickHandler: showSettingsModal, tooltip: 'YouTube Transcript Exporter Settings', ariaLabel: 'YouTube Transcript Exporter Settings.' }, ];
  2281.  
  2282. buttonLocation(buttons, addSettingsButton);
  2283. }
  2284.  
  2285. // functions to handle the button clicks
  2286. function handleChatGPTClick() { handleTranscriptAction(function() { selectAndCopyTranscript('ChatGPT'); }); }
  2287. function handleNotebookLMClick() { handleTranscriptAction(function() { selectAndCopyTranscript('NotebookLM'); }); }
  2288. function handleDownloadClick() { handleTranscriptAction(downloadTranscriptAsText); }
  2289.  
  2290. // function to check for a transcript
  2291. function handleTranscriptAction(callback) {
  2292.  
  2293. // check if the transcript button exists or at least the div
  2294. const transcriptButton = document.querySelector('#button-container button[aria-label="Show transcript"]');
  2295. if (transcriptButton) {
  2296. } else {
  2297. const transcriptSection = document.querySelector('ytd-video-description-transcript-section-renderer');
  2298. if (transcriptSection) {
  2299. } else {
  2300. alert('Transcript unavailable or cannot be found.\nPlease ensure the "Show transcript" button exists.');
  2301. console.log("YTE: Transcript button not found. Subtitles/closed captions unavailable or language unsupported.");
  2302. return;
  2303. }
  2304. }
  2305.  
  2306. // check if the transcript has loaded
  2307. const transcriptItems = document.querySelectorAll('ytd-transcript-segment-list-renderer ytd-transcript-segment-renderer');
  2308. if (transcriptItems.length > 0) {
  2309. callback();
  2310. } else {
  2311. alert('Transcript has not loaded successfully.\nPlease reload this page.');
  2312. console.log("YTE: Transcript has not loaded.");
  2313. return;
  2314. }
  2315. }
  2316.  
  2317. // function to get video information
  2318. function getVideoInfo() {
  2319. //const ytTitle = document.querySelector('#title yt-formatted-string')?.textContent.trim() || 'N/A';
  2320. const ytTitle = document.querySelector('div#title h1 > yt-formatted-string')?.textContent.trim() || 'N/A';
  2321. //const channelName = document.querySelector('ytd-channel-name#channel-name yt-formatted-string#text a')?.textContent.trim() || 'N/A';
  2322. const channelName = document.querySelector( 'ytd-video-owner-renderer ytd-channel-name#channel-name yt-formatted-string#text a' )?.textContent.trim() || 'N/A';
  2323. const uploadDate = document.querySelector('ytd-video-primary-info-renderer #info-strings yt-formatted-string')?.textContent.trim() || 'N/A';
  2324. const videoURL = window.location.href;
  2325.  
  2326. return { ytTitle, channelName, uploadDate, videoURL };
  2327. }
  2328.  
  2329. // function to get the transcript text
  2330. function getTranscriptText() {
  2331. const transcriptContainer = document.querySelector('ytd-transcript-segment-list-renderer #segments-container');
  2332. if (!transcriptContainer) {
  2333. //console.error("YTE: Transcript container not found.");
  2334. return '';
  2335. }
  2336.  
  2337. const transcriptElements = transcriptContainer.children;
  2338. let transcriptLines = [];
  2339.  
  2340. Array.from(transcriptElements).forEach(element => {
  2341. if (element.tagName === 'YTD-TRANSCRIPT-SECTION-HEADER-RENDERER') {
  2342. // chapter header segment
  2343. if (USER_CONFIG.includeChapterHeaders) {
  2344. const chapterTitleElement = element.querySelector('h2 > span');
  2345. if (chapterTitleElement) {
  2346. const chapterTitle = chapterTitleElement.textContent.trim();
  2347. transcriptLines.push(`\nChapter: ${chapterTitle}`);
  2348. }
  2349. }
  2350. } else if (element.tagName === 'YTD-TRANSCRIPT-SEGMENT-RENDERER') {
  2351. // transcript segment
  2352. const timeElement = element.querySelector('.segment-timestamp');
  2353. const textElement = element.querySelector('.segment-text');
  2354. if (timeElement && textElement) {
  2355. const time = timeElement.textContent.trim();
  2356. const text = textElement.innerText.trim();
  2357. if (USER_CONFIG.includeTimestamps) {
  2358. transcriptLines.push(`${time} ${text}`);
  2359. } else { transcriptLines.push(`${text}`); }
  2360. }
  2361. }
  2362. });
  2363.  
  2364. return transcriptLines.join('\n');
  2365. }
  2366.  
  2367. // function to select and copy the transcript into the clipboard
  2368. function selectAndCopyTranscript(target) {
  2369. const transcriptText = getTranscriptText();
  2370. const { ytTitle, channelName, uploadDate, videoURL } = getVideoInfo();
  2371. let finalText = '';
  2372. let targetUrl = '';
  2373. if (target === 'ChatGPT') {
  2374. 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}`;
  2375. targetUrl = USER_CONFIG.targetChatGPTUrl;
  2376. } else if (target === 'NotebookLM') {
  2377. finalText = `Information about the YouTube Video:\nTitle: ${ytTitle}\nChannel: ${channelName}\nUpload Date: ${uploadDate}\nURL: ${videoURL}\n\n\nYouTube Transcript:\n${transcriptText.trimStart()}`;
  2378. targetUrl = USER_CONFIG.targetNotebookLMUrl;
  2379. }
  2380. navigator.clipboard.writeText(finalText).then(() => {
  2381. showNotification('Transcript copied. Opening website . . .');
  2382. if (USER_CONFIG.openSameTab) { window.open(targetUrl, '_self');
  2383. } else { window.open(targetUrl, '_blank'); }
  2384. });
  2385. }
  2386.  
  2387. // function to get the formatted transcript with video details for the text file
  2388. function getFormattedTranscript() {
  2389. const transcriptText = getTranscriptText();
  2390. const { ytTitle, channelName, uploadDate, videoURL } = getVideoInfo();
  2391.  
  2392. return `Information about the YouTube Video:\nTitle: ${ytTitle}\nChannel: ${channelName}\nUpload Date: ${uploadDate}\nURL: ${videoURL}\n\n\nYouTube Transcript:\n${transcriptText.trimStart()}`;
  2393. }
  2394.  
  2395. // function to download the transcript as a text file
  2396. function downloadTranscriptAsText() {
  2397. const finalText = getFormattedTranscript();
  2398. const { ytTitle, channelName, uploadDate } = getVideoInfo();
  2399. const blob = new Blob([finalText], { type: 'text/plain' });
  2400.  
  2401. const sanitize = str => str.replace(/[<>:"/\\|?*]+/g, '');
  2402. const uploadDateFormatted = new Date(uploadDate).toLocaleDateString("en-CA");
  2403. // naming of text file based on user setting
  2404. let fileName = '';
  2405. switch (USER_CONFIG.fileNamingFormat) {
  2406. case 'title-channel': fileName = `${sanitize(ytTitle)} - ${sanitize(channelName)}.txt`; break;
  2407. case 'channel-title': fileName = `${sanitize(channelName)} - ${sanitize(ytTitle)}.txt`; break;
  2408. case 'date-title-channel': fileName = `${sanitize(uploadDateFormatted)} - ${sanitize(ytTitle)} - ${sanitize(channelName)}.txt`; break;
  2409. case 'date-channel-title': fileName = `${sanitize(uploadDateFormatted)} - ${sanitize(channelName)} - ${sanitize(ytTitle)}.txt`; break;
  2410. default: fileName = `${sanitize(ytTitle)} - ${sanitize(channelName)}.txt`;
  2411. }
  2412. const url = URL.createObjectURL(blob);
  2413. // create a temporary anchor element to trigger the download
  2414. const a = document.createElement('a');
  2415. a.href = url;
  2416. a.download = fileName;
  2417. document.body.appendChild(a);
  2418. a.click();
  2419. // clean up
  2420. document.body.removeChild(a);
  2421. URL.revokeObjectURL(url);
  2422. showNotification('File has been downloaded.');
  2423. }
  2424.  
  2425.  
  2426. // function to preload the transcript
  2427. function preLoadTranscript() {
  2428. return new Promise((resolve, reject) => {
  2429. const panel = document.querySelector( 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-searchable-transcript"]' );
  2430. if (!panel) {
  2431. console.log('YTE: Transcript panel not found. Reload the page to try again.');
  2432. showNotificationError("Transcript Not Available");
  2433. reject();
  2434. return;
  2435. }
  2436.  
  2437. const masthead = document.querySelector("#end");
  2438. const notification = document.createElement("div");
  2439. notification.classList.add("notification-error", "loading");
  2440. const textSpan = document.createElement("span");
  2441. textSpan.textContent = "Transcript Is Loading";
  2442. notification.appendChild(textSpan);
  2443. masthead.prepend(notification);
  2444.  
  2445. panel.classList.add("transcript-preload");
  2446. panel.setAttribute("visibility", "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED");
  2447.  
  2448. let loaded = false;
  2449.  
  2450. const observer = new MutationObserver(() => {
  2451. const transcriptItems = panel.querySelectorAll("ytd-transcript-segment-renderer");
  2452. if (transcriptItems.length > 0) {
  2453. loaded = true;
  2454. cleanup(false);
  2455. clearTimeout(fallbackTimer);
  2456. observer.disconnect();
  2457. resolve();
  2458. }
  2459. });
  2460.  
  2461. observer.observe(panel, { childList: true, subtree: true });
  2462.  
  2463. const fallbackTimer = setTimeout(() => {
  2464. if (!loaded) {
  2465. console.error( "YTE: The transcript took too long to load. Reload the page to try again." );
  2466. observer.disconnect();
  2467. cleanup(true);
  2468. reject();
  2469. }
  2470. }, 6000);
  2471.  
  2472. function cleanup(failed) {
  2473. notification.remove();
  2474. panel.classList.remove("transcript-preload");
  2475. panel.setAttribute("visibility", "ENGAGEMENT_PANEL_VISIBILITY_HIDDEN");
  2476. if (failed) { showNotificationError("Transcript Failed to Load"); }
  2477. }
  2478. });
  2479. }
  2480.  
  2481. // function to display a notification if transcript cannot be found
  2482. function showNotificationError(message) {
  2483. const masthead = document.querySelector('#end');
  2484. const notification = document.createElement('div');
  2485. notification.textContent = message;
  2486. notification.classList.add('notification-error');
  2487.  
  2488. masthead.prepend(notification);
  2489.  
  2490. if (document.visibilityState === 'hidden') {
  2491. document.addEventListener('visibilitychange', function handleVisibilityChange() {
  2492. if (document.visibilityState === 'visible') {
  2493. document.removeEventListener('visibilitychange', handleVisibilityChange);
  2494. setTimeout(() => notification.remove(), 3000);
  2495. }
  2496. });
  2497. } else { setTimeout(() => notification.remove(), 3000); }
  2498. }
  2499.  
  2500. // function to automatically open the chapter panel
  2501. function openChapters() {
  2502. const panel = document.querySelector('ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"]') ||
  2503. document.querySelector('ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-auto-chapters"]');
  2504. if (panel) {
  2505. panel.setAttribute('visibility', 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED');
  2506. } //else { console.log("YTE: Chapter panel not found or does not exist."); }
  2507. }
  2508.  
  2509. // function to display the remaining time based on playback speed
  2510. function RemainingTime() {
  2511. const STREAM_SELECTOR = '.video-stream.html5-main-video';
  2512. const CONTAINER_SELECTOR = '#columns #primary #below';
  2513. const FULLSCREEN_CONTAINER_SELECTOR = '#movie_player > div.ytp-chrome-bottom';
  2514. // function to format seconds
  2515. function formatTime(seconds) {
  2516. if (!isFinite(seconds) || seconds < 0) seconds = 0;
  2517. const h = Math.floor(seconds / 3600);
  2518. const m = Math.floor((seconds % 3600) / 60);
  2519. const s = Math.floor(seconds % 60);
  2520. return h > 0
  2521. ? `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
  2522. : `${m}:${s.toString().padStart(2, '0')}`;
  2523. }
  2524.  
  2525. const element = document.createElement('div');
  2526. element.classList.add('remaining-time-container');
  2527.  
  2528. const textNode = document.createTextNode('');
  2529. element.appendChild(textNode);
  2530.  
  2531. const initialContainer = document.querySelector(CONTAINER_SELECTOR);
  2532. if (initialContainer) { initialContainer.prepend(element); }
  2533. // hide for live video
  2534. const timeDisplay = document.querySelector('.ytp-time-display');
  2535. if (!timeDisplay) { console.error("YTE: RemainingTime: Required querySelector not found."); return; }
  2536. const detectLive = () => {
  2537. if (timeDisplay.classList.contains('ytp-live')) {
  2538. element.classList.add('live');
  2539. } else { element.classList.remove('live'); }
  2540. };
  2541. detectLive();
  2542. new MutationObserver(detectLive).observe(timeDisplay, { attributes: true });
  2543. // dynamic placement based on fullscreen mode
  2544. const updateContainer = () => {
  2545. const container = document.querySelector(CONTAINER_SELECTOR);
  2546. const fullscreenContainer = document.querySelector(FULLSCREEN_CONTAINER_SELECTOR);
  2547. const remainingTimeContainer = document.querySelector('.remaining-time-container');
  2548. if (document.fullscreenElement && fullscreenContainer) {
  2549. if (remainingTimeContainer && remainingTimeContainer.parentNode !== fullscreenContainer) {
  2550. fullscreenContainer.appendChild(remainingTimeContainer);
  2551. }
  2552. } else if (container) {
  2553. if (getComputedStyle(container).position === 'static') {
  2554. container.style.position = 'relative';
  2555. }
  2556. if (remainingTimeContainer && remainingTimeContainer.parentNode !== container) {
  2557. container.prepend(remainingTimeContainer);
  2558. }
  2559. }
  2560. };
  2561. document.addEventListener('fullscreenchange', () => { setTimeout(updateContainer, 250); });
  2562. updateContainer();
  2563. // time updates
  2564. const video = document.querySelector(STREAM_SELECTOR);
  2565. if (video) {
  2566. video.ontimeupdate = () => {
  2567. const duration = video.duration;
  2568. const currentTime = video.currentTime;
  2569. const playbackRate = video.playbackRate || 1;
  2570. const remaining = (duration - currentTime) / playbackRate;
  2571. const watchedPercent = duration ? Math.round((currentTime / duration) * 100) + '%' : '0%';
  2572. const totalFormatted = formatTime(duration);
  2573. const elapsedFormatted = formatTime(currentTime);
  2574. const remainingFormatted = formatTime(remaining);
  2575. textNode.data = `total: ${totalFormatted} | elapsed: ${elapsedFormatted} watched: ${watchedPercent} remaining: ${remainingFormatted} (${playbackRate}x)`;
  2576. };
  2577. }
  2578. }
  2579.  
  2580. // function to keep the progress bar visible with chapters container
  2581. function keepProgressBarVisible() {
  2582. document.documentElement.classList.add('ProgressBar');
  2583. const player = document.querySelector('.html5-video-player');
  2584. const video = document.querySelector('video[src]');
  2585. const timeDisplay = document.querySelector('.ytp-time-display');
  2586. const chaptersContainer = player && player.querySelector('.ytp-chapters-container');
  2587. const progressBarContainer = player && player.querySelector('.ytp-progress-bar-container');
  2588. if (!player || !video || !timeDisplay) { console.error("YTE: ProgressBar: A Required querySelector Not Found."); return; }
  2589. if (!progressBarContainer) { console.error("YTE: ProgressBar: Progress Bar Container Not Found."); return; }
  2590. const bar = document.createElement('div');
  2591. bar.id = 'ProgressBar-bar';
  2592. const progress = document.createElement('div');
  2593. progress.id = 'ProgressBar-progress';
  2594. const buffer = document.createElement('div');
  2595. buffer.id = 'ProgressBar-buffer';
  2596. const startDiv = document.createElement('div');
  2597. startDiv.id = 'ProgressBar-start';
  2598. const endDiv = document.createElement('div');
  2599. endDiv.id = 'ProgressBar-end';
  2600. player.appendChild(bar);
  2601. bar.appendChild(buffer);
  2602. bar.appendChild(progress);
  2603. player.appendChild(startDiv);
  2604. player.appendChild(endDiv);
  2605. progress.style.transform = 'scaleX(0)';
  2606. // live video check
  2607. let isLive = null;
  2608. const detectLive = () => {
  2609. const live = timeDisplay.classList.contains('ytp-live');
  2610. if (live !== isLive) {
  2611. isLive = live;
  2612. if (isLive) {
  2613. bar.classList.remove('active');
  2614. startDiv.classList.remove('active');
  2615. endDiv.classList.remove('active');
  2616. } else {
  2617. bar.classList.add('active');
  2618. startDiv.classList.add('active');
  2619. endDiv.classList.add('active');
  2620. }
  2621. }
  2622. };
  2623. detectLive();
  2624.  
  2625. function animateProgress() {
  2626. const fraction = video.currentTime / video.duration;
  2627. progress.style.transform = `scaleX(${fraction})`;
  2628. requestAnimationFrame(animateProgress);
  2629. }
  2630.  
  2631. requestAnimationFrame(animateProgress);
  2632.  
  2633. function renderBuffer() {
  2634. for (let i = video.buffered.length - 1; i >= 0; i--) {
  2635. if (video.currentTime < video.buffered.start(i)) continue;
  2636. buffer.style.transform = `scaleX(${video.buffered.end(i) / video.duration})`;
  2637. break;
  2638. }
  2639. }
  2640. video.addEventListener('progress', renderBuffer);
  2641. video.addEventListener('seeking', renderBuffer);
  2642.  
  2643. // chapters container
  2644. let cachedMaskImage = null;
  2645.  
  2646. function updateLayout() {
  2647. const initialWidth = progressBarContainer.getBoundingClientRect().width;
  2648.  
  2649. let attempts = 0;
  2650. const maxAttempts = 6;
  2651.  
  2652. const waitForSizeChange = new Promise((resolve) => {
  2653. const intervalId = setInterval(() => {
  2654. const currentWidth = progressBarContainer.getBoundingClientRect().width;
  2655.  
  2656. if (currentWidth !== initialWidth) { clearInterval(intervalId); resolve(); }
  2657. else if (++attempts >= maxAttempts) { clearInterval(intervalId); resolve(); }
  2658. }, 250);
  2659. });
  2660.  
  2661. waitForSizeChange.then(() => {
  2662. const playerRect = player.getBoundingClientRect();
  2663. const progressBarRect = progressBarContainer.getBoundingClientRect();
  2664. const progressBarWidth = progressBarRect.width;
  2665.  
  2666. bar.style.position = 'absolute';
  2667. bar.style.left = (progressBarRect.left - playerRect.left) + 'px';
  2668. bar.style.width = progressBarWidth + 'px';
  2669.  
  2670. if (chaptersContainer) {
  2671. const chapters = chaptersContainer.querySelectorAll('.ytp-chapter-hover-container');
  2672. if (chapters.length) {
  2673. const svgWidth = 100;
  2674. const svgHeight = 10;
  2675. let rects = '';
  2676.  
  2677. chapters.forEach((chapter) => {
  2678. const rect = chapter.getBoundingClientRect();
  2679. const startPx = rect.left - progressBarRect.left;
  2680. const chapterWidth = rect.width;
  2681.  
  2682. const startPerc = (startPx / progressBarWidth) * svgWidth;
  2683. const widthPerc = (chapterWidth / progressBarWidth) * svgWidth;
  2684.  
  2685. rects += `<rect x="${startPerc}" y="0" width="${widthPerc}" height="${svgHeight}" fill="white"/>`;
  2686. });
  2687.  
  2688. const svg = `<svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">${rects}</svg>`;
  2689. const encoded = encodeURIComponent(svg).replace(/%20/g, ' ');
  2690. cachedMaskImage = `url("data:image/svg+xml;utf8,${encoded}")`;
  2691.  
  2692. bar.style.maskImage = cachedMaskImage;
  2693. bar.style.webkitMaskImage = cachedMaskImage;
  2694. bar.style.maskRepeat = 'no-repeat';
  2695. bar.style.webkitMaskRepeat = 'no-repeat';
  2696. bar.style.maskSize = '100% 100%';
  2697. bar.style.webkitMaskSize = '100% 100%';
  2698. } else {
  2699. if (cachedMaskImage) {
  2700. bar.style.maskImage = '';
  2701. bar.style.webkitMaskImage = '';
  2702. cachedMaskImage = null;
  2703. }
  2704. }
  2705. }
  2706. });
  2707. }
  2708. // handle layout changes
  2709. document.addEventListener('yt-set-theater-mode-enabled', () => { updateLayout(); });
  2710. window.addEventListener('resize', () => { updateLayout(); });
  2711.  
  2712. // initialization
  2713. renderBuffer();
  2714. updateLayout();
  2715. }
  2716.  
  2717. // sidebar and left header links
  2718. function buttonsLeft() {
  2719. function openSidebar() {
  2720. const guideButton = document.querySelector('#guide-button button');
  2721. if (guideButton) {
  2722. guideButton.click();
  2723. }
  2724. }
  2725.  
  2726. // create sidebar button
  2727. function createButton(text, onClick) {
  2728. const btn = document.createElement('button');
  2729. btn.textContent = text;
  2730. btn.classList.add('buttons-left');
  2731. btn.addEventListener('click', (e) => {
  2732. e.preventDefault();
  2733. onClick();
  2734. });
  2735. return btn;
  2736. }
  2737.  
  2738. // create links
  2739. function createLink(text, url) {
  2740. const link = document.createElement('a');
  2741. link.textContent = text;
  2742. link.classList.add('buttons-left');
  2743. link.href = url;
  2744. return link;
  2745. }
  2746.  
  2747. const masthead = document.querySelector('ytd-masthead'); if (!masthead) return;
  2748. const container = masthead.querySelector('#container #start'); if (!container) return;
  2749.  
  2750. const isHideSidebarChecked = USER_CONFIG.mButtonDisplay;
  2751.  
  2752. if (container.querySelector('.buttons-left')) { return; }
  2753. // adding the buttons
  2754. const buttonsConfig = [
  2755. { type: 'button', text: USER_CONFIG.mButtonText, onClick: openSidebar },
  2756. { type: 'link', text: USER_CONFIG.buttonLeft1Text, url: USER_CONFIG.buttonLeft1Url },
  2757. { type: 'link', text: USER_CONFIG.buttonLeft2Text, url: USER_CONFIG.buttonLeft2Url },
  2758. { type: 'link', text: USER_CONFIG.buttonLeft3Text, url: USER_CONFIG.buttonLeft3Url },
  2759. { type: 'link', text: USER_CONFIG.buttonLeft4Text, url: USER_CONFIG.buttonLeft4Url },
  2760. { type: 'link', text: USER_CONFIG.buttonLeft5Text, url: USER_CONFIG.buttonLeft5Url },
  2761. { type: 'link', text: USER_CONFIG.buttonLeft6Text, url: USER_CONFIG.buttonLeft6Url },
  2762. { type: 'link', text: USER_CONFIG.buttonLeft7Text, url: USER_CONFIG.buttonLeft7Url },
  2763. ];
  2764. buttonsConfig.forEach(config => {
  2765. if (config.text && config.text.trim() !== '') {
  2766. let element;
  2767. if (config.type === 'button') {
  2768. if (isHideSidebarChecked) {
  2769. element = createButton(config.text, config.onClick);
  2770. if (config.text === DEFAULT_CONFIG.mButtonText) {
  2771. element.style.display = 'inline-block';
  2772. element.style.fontSize = '25px';
  2773. element.style.margin = '0';
  2774. element.style.padding = '0 0 5px 0';
  2775. element.style.transform = 'scaleX(1.25)';
  2776. }
  2777. }
  2778. } else if (config.type === 'link') {
  2779. element = createLink(config.text, config.url);
  2780. }
  2781. if (element) {
  2782. container.appendChild(element);
  2783. }
  2784. }
  2785. });
  2786. }
  2787.  
  2788. // color code videos on home
  2789. function ColorCodeVideos() {
  2790. console.log("ColorCodeVideos: Initializing and attaching event listener.");
  2791. // define age categories
  2792. const categories = {
  2793. live: ['watching'],
  2794. streamed: ['Streamed'],
  2795. upcoming: ['waiting', 'scheduled for'],
  2796. newly: ['1 day ago', 'hours ago', 'hour ago', 'minutes ago', 'minute ago', 'seconds ago', 'second ago'],
  2797. recent: ['1 week ago', '7 days ago', '6 days ago', '5 days ago', '4 days ago', '3 days ago', '2 days ago'],
  2798. 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'],
  2799. old: ['years ago', '1 year ago', '12 months ago', '11 months ago', '10 months ago', '9 months ago', '8 months ago', '7 months ago']
  2800. };
  2801. function processVideos() {
  2802. console.log("ColorCodeVideos processVideos: Called.");
  2803. document.querySelectorAll('[class*="ytd-video-meta-block"]').forEach(el => {
  2804. const textContent = el.textContent.trim().toLowerCase();
  2805. for (const [className, ages] of Object.entries(categories)) {
  2806. if (ages.some(age => textContent.includes(age.toLowerCase()))) {
  2807. const videoContainer = el.closest('ytd-rich-item-renderer');
  2808. if (videoContainer && !videoContainer.classList.contains(`yte-style-${className}-video`)) {
  2809. videoContainer.classList.add(`yte-style-${className}-video`);
  2810. }
  2811. }
  2812. }
  2813. });
  2814. document.querySelectorAll('span.ytd-video-meta-block').forEach(el => {
  2815. const text = el.textContent;
  2816. const videoContainer = el.closest('ytd-rich-item-renderer');
  2817. if (!videoContainer) return;
  2818. if (/Scheduled for/i.test(text) && !videoContainer.classList.contains('yte-style-upcoming-video')) {
  2819. videoContainer.classList.add('yte-style-upcoming-video');
  2820. }
  2821. if (/Streamed/i.test(text) && !el.querySelector('.yte-style-streamed-text')) {
  2822. el.childNodes.forEach(node => {
  2823. if (node.nodeType === Node.TEXT_NODE && /Streamed/i.test(node.nodeValue)) {
  2824. const span = document.createElement('span');
  2825. span.className = 'yte-style-streamed-text';
  2826. span.textContent = node.nodeValue.match(/Streamed/i)[0];
  2827. const rest = document.createTextNode(node.nodeValue.replace(/Streamed/i, ''));
  2828. el.replaceChild(rest, node);
  2829. el.insertBefore(span, rest);
  2830. }
  2831. });
  2832. }
  2833. });
  2834. }
  2835. processVideos();
  2836.  
  2837. document.addEventListener('yt-service-request-sent', () => {
  2838. setTimeout(() => {
  2839. processVideos();
  2840. }, 2000);
  2841. });
  2842. }
  2843.  
  2844. // initiate the script
  2845. let lastVideoURL = null;
  2846.  
  2847. async function initializeTranscript(currentVideoURL) {
  2848. if (USER_CONFIG.preventBackgroundExecution) { await ChromeUserWait(); }
  2849. buttonsLeft();
  2850.  
  2851. const isVideoPage = /^https:\/\/.*\.youtube\.com\/watch\?v=/.test(currentVideoURL);
  2852. if (isVideoPage) {
  2853. if (USER_CONFIG.autoOpenChapters) { openChapters(); }
  2854. if (USER_CONFIG.DisplayRemainingTime) { RemainingTime(); }
  2855. if (USER_CONFIG.ProgressBar) { ProgressBarCSS(); keepProgressBarVisible(); }
  2856.  
  2857. let transcriptLoaded = false;
  2858. try { await preLoadTranscript(); transcriptLoaded = true; }
  2859. catch (error) { setTimeout(() => { addSettingsButton(); }, 3000); }
  2860. if (transcriptLoaded) { addButton(); }
  2861. //console.log("YTE: YouTube Transcript Exporter Initialized: On Video Page.");
  2862. } else {
  2863. addSettingsButton();
  2864. //console.log("YTE: YouTube Transcript Exporter Initialized: On Normal Page.");
  2865. if (window.location.href !== "https://www.youtube.com/") { return; }
  2866. if (USER_CONFIG.colorCodeVideosEnabled) { ColorCodeVideos(); }
  2867. }
  2868. }
  2869.  
  2870. // YouTube navigation handler
  2871. function handleYouTubeNavigation() {
  2872. //console.log("YTE: Event Listner Arrived");
  2873. const currentVideoURL = window.location.href;
  2874. if (currentVideoURL !== lastVideoURL) {
  2875. lastVideoURL = currentVideoURL;
  2876. //console.log("YTE: Only One Survived");
  2877. loadCSSsettings();
  2878. customCSS();
  2879. setTimeout(() => { initializeTranscript(currentVideoURL); }, 500);
  2880. }
  2881. }
  2882.  
  2883. // reset
  2884. function handleYTNavigation() {
  2885. document.querySelectorAll(
  2886. '.button-wrapper, .remaining-time-container, #yt-transcript-settings-modal, .sub-panel-overlay, #ProgressBar-bar, #ProgressBar-start, #ProgressBar-end, ProgressBar-progress, ProgressBar-buffer'
  2887. ).forEach(el => el.remove());
  2888. }
  2889.  
  2890. // function due to the inadequacy of Chrome
  2891. async function ChromeUserWait() {
  2892. if (document.visibilityState !== 'visible') {
  2893. //console.log("YTE: Waiting for this tab to become visible...");
  2894. return new Promise((resolve) => {
  2895. document.addEventListener('visibilitychange', function onVisibilityChange() {
  2896. if (document.visibilityState === 'visible') {
  2897. document.removeEventListener('visibilitychange', onVisibilityChange);
  2898. resolve();
  2899. }
  2900. });
  2901. });
  2902. }
  2903. }
  2904.  
  2905. // event listeners
  2906. document.addEventListener('yt-navigate-start', handleYTNavigation); // reset
  2907. document.addEventListener('yt-navigate-finish', handleYouTubeNavigation); // default
  2908. document.addEventListener('yt-page-data-updated', handleYouTubeNavigation); // backup
  2909. document.addEventListener('yt-page-data-fetched', handleYouTubeNavigation); // redundancy
  2910. //document.addEventListener('yt-player-updated', handleYouTubeNavigation);
  2911. //document.addEventListener('yt-update-title', handleYouTubeNavigation);
  2912. //document.addEventListener('yt-page-type-changed', handleYouTubeNavigation);
  2913. document.addEventListener('yt-service-request-completed', handleYouTubeNavigation); // for chrome
  2914. })();