Jupiter's Discord Chat Saver

A free, full-featured chat saver for Discord.

  1. // ==UserScript==
  2. // @name Jupiter's Discord Chat Saver
  3. // @description A free, full-featured chat saver for Discord.
  4. // @namespace Violentmonkey Scripts
  5. // @match https://discord.com/channels/*
  6. // @grant none
  7. // @version 1.4
  8. // @author Jupiter Liar
  9. // @description 01/04/2024, 11:40 AM
  10. // @license CC BY-SA
  11. // @grant GM_download
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.9/beautify-html.min.js
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. const logInitial = 'Here is a record of the download process.\n' +
  20. 'Certain kinds of errors cannot be overcome, or be detected by this script.\n' +
  21. 'Files which failed to download can be downloaded manually.\n';
  22. let outputLog = logInitial;
  23.  
  24.  
  25.  
  26. function resetLog() {
  27. outputLog = logInitial;
  28. }
  29.  
  30. let showMinorLogs = false;
  31. let showMajorLogs = true;
  32.  
  33.  
  34. // Declare variables for settings
  35. let attachmentsEnabled = true;
  36. let audioEnabled = true;
  37. let fetchFullSize = true;
  38. let buttonGenerationLocation = 'show-button-corner'; // Default to 'show-button-corner'
  39. let stopScrollingVar = false;
  40.  
  41. // Load settings from localStorage and apply them
  42. const loadSettings = () => {
  43. const savedSettings = JSON.parse(localStorage.getItem('chatCopySettings'));
  44. console.log(savedSettings);
  45.  
  46. if (savedSettings) {
  47. // Apply saved settings to variables
  48. attachmentsEnabled = savedSettings.enableAttachments;
  49. audioEnabled = savedSettings.enableAudio;
  50. fetchFullSize = savedSettings.enableFullSizeImages;
  51. buttonGenerationLocation = savedSettings.buttonLocation;
  52. showMinorLogs = savedSettings.enableLogMinor;
  53. } else {
  54. // Set default settings if no saved data exists
  55. attachmentsEnabled = true;
  56. audioEnabled = true;
  57. fetchFullSize = true;
  58. buttonGenerationLocation = 'show-button-corner';
  59. showMinorLogs = false;
  60. }
  61. };
  62.  
  63. // Call loadSettings to initialize values when the script loads
  64. loadSettings();
  65.  
  66.  
  67. const instructions = `
  68. <h1><span>How does this thing work?</span></h1>
  69.  
  70. <p>This may be a little different from other chat savers out there, so pay attention.</p>
  71.  
  72. <p>By now you've clicked the blue <strong>Copy Chat</strong> button, and have opened the <strong>Copy</strong> popup. This popup can be dragged and resized like a normal window. At the bottom, you'll notice it says "Recording...". It will always say this. Because as long as the popup is open, it will record what is in the current chat.</p>
  73.  
  74. <p>But it can only record one chat at a time. If you navigate away, the popup will close, and the copy process will end.</p>
  75.  
  76. <h2>How much of the chat will I save?</h2>
  77.  
  78. <p>As much as you load. Simply scroll through the chat, allowing each portion to load as you go. Every time a portion loads, the <strong>Copy</strong> popup will record it. Discord unloads portions of the chat as you go, but the <strong>Copy</strong> popup keeps them.</p>
  79.  
  80. <p>When you've loaded all the portions of the chat that you want to save, then you can press the Save button. The script will go through the chat that it has copied, fetching resources as it goes.</p>
  81.  
  82. <p><strong>NOTE: Some resources cannot be fetched. Do an internet search for "CORS", and you will understand why.</strong></p>
  83.  
  84. <p>Once everything that can be fetched has been fetched, your browser will download the chat as a ZIP. The zip will contain:</p>
  85.  
  86. <ul>
  87. <li>an HTML file</li>
  88. <li>the images in the chat, including any animations</li>
  89. <li>audio (if enabled)</li>
  90. <li>the full-size versions of images (if enabled)</li>
  91. <li>the styles that determine how the chat looks</li>
  92. <li>attachments (if enabled)</li>
  93. <li>a log of the download process</li>
  94. </ul>
  95.  
  96. <p>As said above, some files may not download, for various reasons. After downloading, check to make sure you got everything you wanted.</p>
  97.  
  98. <p>Clicking the <strong>Settings</strong> gear will give you the options to enable or disable downloading certain kinds of media, as well as an option to turn on extra logs in the dev console.</p>
  99.  
  100. <h2>How does <strong>Autoscroll</strong> work?</h2>
  101.  
  102. <p><strong>Autoscroll</strong> scrolls the chat so you don't have to. It can scroll until it gets to a specific date, or if you'd prefer to just get everything, it can scroll until it can't go any further.</p>
  103.  
  104. <p>To use it, first click the <strong>Autoscroll</strong> button to open up its options. Click <strong>Return</strong> at any time to leave the options. If <strong>First/Last</strong> is selected, the chat will scroll until it can't go any further. If <strong>Date</strong> is selected, it will scroll until it reaches a given date.</p>
  105.  
  106. <p>To begin scrolling, press one of the two <strong>arrows</strong>, and the chat will scroll in the arrow's direction. Press <strong>Stop</strong> to stop, naturally. Pressing <strong>Return</strong> will also stop the scrolling process.</p>
  107.  
  108. <p><strong>Note:</strong> If you set a date, and the arrow you click would take you further from that date, nothing will happen.</p>
  109.  
  110. <p><strong>Further note:</strong> Due to different time zones, the date feature may be imprecise. Check the <strong>Copy<strong> popup to make sure you have captured everything you want.</p>
  111.  
  112. <h2>How many messages can I save for free?</h2>
  113.  
  114. <p><strong>All</strong> of them.</p>
  115.  
  116. <h2>Is there a premium version with more features?</h2>
  117.  
  118. <p>No. All the features I could make, I included in this version, for free.</p>
  119.  
  120. <h2>Can I support your work with a donation?</h2>
  121.  
  122. <p>You can reach my Buy Me a Coffee page through my <a href="https://linktr.ee/jupiterliar">Linktree.</a></p>
  123. `;
  124.  
  125. function logMinor(message) {
  126. if (showMinorLogs) {
  127. console.log(message); // No need to record these.
  128. }
  129. }
  130.  
  131. function logMajor(message) {
  132. if (showMajorLogs) {
  133. console.log(message);
  134. outputLog += `\n${message}`;
  135. onScreenLogging('log', message);
  136. }
  137. }
  138.  
  139. function logError(message) {
  140. const errorMessage = 'ERROR: ' + message;
  141. console.error(errorMessage);
  142. outputLog += `\n\n${errorMessage}\n`;
  143. onScreenLogging('error', errorMessage);
  144. }
  145.  
  146. // Add styles
  147. const style = document.createElement('style');
  148. style.id = "discord-chat-saver-styles";
  149. style.textContent = `
  150. :root {
  151. --dcs-box-shadow: .1em .1em .2em inset hsla(0, 0%, 100%, .35),
  152. -.1em -.1em .2em inset hsla(0, 0%, 0%, .5);
  153. --dcs-opposite-box-shadow: .1em .1em .1em inset hsla(0, 0%, 0%, .25),
  154. -.1em -.1em .1em inset hsla(0, 0%, 100%, .175);
  155. /* --dcs-blue: hsl(235, 85%, 65%); */
  156. --dcs-hs: 235, 100%;
  157. --dcs-blue: hsl(var(--dcs-hs), 60%);
  158. --dcs-drop-shadow: drop-shadow(.1em .1em .1em black);
  159. --dcs-yellow: hsl(60, 100%, 35%);
  160. --dcs-box-shadow-2: .2em .2em .4em inset hsla(0, 0%, 100%, .35),
  161. -.2em -.2em .4em inset hsla(0, 0%, 0%, .5);
  162. --scale-factor: 1;
  163. }
  164.  
  165. @media (max-height: 600px) {
  166. :root {
  167. --scale-factor: 0.75;
  168. }
  169. }
  170.  
  171. #copy-button-outer {
  172. margin: .5em 1.5em auto auto;
  173. position: sticky;
  174. width: fit-content;
  175. height: 0;
  176. z-index: 1;
  177. font-weight: 700;
  178. }
  179.  
  180. #copy-button-inner {
  181. padding: 1em;
  182. background: var(--dcs-blue);
  183. border-radius: 50%;
  184. position: sticky;
  185. color: white;
  186. filter: var(--dcs-drop-shadow);
  187. box-shadow: var(--dcs-box-shadow);
  188. cursor: pointer;
  189. aspect-ratio: 1;
  190. display: flex;
  191. align-items: center;
  192. }
  193.  
  194. #copy-button-inner span {
  195. filter: inherit;
  196. display: block;
  197. text-align: center;
  198. }
  199.  
  200. #chat-copy-outer {
  201. position: fixed;
  202. z-index: 101;
  203. background: var(--bg-overlay-chat, var(--background-primary));
  204. border-radius: 1.5em;
  205. width: 33.3vw;
  206. max-height: calc(100vh - 8em);
  207. overflow: hidden;
  208. display: flex;
  209. flex-direction: column;
  210. box-shadow: var(--dcs-box-shadow);
  211. background: #DDD;
  212. filter: var(--dcs-drop-shadow);
  213. font-weight: 500;
  214. color: var(--text-normal);
  215. min-height: 17em;
  216. }
  217.  
  218. #chat-copy-outer .drag-bar {
  219. cursor: move;
  220. background: var(--dcs-blue);
  221. padding: 0.5em;
  222. border-radius: 1em 1em 0 0;
  223. display: flex;
  224. align-items: center;
  225. justify-content: center;
  226. position: relative;
  227. box-shadow: var(--dcs-box-shadow);
  228. }
  229.  
  230. #chat-copy-outer .drag-bar span {
  231. font-size: 16px;
  232. font-weight: bold;
  233. color: white;
  234. z-index: 1; /* Ensure the text is on top of the bars */
  235. display: flex;
  236. align-items: center;
  237. width: 100%;
  238. filter: var(--dcs-drop-shadow);
  239. font-family: Arial;
  240. }
  241.  
  242. /* Left and right bars before and after the text using pseudo-elements */
  243. #chat-copy-outer .drag-bar span::before,
  244. #chat-copy-outer .drag-bar span::after {
  245. content: '';
  246. flex-grow: 1;
  247. background-color: #DDD;
  248. margin: 0 0.5em;
  249. min-height: 1px;
  250. box-shadow: 0 .25em #DDD, 0 -.25em #DDD;
  251. }
  252.  
  253. /* Left bar (before the text) */
  254. #chat-copy-outer .drag-bar span::before {
  255. flex-basis: 1em;
  256. }
  257.  
  258. /* Right bar (after the text) */
  259. #chat-copy-outer .drag-bar span::after {
  260. margin-right: 2em;
  261. }
  262.  
  263. #chat-copy-outer .close-box {
  264. position: absolute;
  265. /* top: 0.5em; */
  266. right: 0.5em;
  267. cursor: pointer;
  268. /* color: red; */
  269. font-weight: bold;
  270. z-index: 5;
  271. font-size: 2em;
  272. }
  273.  
  274. #chat-copy-inner {
  275. flex: 1;
  276. overflow: auto;
  277. padding: 1em;
  278. /* border: 1px solid rgba(255, 255, 255, 0.2); */
  279. margin: 0 .2em;
  280. background: var(--bg-overlay-chat, var(--background-primary));
  281. box-shadow: var(--dcs-opposite-box-shadow);
  282. }
  283.  
  284. #chat-copy-outer .resize-handle {
  285. width: 1.5em;
  286. height: 1.5em;
  287. position: absolute;
  288. right: 0;
  289. bottom: 0;
  290. cursor: se-resize;
  291. background: var(--dcs-blue);
  292. border-radius: 0 0 1em 0;
  293. z-index: 1;
  294. box-shadow: var(--dcs-box-shadow);
  295. overflow: hidden;
  296. }
  297.  
  298. #chat-copy-outer .resize-handle::before {
  299. z-index: 1;
  300. content: '';
  301. position: absolute;
  302. width: 100%;
  303. height: 100%;
  304. box-shadow: var(--dcs-box-shadow), inset 2px 2px var(--dcs-blue),
  305. inset -3px -3px var(--dcs-blue);
  306. }
  307.  
  308. /* Add diagonal lines to the resize handle */
  309. #chat-copy-outer .resize-handle::after {
  310. content: '';
  311. flex-grow: 1;
  312. background-color: #DDD;
  313. min-height: 1px;
  314. box-shadow: 0 .3em #DDD, 0 -.3em #DDD;
  315. width: 200%;
  316. position: absolute;
  317. transform: rotate(-45deg);
  318. filter: var(--dcs-drop-shadow);
  319. left: -50%;
  320. top: 20%;
  321. }
  322.  
  323. #chat-copy-inner li::marker {
  324. content: '';
  325. }
  326.  
  327. /* Recording bar styling */
  328. .recording-bar {
  329. background-color: #444;
  330. color: #fff;
  331. text-align: center;
  332. padding: 5px;
  333. font-size: 14px;
  334. font-family: Arial, sans-serif;
  335. position: relative;
  336. padding-right: calc(5px + 1em);
  337. box-shadow: var(--dcs-box-shadow);
  338. }
  339.  
  340. .recording-bar span::after {
  341. content: '...';
  342. position: absolute;
  343. animation: recording-dots 1.5s steps(4, end) infinite;
  344. }
  345.  
  346. /* Animation for the dots */
  347. @keyframes recording-dots {
  348. 0% {
  349. content: '';
  350. }
  351. 33% {
  352. content: '.';
  353. }
  354. 66% {
  355. content: '..';
  356. }
  357. 100% {
  358. content: '...';
  359. }
  360. }
  361.  
  362. .chat-copy-big-button {
  363. position: absolute;
  364. border-radius: 50%;
  365. z-index: 1;
  366. color: white;
  367. padding: 0.8em;
  368. aspect-ratio: 1;
  369. display: flex;
  370. align-items: center;
  371. justify-content: center;
  372. box-shadow: var(--dcs-box-shadow);
  373. filter: drop-shadow(1px 1px 1px black);
  374. cursor: pointer;
  375. scale: var(--scale-factor);
  376. }
  377.  
  378. .chat-copy-big-button span {
  379. filter: var(--dcs-drop-shadow);
  380. font-weight: 700;
  381. }
  382.  
  383. #chat-copy-save-button {
  384.  
  385. background: hsl(150, 100%, 35%);
  386.  
  387. right: calc(2em * var(--scale-factor));
  388. bottom: calc(2em * var(--scale-factor));
  389. font-family: Arial;
  390.  
  391. }
  392.  
  393. #chat-copy-config-button {
  394. top: calc(3em * var(--scale-factor));
  395. left: calc(1em * var(--scale-factor));
  396. background: hsl(00, 100%, 50%);
  397. }
  398.  
  399. #chat-copy-config-button span:not(.save-size-span) {
  400. font-size: 2.5em;
  401. position: absolute;
  402. }
  403.  
  404. #chat-copy-instruction-button {
  405. top: calc(3em * var(--scale-factor));
  406. right: calc(2em * var(--scale-factor));
  407. background: hsl(330, 100%, 50%);
  408. }
  409.  
  410. #chat-copy-instruction-button span:not(.save-size-span) {
  411. font-size: 1.75em;
  412. position: absolute;
  413. }
  414.  
  415.  
  416. #chat-copy-instruction-button span {
  417. filter: brightness(8) var(--dcs-drop-shadow);
  418. }
  419.  
  420. .save-size-span {
  421. opacity: 0;
  422. }
  423.  
  424. div#progress-log-overlay {
  425. pointer-events: none;
  426. height: 32em;
  427. max-height: 32em;
  428. width: 100%;
  429. position: absolute;
  430. z-index: 9;
  431. margin-bottom: 8em;
  432. bottom: 0;
  433. padding: 1em 2em;
  434. box-sizing: border-box;
  435. mask-image: linear-gradient(to top, rgba(0, 0, 0, 1) 75%, rgba(0, 0, 0, 0) 100%);
  436. display: flex;
  437. flex-direction: column;
  438. justify-content: flex-end;
  439. filter: drop-shadow(.5em 0 0 white) drop-shadow(-.5em 0 0 white);
  440. }
  441.  
  442. div#progress-log-overlay span.line-outer {
  443. text-align: center;
  444. line-height: normal;
  445. font-size: .95em;
  446. }
  447.  
  448. div#progress-log-overlay span.line-inner {
  449. background: white;
  450. color: black;
  451. animation: fadeOut 10s forwards;
  452. word-break: break-word;
  453. font-size: inherit;
  454. }
  455.  
  456. div#progress-log-overlay span.line-inner.error {
  457. color: red;
  458. }
  459.  
  460. @keyframes fadeOut {
  461. 0% {
  462. opacity: 1;
  463. }
  464. 25% {
  465. opacity: 1;
  466. }
  467. 100% {
  468. opacity: 0;
  469. }
  470. }
  471.  
  472. #chat-copy-settings-outer, #chat-copy-instruction-outer {
  473. background: var(--bg-overlay-chat, var(--background-primary));
  474. box-shadow: var(--dcs-opposite-box-shadow);
  475. margin: 0 .2em;
  476. padding: 1em;
  477. overflow: auto;
  478. flex: 1;
  479. max-height: calc(100vh - 16em);
  480. z-index: 2;
  481. min-height: 12em;
  482. display: flex;
  483. }
  484.  
  485. #chat-copy-settings {
  486.  
  487.  
  488.  
  489. /* border: 1px solid rgba(255, 255, 255, 0.2); */
  490.  
  491.  
  492. width: fit-content;
  493. margin: auto;
  494. }
  495.  
  496. #chat-copy-settings .wrapper {
  497. margin-left: 1em;
  498. }
  499.  
  500. #chat-copy-settings .wrapper .wrapper-inner {
  501. display: inline;
  502. }
  503.  
  504. #chat-copy-settings .wrapper .wrapper-inner * {
  505. line-height: normal;
  506. }
  507.  
  508. #chat-copy-settings h2 {
  509. font-size: 1.5em;
  510. font-weight: bold;
  511. margin-bottom: 0.65em;
  512. position: relative;
  513. display: flex;
  514. align-items: center;
  515. justify-content: center;
  516. color: white;
  517. }
  518.  
  519. #chat-copy-settings h2 span {
  520. margin-right: auto;
  521. filter: var(--dcs-drop-shadow);
  522. }
  523.  
  524. #chat-copy-settings h2::before {
  525. content: "";
  526. position: absolute;
  527. background: linear-gradient(to right, hsl(00, 100%, 40%), transparent);
  528. z-index: -1;
  529. width: 100%;
  530. height: 100%;
  531. --bottom-extra: 0.1em;
  532. padding: .25em 0.5em calc(0.25em + var(--bottom-extra));
  533. margin-top: var(--bottom-extra);
  534. }
  535.  
  536. #chat-copy-settings h3 {
  537. margin-bottom: .5em;
  538. font-weight: 600;
  539. font-size: 1.1em;
  540. }
  541.  
  542. #chat-copy-settings p {
  543. margin: .5em 0;
  544. max-width: 20em;
  545. }
  546.  
  547. #chat-copy-settings .divider {
  548. height: 1px;
  549. background: black;
  550. margin: 1em 0;
  551. }
  552.  
  553. #chat-copy-settings #close-config-button {
  554. display: block;
  555. margin-top: 1em;
  556. margin-left: auto;
  557. font-size: 1rem;
  558. font-weight: 600;
  559. padding: .5em 1em;
  560. background: var(--dcs-blue);
  561. color: white;
  562. box-shadow: var(--dcs-box-shadow);
  563. border-radius: 1em;
  564. }
  565.  
  566. #chat-copy-settings #close-config-button span {
  567. filter: var(--dcs-drop-shadow);
  568. }
  569.  
  570. #clear-settings-button {
  571. display: none;
  572. }
  573.  
  574. #direct-messages-SBB, #servers-footer-SBB, #settings-SBB {
  575. background: var(--dcs-blue);
  576. color: white;
  577. text-align: center;
  578. box-shadow: var(--dcs-box-shadow);
  579.  
  580. }
  581.  
  582. #direct-messages-SBB span, #servers-footer-SBB span, #settings-SBB span {
  583. filter: var(--dcs-drop-shadow);
  584.  
  585. }
  586.  
  587. #direct-messages-SBB {
  588. width: fit-content;
  589. line-height: normal;
  590. padding: .5em 1em;
  591. box-shadow: var(--dcs-box-shadow-2);
  592. margin-top: .25em;
  593. border-radius: 2em;
  594. }
  595.  
  596. #servers-footer-SBB {
  597. /* margin-left: 12px; */
  598. padding: .5em;
  599. border-radius: 1em;
  600. margin-right: 1px;
  601. margin-bottom: .33em;
  602. box-shadow: var(--dcs-box-shadow-2);
  603. }
  604.  
  605. #servers-footer-SBB span {
  606.  
  607. }
  608.  
  609. #settings-SBB {
  610. line-height: normal;
  611. width: fit-content;
  612. padding: .5em 1em;
  613. border-radius: 2em;
  614. translate: -.5em 0;
  615.  
  616. }
  617.  
  618.  
  619. @keyframes moveAndFadeOut {
  620. 0% {
  621. transform: translate(0, 0);
  622. opacity: 1;
  623. }
  624. 25% {
  625. opacity: 0.5;
  626. }
  627. 50% {
  628. opacity: 0;
  629. }
  630. 100% {
  631. transform: translate(-100vw, 100vh); /* Moving to bottom left off-screen */
  632. opacity: 0;
  633. visibility: hidden; /* Hides the element completely after animation */
  634. }
  635. }
  636.  
  637. #copy-button-outer.move-fade-out {
  638. animation: moveAndFadeOut 2s ease-out forwards; /* Adjust duration as needed */
  639. }
  640.  
  641. #chat-copy-instruction {
  642. margin-bottom: 1em;
  643. height: fit-content;
  644. }
  645.  
  646. #chat-copy-instruction h1 {
  647. font-size: 1.5em;
  648. font-weight: bold;
  649. line-height: 1.15;
  650. display: flex;
  651. justify-content: center;
  652. align-items: center;
  653. position: relative;
  654. color: white;
  655. margin-bottom: 0.65em;
  656. }
  657.  
  658. #chat-copy-instruction h1::before {
  659. content: '';
  660. z-index: -1;
  661. width: 100%;
  662. height: 100%;
  663. --bottom-extra: 0.1em;
  664. padding: .25em 0.5em calc(0.25em + var(--bottom-extra));
  665. margin-top: var(--bottom-extra);
  666. background: linear-gradient(to right, hsl(330, 100%, 40%), hsla(330, 100%, 40%, 0.5));
  667. position: absolute;
  668. }
  669.  
  670. #chat-copy-instruction h1 span {
  671. margin-right: auto;
  672. filter: var(--dcs-drop-shadow);
  673. }
  674.  
  675. #chat-copy-instruction h2 {
  676. font-size: 1.2em;
  677. font-weight: bold;
  678. }
  679.  
  680. #chat-copy-instruction ul {
  681. list-style-type: disc; /* Ensures bullets are shown */
  682. padding-left: 20px; /* Optional: Adds indentation to the list */
  683. }
  684.  
  685. #chat-copy-instruction p {
  686. line-height: 1.15em;
  687. }
  688.  
  689. #chat-copy-instruction #close-instruction-button {
  690. position: absolute;
  691. display: block;
  692. margin-top: 1em;
  693. margin-left: auto;
  694. font-size: 1rem;
  695. font-weight: 600;
  696. padding: .5em 1em;
  697. background: var(--dcs-blue);
  698. color: white;
  699. box-shadow: var(--dcs-box-shadow);
  700. border-radius: 1em;
  701. bottom: 2em;
  702. right: 2em;
  703. }
  704.  
  705. #chat-copy-instruction #close-instruction-button span {
  706. filter: var(--dcs-drop-shadow);
  707. }
  708.  
  709. #chat-copy-instruction strong {
  710. font-weight: bold;
  711. }
  712.  
  713. #chat-copy-instruction a {
  714. font-weight: bold;
  715. }
  716.  
  717. #autoscroll-button {
  718. aspect-ratio: unset;
  719. background: var(--dcs-yellow);
  720. }
  721.  
  722. #autoscroll-div {
  723. position: absolute;
  724. bottom: 0;
  725. right: calc(2em * var(--scale-factor));
  726. margin-bottom: calc(5em * var(--scale-factor) + 24px);
  727. scale: var(--scale-factor);
  728. transform-origin: bottom center;
  729. }
  730.  
  731. #autoscroll-div > * {
  732. position: relative;
  733. }
  734.  
  735. #autoscroll-option-now radio, #autoscroll-option-date radio {
  736. margin-left: 0;
  737. }
  738.  
  739. #autoscroll-option-now, #autoscroll-option-date,
  740. #autoscroll-stop-button, #autoscroll-return-button {
  741. background: var(--dcs-yellow);
  742. padding: .25em 0.5em;
  743. border-radius: .5em;
  744. box-shadow: var(--dcs-box-shadow);
  745. }
  746.  
  747. #autoscroll-control {
  748. display: flex;
  749. flex-direction: column;
  750. align-items: center;
  751. gap: 10px;
  752. }
  753.  
  754. #autoscroll-control .arrow, #autoscroll-option-date, #autoscroll-option-now,
  755. #autoscroll-option-date *, #autoscroll-option-now *,
  756. #autoscroll-stop-button, #autoscroll-return-button {
  757. cursor: pointer;
  758. color: black;
  759. }
  760.  
  761. #autoscroll-option-now, #autoscroll-option-date {
  762. gap: .33em;
  763. display: flex;
  764. align-items: center;
  765. }
  766.  
  767. #date-div {
  768. color: black;
  769. }
  770.  
  771. #autoscroll-option-now, #autoscroll-option-now label {
  772. display: flex;
  773. }
  774.  
  775. #autoscroll-option-date input, #autoscroll-option-now input {
  776. margin: 0;
  777. }
  778.  
  779. #autoscroll-control .arrow {
  780. filter: var(--dcs-drop-shadow);
  781. height: 4em;
  782. width: 3em;
  783. }
  784.  
  785. #autoscroll-control .arrow * {
  786. background: var(--dcs-yellow);
  787. height: 100%;
  788. width: 100%;
  789. position: absolute;
  790. }
  791.  
  792. #up-arrow-level-2 {
  793. clip-path: polygon(50% 12.5%, 16.667% 50%, 33.333% 50%, 33.333% 87.5%, 50% 87.5%, 66.667% 87.5%, 66.667% 50%, 83.333% 50%, 50% 12.5%);
  794. }
  795.  
  796. #up-arrow-level-4A, #up-arrow-level-4A-copy {
  797. clip-path: polygon(0 0, 50% 0, 50% 12.5%, 16.667% 50%, 33.333% 50%, 33.333% 87.5%, 50% 87.5%, 50% 100%, 0 100%, 0 0);
  798. }
  799.  
  800. #up-arrow-level-4B, #up-arrow-level-4B-copy {
  801. clip-path: polygon(100% 100%, 50% 100%, 50% 87.5%, 66.667% 87.5%, 66.667% 50%, 83.333% 50%, 50% 12.5%, 50% 0, 100% 0, 100% 100%);
  802. }
  803.  
  804. #down-arrow-level-2 {
  805. clip-path: polygon(50% 87.5%, 16.667% 50%, 33.333% 50%, 33.333% 12.5%, 50% 12.5%, 66.667% 12.5%, 66.667% 50%, 83.333% 50%, 50% 87.5%);
  806. }
  807.  
  808. #down-arrow-level-2, #up-arrow-level-2 {
  809. --numberval: .1em;
  810. --negnum: calc(var(--numberval) * -1);
  811. --dubnum: calc(var(--numberval)* 2);
  812. --factor: 1.5;
  813. --hilight: calc(0.35 * var(--factor));
  814. --shadow: calc(0.25 * var(--factor));
  815. }
  816.  
  817. #down-arrow-level-4A, #down-arrow-level-4A-copy {
  818. clip-path: polygon(0 100%, 50% 100%, 50% 87.5%, 16.667% 50%, 33.333% 50%, 33.333% 12.5%, 50% 12.5%, 50% 0, 0 0, 0 100%);
  819. }
  820.  
  821. #down-arrow-level-4B, #down-arrow-level-4B-copy {
  822. clip-path: polygon(100% 0, 50% 0, 50% 12.5%, 66.667% 12.5%, 66.667% 50%, 83.333% 50%, 50% 87.5%, 50% 100%, 100% 100%, 100% 0);
  823. }
  824.  
  825. #up-arrow-level-3, #down-arrow-level-3 {
  826. background: none !important;
  827. filter: drop-shadow(var(--numberval) var(--numberval) var(--numberval) hsla(0, 0%, 100%, var(--hilight)));
  828. }
  829.  
  830. #up-arrow-level-3-copy, #down-arrow-level-3-copy {
  831. background: none !important;
  832.  
  833. filter: drop-shadow(var(--negnum) var(--negnum) var(--numberval) hsla(0, 0%, 0%, var(--shadow)));
  834. }
  835.  
  836. #date-div {
  837. display: grid;
  838. grid-template-columns: auto auto auto;
  839. grid-template-rows: auto auto;
  840. gap: 0.25em;
  841. background: white;
  842. }
  843.  
  844. #date-div * {
  845. text-align: center;
  846. font-size: 0.75em;
  847. max-width: 9.5em;
  848. }
  849.  
  850. #date-div span {
  851. text-align: center;
  852. width: fit-content;
  853. margin: auto;
  854. }
  855.  
  856. #date-div input {
  857. width: 2.5em;
  858. }
  859.  
  860. #date-div input:first-of-type {
  861. width: 4em;
  862. }
  863.  
  864. #date-div #time-zone-warning {
  865. grid-column: span 3;
  866. }
  867.  
  868. #autoscroll-stop-button {
  869.  
  870. }
  871.  
  872. @keyframes blink {
  873. 0% {
  874. background-color: var(--dcs-yellow);
  875. }
  876. 50% {
  877. background-color: yellow; /* Change this to your desired color */
  878. }
  879. 100% {
  880. background-color: var(--dcs-yellow);
  881. }
  882. }
  883.  
  884. .blinking-arrow-button {
  885. animation: blink 1s infinite; /* 1s duration and infinite loop */
  886. }
  887.  
  888. #current-date {
  889. font-size: 0.8em;
  890. display: flex;
  891. flex-direction: column;
  892. background: white;
  893. padding: 0.5em;
  894. text-align: center;
  895. }
  896.  
  897. #time-zone-warning {
  898. max-width: 9.5em;
  899. }
  900.  
  901. `;
  902. document.head.appendChild(style);
  903.  
  904. // Variables
  905. const foundChats = new WeakSet();
  906. let debounceTimeout;
  907.  
  908. // Drag-and-Drop Functionality
  909. function makeDraggable(element) {
  910. const dragBar = element.querySelector('.drag-bar');
  911. let isDragging = false,
  912. offsetX, offsetY;
  913.  
  914. dragBar.addEventListener('mousedown', (e) => {
  915. isDragging = true;
  916. offsetX = e.clientX - element.offsetLeft;
  917. offsetY = e.clientY - element.offsetTop;
  918. document.addEventListener('mousemove', onDrag);
  919. document.addEventListener('mouseup', stopDrag);
  920. });
  921.  
  922. function onDrag(e) {
  923. if (!isDragging) return;
  924.  
  925. // Calculate new position
  926. let newX = e.clientX - offsetX;
  927. let newY = e.clientY - offsetY;
  928.  
  929. // Constrain position to prevent going out of bounds
  930. newX = Math.max(0, Math.min(newX, window.innerWidth - element.offsetWidth));
  931. newY = Math.max(0, Math.min(newY, window.innerHeight - element.offsetHeight));
  932.  
  933. element.style.left = `${newX}px`;
  934. element.style.top = `${newY}px`;
  935. }
  936.  
  937. function stopDrag() {
  938. isDragging = false;
  939. document.removeEventListener('mousemove', onDrag);
  940. document.removeEventListener('mouseup', stopDrag);
  941. }
  942.  
  943. // Adjust position if the viewport size changes
  944. window.addEventListener('resize', () => {
  945. const currentLeft = parseInt(element.style.left, 10) || 0;
  946. const currentTop = parseInt(element.style.top, 10) || 0;
  947.  
  948. // Constrain position based on new viewport dimensions
  949. const newLeft = Math.min(currentLeft, window.innerWidth - element.offsetWidth);
  950. const newTop = Math.min(currentTop, window.innerHeight - element.offsetHeight);
  951.  
  952. element.style.left = `${Math.max(0, newLeft)}px`;
  953. element.style.top = `${Math.max(0, newTop)}px`;
  954. });
  955. }
  956.  
  957. // Add Resizable Functionality
  958. function makeResizable(element) {
  959. const resizeHandle = element.querySelector('.resize-handle');
  960. let isResizing = false,
  961. startWidth, startHeight, startX, startY;
  962.  
  963. resizeHandle.addEventListener('mousedown', (e) => {
  964. isResizing = true;
  965. startWidth = element.offsetWidth;
  966. startHeight = element.offsetHeight;
  967. startX = e.clientX;
  968. startY = e.clientY;
  969. document.addEventListener('mousemove', onResize);
  970. document.addEventListener('mouseup', stopResize);
  971. });
  972.  
  973. function onResize(e) {
  974. if (!isResizing) return;
  975. element.style.width = `${startWidth + (e.clientX - startX)}px`;
  976. element.style.height = `${startHeight + (e.clientY - startY)}px`;
  977. }
  978.  
  979. function stopResize() {
  980. isResizing = false;
  981. document.removeEventListener('mousemove', onResize);
  982. document.removeEventListener('mouseup', stopResize);
  983. }
  984. }
  985.  
  986. // Handle Button Click
  987. function createChatCopyUI(chatElement) {
  988. logMinor('Creating chat copy div...');
  989.  
  990. let main = document.querySelector('main[class*="chatContent"]') ||
  991. document.querySelector('section[class*="chatContent"]');
  992.  
  993. // Check if the UI already exists
  994. if (main.querySelector('#chat-copy-outer')) return;
  995.  
  996. // Create outer container
  997. const outerDiv = document.createElement('div');
  998. outerDiv.id = 'chat-copy-outer';
  999.  
  1000. // Create drag bar
  1001. const dragBar = document.createElement('div');
  1002. dragBar.className = 'drag-bar';
  1003. const dragBarSpan = document.createElement('span');
  1004. dragBarSpan.textContent = 'Copy';
  1005. dragBar.appendChild(dragBarSpan);
  1006.  
  1007. // Create close box
  1008. const closeBox = document.createElement('div');
  1009. closeBox.className = 'close-box';
  1010. closeBox.textContent = '×';
  1011. closeBox.addEventListener('click', () => {
  1012. // stopScrolling();
  1013. outerDiv.remove();
  1014. stopScrollingVar = true;
  1015. if (chatObserver) {
  1016. chatObserver.disconnect();
  1017. logMinor('Observer disconnected.');
  1018. }
  1019. });
  1020.  
  1021. // Create resizable handle
  1022. const resizeHandle = document.createElement('div');
  1023. resizeHandle.className = 'resize-handle';
  1024.  
  1025. // Create inner content area
  1026. const innerDiv = document.createElement('div');
  1027. innerDiv.id = 'chat-copy-inner';
  1028.  
  1029. logMinor('Reached append stage.');
  1030.  
  1031. // Append elements
  1032. outerDiv.appendChild(dragBar);
  1033. outerDiv.appendChild(closeBox);
  1034. outerDiv.appendChild(innerDiv);
  1035. outerDiv.appendChild(resizeHandle);
  1036. main.appendChild(outerDiv);
  1037.  
  1038. const copyButton = document.getElementById('copy-button-inner');
  1039. const rect = copyButton.getBoundingClientRect();
  1040.  
  1041. outerDiv.style.top = `${rect.top}px`; // Align with the top of the button
  1042. outerDiv.style.right = `${window.innerWidth - rect.right - 0.2 * parseFloat(getComputedStyle(document.documentElement).fontSize)}px`; // Align with the right of the button, adding 0.2em
  1043.  
  1044. // Create recording bar
  1045. const recordingBar = document.createElement('div');
  1046. recordingBar.className = 'recording-bar';
  1047. const recordingBarSpan = document.createElement('span');
  1048.  
  1049. recordingBarSpan.textContent = 'Recording';
  1050.  
  1051. // Append the recording bar to the outerDiv
  1052. outerDiv.appendChild(recordingBar);
  1053. recordingBar.appendChild(recordingBarSpan);
  1054.  
  1055. // Make it draggable and resizable
  1056. makeDraggable(outerDiv);
  1057. makeResizable(outerDiv);
  1058.  
  1059. copyChatMessages(chatElement);
  1060.  
  1061. const saveButton = document.createElement('div');
  1062. saveButton.id = "chat-copy-save-button";
  1063. saveButton.classList.add('chat-copy-big-button');
  1064. const saveButtonSpan = document.createElement('span');
  1065. saveButtonSpan.textContent = "Save";
  1066.  
  1067. const configButton = document.createElement('div');
  1068. configButton.id = "chat-copy-config-button";
  1069. configButton.classList.add('chat-copy-big-button');
  1070. const configButtonSpan = document.createElement('span');
  1071. configButtonSpan.textContent = "⚙";
  1072.  
  1073. const instructionButton = document.createElement('div');
  1074. instructionButton.id = "chat-copy-instruction-button";
  1075. instructionButton.classList.add('chat-copy-big-button');
  1076. const instructionButtonSpan = document.createElement('span');
  1077. instructionButtonSpan.textContent = "❓";
  1078.  
  1079. const saveSizeSpan = document.createElement('span');
  1080. saveSizeSpan.textContent = "Save";
  1081. saveSizeSpan.classList.add('save-size-span');
  1082. const saveSizeSpan2 = saveSizeSpan.cloneNode(true);
  1083.  
  1084.  
  1085.  
  1086. saveButton.appendChild(saveButtonSpan);
  1087. outerDiv.appendChild(saveButton);
  1088.  
  1089. configButton.appendChild(configButtonSpan);
  1090. configButton.appendChild(saveSizeSpan);
  1091. outerDiv.appendChild(configButton);
  1092.  
  1093. instructionButton.appendChild(instructionButtonSpan);
  1094. instructionButton.appendChild(saveSizeSpan2);
  1095. outerDiv.appendChild(instructionButton);
  1096.  
  1097. saveButton.addEventListener('click', () => {
  1098. saveChatContent(innerDiv);
  1099. });
  1100.  
  1101. configButton.addEventListener('click', () => {
  1102. openconfig(innerDiv);
  1103. });
  1104.  
  1105. instructionButton.addEventListener('click', () => {
  1106. openInstructions(innerDiv);
  1107. });
  1108.  
  1109. // Create autoscroll button
  1110. const autoscrollDiv = document.createElement('div');
  1111. autoscrollDiv.id = 'autoscroll-div';
  1112. const autoscrollButton = document.createElement('div');
  1113. autoscrollButton.id = "autoscroll-button";
  1114. autoscrollButton.classList.add('chat-copy-big-button');
  1115. const autoscrollSpan = document.createElement('span');
  1116. autoscrollSpan.textContent = "Autoscroll...";
  1117. autoscrollButton.appendChild(autoscrollSpan);
  1118. autoscrollDiv.appendChild(autoscrollButton);
  1119. outerDiv.appendChild(autoscrollDiv);
  1120.  
  1121. autoscrollButton.addEventListener('click', () => {
  1122. openAutoscrollControl(autoscrollDiv, autoscrollButton);
  1123. });
  1124.  
  1125. }
  1126.  
  1127. let date = false;
  1128. let dateStored = false;
  1129. let autoscrollStage = 1;
  1130.  
  1131. function resetDate() {
  1132. date = false;
  1133. logMinor('Date has been reset.');
  1134. }
  1135.  
  1136.  
  1137.  
  1138. function openAutoscrollControl(autoscrollDiv, autoscrollButton) {
  1139. autoscrollStage = 2;
  1140. resetDate();
  1141. // Hide the autoscroll button
  1142. autoscrollButton.style.display = 'none';
  1143.  
  1144. // Create autoscroll control container
  1145. const autoscrollControl = document.createElement('div');
  1146. autoscrollControl.id = "autoscroll-control";
  1147. // autoscrollControl.style.display = 'flex';
  1148. // autoscrollControl.style.flexDirection = 'column';
  1149. // autoscrollControl.style.alignItems = 'center';
  1150. // autoscrollControl.style.gap = '10px';
  1151.  
  1152. // Create up arrow
  1153. const upArrow = document.createElement('div');
  1154. upArrow.id = 'up-arrow';
  1155. upArrow.className = 'arrow';
  1156. // upArrow.innerHTML = `
  1157. // <svg width="36" height="48" viewBox="10 4 4 12" xmlns="http://www.w3.org/2000/svg">
  1158. // <path d="M 14 16 L 10 16 L 10 10 L 8 10 L 12 4 L 16 10 L 14 10 Z" fill="currentColor"></path>
  1159. // </svg>`;
  1160. const upArrowL2 = document.createElement('div');
  1161. upArrowL2.id = 'up-arrow-level-2';
  1162. const upArrowL3 = document.createElement('div');
  1163. upArrowL3.id = 'up-arrow-level-3';
  1164. const upArrowL3copy = document.createElement('div');
  1165. upArrowL3copy.id = 'up-arrow-level-3-copy';
  1166. const upArrowL4A = document.createElement('div');
  1167. upArrowL4A.id = 'up-arrow-level-4A';
  1168. const upArrowL4Acopy = document.createElement('div');
  1169. upArrowL4Acopy.id = 'up-arrow-level-4A-copy';
  1170. const upArrowL4B = document.createElement('div');
  1171. upArrowL4B.id = 'up-arrow-level-4B';
  1172. const upArrowL4Bcopy = document.createElement('div');
  1173. upArrowL4Bcopy.id = 'up-arrow-level-4B-copy';
  1174.  
  1175. upArrow.appendChild(upArrowL2);
  1176. upArrowL2.appendChild(upArrowL3);
  1177. upArrowL2.appendChild(upArrowL3copy);
  1178. upArrowL3.appendChild(upArrowL4A);
  1179. upArrowL3.appendChild(upArrowL4B);
  1180. upArrowL3copy.appendChild(upArrowL4Acopy);
  1181. upArrowL3copy.appendChild(upArrowL4Bcopy);
  1182.  
  1183. // Create radio group
  1184. const radioGroupNow = document.createElement('div');
  1185. radioGroupNow.id = 'autoscroll-option-now';
  1186. const radioGroupDate = document.createElement('div');
  1187. radioGroupDate.id = 'autoscroll-option-date';
  1188.  
  1189. // First radio: "Now"
  1190. const nowRadio = document.createElement('input');
  1191. nowRadio.type = 'radio';
  1192. nowRadio.name = 'autoscroll';
  1193. nowRadio.id = 'autoscroll-now';
  1194. nowRadio.checked = true;
  1195.  
  1196. const nowLabel = document.createElement('label');
  1197. nowLabel.htmlFor = 'autoscroll-now';
  1198. nowLabel.innerHTML = 'First/<br>Last';
  1199.  
  1200. // Second radio: "Date"
  1201. const dateRadio = document.createElement('input');
  1202. dateRadio.type = 'radio';
  1203. dateRadio.name = 'autoscroll';
  1204. dateRadio.id = 'autoscroll-date';
  1205.  
  1206. const dateLabel = document.createElement('label');
  1207. dateLabel.htmlFor = 'autoscroll-date';
  1208. dateLabel.textContent = 'Date';
  1209.  
  1210. // Append radios and labels
  1211. radioGroupNow.appendChild(nowRadio);
  1212. radioGroupNow.appendChild(nowLabel);
  1213. radioGroupDate.appendChild(dateRadio);
  1214. radioGroupDate.appendChild(dateLabel);
  1215.  
  1216. // Create down arrow
  1217. const downArrow = document.createElement('div');
  1218. downArrow.id = 'down-arrow';
  1219. downArrow.className = 'arrow';
  1220. // downArrow.innerHTML = `
  1221. // <svg width="36" height="48" viewBox="10 8 4 12" xmlns="http://www.w3.org/2000/svg">
  1222. // <path d="M 10 8 L 14 8 L 14 14 L 16 14 L 12 20 L 8 14 L 10 14 Z" fill="currentColor"></path>
  1223. // </svg>`;
  1224. const downArrowL2 = document.createElement('div');
  1225. downArrowL2.id = 'down-arrow-level-2';
  1226. const downArrowL3 = document.createElement('div');
  1227. downArrowL3.id = 'down-arrow-level-3';
  1228. const downArrowL3copy = document.createElement('div');
  1229. downArrowL3copy.id = 'down-arrow-level-3-copy';
  1230. const downArrowL4A = document.createElement('div');
  1231. downArrowL4A.id = 'down-arrow-level-4A';
  1232. const downArrowL4Acopy = document.createElement('div');
  1233. downArrowL4Acopy.id = 'down-arrow-level-4A-copy';
  1234. const downArrowL4B = document.createElement('div');
  1235. downArrowL4B.id = 'down-arrow-level-4B';
  1236. const downArrowL4Bcopy = document.createElement('div');
  1237. downArrowL4Bcopy.id = 'down-arrow-level-4B-copy';
  1238.  
  1239. downArrow.appendChild(downArrowL2);
  1240. downArrowL2.appendChild(downArrowL3);
  1241. downArrowL2.appendChild(downArrowL3copy);
  1242. downArrowL3.appendChild(downArrowL4A);
  1243. downArrowL3.appendChild(downArrowL4B);
  1244. downArrowL3copy.appendChild(downArrowL4Acopy);
  1245. downArrowL3copy.appendChild(downArrowL4Bcopy);
  1246.  
  1247. // Add Return button
  1248. const returnButtonDiv = document.createElement('div');
  1249. returnButtonDiv.id = 'autoscroll-return-button';
  1250. const returnButtonSpan = document.createElement('span');
  1251. returnButtonSpan.textContent = 'Return';
  1252. returnButtonDiv.appendChild(returnButtonSpan);
  1253.  
  1254.  
  1255.  
  1256. // Append all elements to autoscroll control
  1257. autoscrollControl.appendChild(upArrow);
  1258. autoscrollControl.appendChild(radioGroupNow);
  1259. autoscrollControl.appendChild(radioGroupDate);
  1260. autoscrollControl.appendChild(returnButtonDiv);
  1261. autoscrollControl.appendChild(downArrow);
  1262.  
  1263. // Append autoscroll control to the div
  1264. autoscrollDiv.appendChild(autoscrollControl);
  1265.  
  1266. // Add listeners for upArrow and downArrow
  1267. upArrow.addEventListener('click', () => autoscroll('up', upArrowL2, downArrowL2));
  1268. downArrow.addEventListener('click', () => autoscroll('down', upArrowL2, downArrowL2));
  1269.  
  1270. // Add listeners for radio groups
  1271. radioGroupNow.addEventListener('change', () => displayDate('remove'));
  1272. radioGroupDate.addEventListener('change', () => displayDate('show', radioGroupDate));
  1273.  
  1274. // Add event listener for the return button
  1275. returnButtonDiv.addEventListener('click', () => autoscrollReturn(autoscrollControl, autoscrollButton));
  1276. }
  1277.  
  1278.  
  1279.  
  1280.  
  1281. let activeScroll = null; // Declare the autoscroll interval globally
  1282.  
  1283. function autoscrollReturn(autoscrollControl, autoscrollButton) {
  1284. // Remove autoscroll control
  1285. autoscrollControl.remove();
  1286.  
  1287. // Show the autoscroll button again
  1288. autoscrollButton.style.removeProperty('display');
  1289.  
  1290. // Stop any active autoscroll
  1291. if (activeScroll) {
  1292. clearInterval(activeScroll);
  1293. activeScroll = null;
  1294. }
  1295. resetDate();
  1296.  
  1297. if (autoscrollStage === 3) {
  1298. const autoscrollDiv = document.querySelector('#chat-copy-outer #autoscroll-div');
  1299. openAutoscrollControl(autoscrollDiv, autoscrollButton)
  1300. autoscrollStage = 2;
  1301. } else {
  1302. autoscrollStage = 1;
  1303. }
  1304. }
  1305.  
  1306.  
  1307. function autoscroll(direction, upArrow, downArrow) {
  1308. stopScrollingVar = false;
  1309. autoscrollStage = 3;
  1310. // Find the chat container element using the provided selectors
  1311. const chatElement = document.querySelector('main[class*="chatContent"] [class*="scrollerBase"], section[class*="chatContent"] [class*="scrollerBase"]');
  1312.  
  1313. if (!chatElement) {
  1314. console.error('Chat element not found!');
  1315. return;
  1316. } else {
  1317. logMinor(chatElement);
  1318. }
  1319.  
  1320. if (direction === 'up') {
  1321. upArrow.classList.add('blinking-arrow-button'); // Add blinking class to upArrow
  1322. downArrow.classList.remove('blinking-arrow-button'); // Remove blinking class from downArrow
  1323. } else if (direction === 'down') {
  1324. downArrow.classList.add('blinking-arrow-button'); // Add blinking class to downArrow
  1325. upArrow.classList.remove('blinking-arrow-button'); // Remove blinking class from upArrow
  1326. }
  1327.  
  1328. const timeZoneWarning = document.querySelector('#date-div #time-zone-warning');
  1329. if (timeZoneWarning) {
  1330. timeZoneWarning.remove();
  1331. }
  1332.  
  1333. // Check if a stop button already exists
  1334. let stopButtonDiv = document.querySelector('#autoscroll-stop-button');
  1335. if (stopButtonDiv) {
  1336. // If the button exists, clear the existing scroll to allow a new one to start
  1337. clearInterval(window.activeScroll);
  1338. window.activeScroll = null;
  1339. clearInterval(activeScroll);
  1340. activeScroll = null;
  1341. } else {
  1342. // If the stop button does not exist, create it
  1343. stopButtonDiv = document.createElement('div');
  1344. stopButtonDiv.id = 'autoscroll-stop-button'; // Add ID for styling
  1345. const stopButtonSpan = document.createElement('span');
  1346. stopButtonSpan.textContent = 'Stop';
  1347. stopButtonDiv.appendChild(stopButtonSpan);
  1348.  
  1349. // Insert Stop button between the arrows
  1350. const autoscrollControl = document.getElementById('autoscroll-control'); // Assuming this is the container for the arrows and the stop button
  1351. const downArrow = document.getElementById('down-arrow'); // Assuming IDs for upArrow and downArrow
  1352. autoscrollControl.insertBefore(stopButtonDiv, downArrow);
  1353. }
  1354.  
  1355. // Remove #autoscroll-option-now and #autoscroll-option-date if present
  1356. const nowOption = document.getElementById('autoscroll-option-now');
  1357. const dateOption = document.getElementById('autoscroll-option-date');
  1358. if (nowOption) nowOption.remove();
  1359. if (!date) {
  1360. if (dateOption) dateOption.remove();
  1361. } else {
  1362. // Create the #current-date div if it doesn't exist
  1363. let currentDateDiv = document.querySelector('#current-date');
  1364. if (!currentDateDiv) {
  1365. currentDateDiv = document.createElement('div');
  1366. currentDateDiv.id = 'current-date';
  1367.  
  1368. const currentDateLabel = document.createElement('span');
  1369. currentDateLabel.textContent = 'Current date:';
  1370.  
  1371. const currentDateSpan = document.createElement('span');
  1372. currentDateSpan.id = 'current-date-span';
  1373. currentDateSpan.textContent = '...'; // Placeholder content
  1374.  
  1375. currentDateDiv.appendChild(currentDateLabel);
  1376. currentDateDiv.appendChild(currentDateSpan);
  1377.  
  1378. const dateDiv = document.getElementById('date-div'); // Assuming this is where the new div should be inserted
  1379. // Append currentDateDiv after dateDiv
  1380. dateDiv.parentNode.insertBefore(currentDateDiv, dateDiv.nextSibling);
  1381. }
  1382. }
  1383.  
  1384. let dateTime;
  1385.  
  1386. // Function to update the #current-date-span with the current date
  1387. function updateCurrentDateSpan(currentMessage) {
  1388.  
  1389. if (currentMessage) {
  1390.  
  1391. const timeElement = currentMessage.querySelector('time');
  1392. if (timeElement && timeElement.hasAttribute('datetime')) {
  1393.  
  1394. // Get the datetime attribute and hack off the 'T' and everything after it
  1395. dateTime = timeElement.getAttribute('datetime').split('T')[0]; // Take only the date part
  1396. const currentDateSpan = document.getElementById('current-date-span');
  1397. // logMinor('Extracting date from current message: ' + datetime);
  1398. if (currentDateSpan) {
  1399. currentDateSpan.textContent = dateTime; // Update the span content with the date
  1400. }
  1401. } else {
  1402. // logMinor('timeElement: ' + timeElement.outerHTML + '\ntimeElement.datetime: ' + timeElement.datetime);
  1403. }
  1404. }
  1405. }
  1406.  
  1407. // Scroll setup
  1408. const scrollSpeed = 100; // Pixels per scroll interval
  1409. const interval = 50; // Interval in milliseconds
  1410. const timeoutDuration = 5000; // 2.5 seconds of inactivity
  1411.  
  1412. // Check and clear any active scroll to avoid stacking
  1413. if (activeScroll) {
  1414. clearInterval(activeScroll);
  1415. activeScroll = null;
  1416. }
  1417.  
  1418. let lastMessage = null;
  1419. let lastMessagePosition = 0;
  1420. // let lastChangeTime = Date.now(); // Initialize with the current time
  1421. let timeoutIntervals = timeoutDuration / interval;
  1422. let currentInterval = 0;
  1423. let lastChangeInterval = 0;
  1424. let dateReached = false;
  1425.  
  1426. // Start the autoscroll routine
  1427. activeScroll = setInterval(() => {
  1428. if (stopScrollingVar) {
  1429. stopScrolling();
  1430. }
  1431.  
  1432. let currentMessage = null;
  1433. let currentMessagePosition = 0;
  1434. currentInterval += 1;
  1435.  
  1436. if (direction === 'up') {
  1437. chatElement.scrollBy(0, -scrollSpeed); // Scroll up
  1438. currentMessage = chatElement.querySelector('li[id^="chat-messages"]');
  1439. if (currentMessage) {
  1440. currentMessagePosition = currentMessage.getBoundingClientRect().top;
  1441. }
  1442. } else if (direction === 'down') {
  1443. chatElement.scrollBy(0, scrollSpeed); // Scroll down
  1444. const messages = chatElement.querySelectorAll('li[id^="chat-messages"]');
  1445. currentMessage = messages[messages.length - 1];
  1446. if (currentMessage) {
  1447. currentMessagePosition = currentMessage.getBoundingClientRect().bottom;
  1448. }
  1449. }
  1450.  
  1451. if (date) {
  1452. // Update current date span
  1453. updateCurrentDateSpan(currentMessage); // Call the function to update the current date
  1454. } else {
  1455. // logMinor('No date.');
  1456. }
  1457.  
  1458. // Check if the message has changed or if the position has changed
  1459. if (currentMessage !== lastMessage || currentMessagePosition !== lastMessagePosition) {
  1460. lastMessage = currentMessage;
  1461. lastMessagePosition = currentMessagePosition;
  1462. lastChangeInterval = currentInterval;
  1463. // lastChangeTime = Date.now(); // Reset the timeout when the message or position changes
  1464. }
  1465.  
  1466. // Stop scrolling after 2.5 seconds of no change
  1467. // if (Date.now() - lastChangeTime >= timeoutDuration) {
  1468. // stopScrolling();
  1469. // logMinor('Autoscroll stopped due to inactivity.');
  1470. // }
  1471. if (currentInterval - lastChangeInterval >= timeoutIntervals) {
  1472. stopScrolling();
  1473. logMinor('Autoscroll stopped due to inactivity.');
  1474. }
  1475.  
  1476. if (date) {
  1477. const [year, month, day] = dateTime.split('-'); // Destructure the split result
  1478. const dateMap = {
  1479. year: parseInt(year, 10),
  1480. month: parseInt(month, 10),
  1481. day: parseInt(day, 10)
  1482. };
  1483.  
  1484. logMinor('date: ' + JSON.stringify(date) + '; dateMap: ' + JSON.stringify(dateMap));
  1485.  
  1486. if (direction === 'up') {
  1487. if (dateMap.year < date.year) {
  1488. dateReached = true;
  1489. } else if (dateMap.year === date.year && dateMap.month < date.month) {
  1490. dateReached = true;
  1491. } else if (
  1492. dateMap.year === date.year &&
  1493. dateMap.month === date.month &&
  1494. dateMap.day < date.day
  1495. ) {
  1496. dateReached = true;
  1497. }
  1498. } else if (direction === 'down') {
  1499. if (dateMap.year > date.year) {
  1500. dateReached = true;
  1501. } else if (dateMap.year === date.year && dateMap.month > date.month) {
  1502. dateReached = true;
  1503. } else if (
  1504. dateMap.year === date.year &&
  1505. dateMap.month === date.month &&
  1506. dateMap.day > date.day
  1507. ) {
  1508. dateReached = true;
  1509. }
  1510. }
  1511. }
  1512.  
  1513. if (dateReached) {
  1514. stopScrolling();
  1515. logMinor('Target date has been included.');
  1516. }
  1517.  
  1518. }, interval);
  1519.  
  1520. // Add listener to stop button
  1521. stopButtonDiv.addEventListener('click', () => {
  1522. stopScrolling();
  1523. // Remove the stop button
  1524. });
  1525.  
  1526. function stopScrolling() {
  1527. clearInterval(activeScroll); // Stop the autoscroll
  1528. activeScroll = null; // Clear the activeScroll variable to ensure no active autoscroll routine is left
  1529. downArrow.classList.remove('blinking-arrow-button');
  1530. upArrow.classList.remove('blinking-arrow-button');
  1531. let currentInterval = 0;
  1532. let lastChangeInterval = 0;
  1533. if (stopButtonDiv) {
  1534. stopButtonDiv.remove();
  1535. }
  1536. dateReached = false;
  1537. stopScrollingVar = false;
  1538.  
  1539. }
  1540.  
  1541. }
  1542.  
  1543.  
  1544. function displayDate(argument, radioGroupDate) {
  1545. logMinor('Creating date div...');
  1546. const existingDateDiv = document.getElementById('date-div');
  1547. if (argument === 'show') {
  1548. if (existingDateDiv) {
  1549. return;
  1550. }
  1551.  
  1552. // Create the date div
  1553. const dateDiv = document.createElement('div');
  1554. dateDiv.id = 'date-div';
  1555.  
  1556. // Create the first row (YYYY, MM, DD)
  1557. const labels = ['YYYY', 'MM', 'DD'];
  1558. labels.forEach(label => {
  1559. const span = document.createElement('span');
  1560. span.textContent = label;
  1561. dateDiv.appendChild(span);
  1562. });
  1563.  
  1564. let values;
  1565. date = {
  1566. year: null,
  1567. month: null,
  1568. day: null
  1569. };
  1570.  
  1571. // If dateStored exists, use those values; otherwise, use the current date
  1572. if (dateStored) {
  1573. values = [
  1574. dateStored.year, // Use stored year
  1575. String(dateStored.month).padStart(2, '0'), // Use stored month
  1576. String(dateStored.day).padStart(2, '0') // Use stored day
  1577. ];
  1578. date = {
  1579. year: parseInt(dateStored.year, 10), // Use stored year
  1580. month: parseInt(dateStored.month, 10), // Use stored month (formatted as 2 digits)
  1581. day: parseInt(dateStored.day, 10) // Use stored day (formatted as 2 digits)
  1582. };
  1583. } else {
  1584. const currentDate = new Date();
  1585. values = [
  1586. currentDate.getFullYear(), // Current year
  1587. String(currentDate.getMonth() + 1).padStart(2, '0'), // Current month
  1588. String(currentDate.getDate()).padStart(2, '0') // Current day
  1589. ];
  1590. date = {
  1591. year: currentDate.getFullYear(), // Current year
  1592. month: currentDate.getMonth() + 1, // Current month
  1593. day: currentDate.getDate() // Current day
  1594. };
  1595. }
  1596.  
  1597. // logMinor(`Date updated to: ${date.year}-${date.month}-${date.day}`);
  1598. logMinor('Date updated to: ' + JSON.stringify(date));
  1599.  
  1600. values.forEach((value, index) => {
  1601. const input = document.createElement('input');
  1602. input.type = 'number';
  1603. input.value = value;
  1604.  
  1605. // Configure input box sizes
  1606. if (index === 0) input.maxLength = 4; // YYYY
  1607. else input.maxLength = 2; // MM or DD
  1608.  
  1609. // Prevent decimals
  1610. input.step = '1';
  1611. if (index === 0) {
  1612. input.min = 1000; // Min for year is 1000
  1613. input.max = 9999; // Max for year is 9999
  1614. } else if (index === 1) {
  1615. input.min = 1; // Min for month is 1
  1616. input.max = 12; // Max for month is 12
  1617. } else if (index === 2) {
  1618. input.min = 1; // Min for day is 1
  1619. input.max = 31; // Max for day is 31
  1620. }
  1621.  
  1622.  
  1623.  
  1624. input.addEventListener('input', () => {
  1625. // Update the date object whenever an input value changes
  1626. date.year = parseInt(document.querySelector('#date-div input:nth-of-type(1)').value, 10);
  1627. date.month = parseInt(document.querySelector('#date-div input:nth-of-type(2)').value, 10);
  1628. date.day = parseInt(document.querySelector('#date-div input:nth-of-type(3)').value, 10);
  1629.  
  1630. // Update dateStored with the new date
  1631. dateStored = date;
  1632. // logMinor(`Date updated to: ${date.year}-${date.month}-${date.day}`);
  1633. logMinor('Date updated to: ' + JSON.stringify(date));
  1634. });
  1635.  
  1636. dateDiv.appendChild(input);
  1637. });
  1638.  
  1639. const timeZoneWarning = document.createElement('span');
  1640. timeZoneWarning.id = 'time-zone-warning';
  1641. timeZoneWarning.textContent = 'Note: Due to different time zones, date may be imprecise.';
  1642.  
  1643. dateDiv.appendChild(timeZoneWarning);
  1644.  
  1645. logMinor('Inserting date div...');
  1646.  
  1647. // Insert the date div immediately after the radioGroupDate
  1648. radioGroupDate.parentNode.insertBefore(dateDiv, radioGroupDate.nextSibling);
  1649.  
  1650. }
  1651.  
  1652. if (argument === 'remove') {
  1653. if (existingDateDiv) {
  1654. existingDateDiv.remove();
  1655. }
  1656. resetDate();
  1657. }
  1658. }
  1659.  
  1660.  
  1661.  
  1662.  
  1663. // Open the instructions
  1664. function openInstructions(innerDiv) {
  1665. const instructionDiv = document.createElement('div');
  1666. instructionDiv.id = "chat-copy-instruction";
  1667. const instructionOuter = document.createElement('div');
  1668. instructionOuter.id = "chat-copy-instruction-outer";
  1669. instructionDiv.innerHTML = instructions;
  1670.  
  1671. instructionOuter.appendChild(instructionDiv);
  1672.  
  1673. if (innerDiv) {
  1674. innerDiv.style.display = "none";
  1675. innerDiv.insertAdjacentElement('afterend', instructionOuter);
  1676. }
  1677.  
  1678. // Close button
  1679. const closeButton = document.createElement('button');
  1680. const closeButtonSpan = document.createElement('span');
  1681. closeButtonSpan.textContent = "Close";
  1682. closeButton.appendChild(closeButtonSpan);
  1683. closeButton.id = 'close-instruction-button';
  1684. closeButton.addEventListener('click', () => {
  1685. instructionOuter.remove();
  1686. if (innerDiv) {
  1687. innerDiv.style.display = "block";
  1688. }
  1689. });
  1690. instructionDiv.appendChild(closeButton);
  1691.  
  1692. }
  1693.  
  1694. // Open the config options
  1695. function openconfig(innerDiv) {
  1696. const configDiv = document.createElement('div');
  1697. configDiv.id = "chat-copy-settings";
  1698. const configOuter = document.createElement('div');
  1699. configOuter.id = "chat-copy-settings-outer";
  1700.  
  1701. configOuter.appendChild(configDiv);
  1702.  
  1703. if (innerDiv) {
  1704. innerDiv.style.display = "none";
  1705. innerDiv.insertAdjacentElement('afterend', configOuter);
  1706. }
  1707.  
  1708. // Heading
  1709. const heading = document.createElement('h2');
  1710. const headingSpan = document.createElement('span');
  1711. headingSpan.textContent = "Options";
  1712. heading.appendChild(headingSpan);
  1713. configDiv.appendChild(heading);
  1714.  
  1715. // Enable checkboxes section
  1716. const enableText = document.createElement('h3');
  1717. enableText.textContent = "Check these boxes to enable:";
  1718. configDiv.appendChild(enableText);
  1719.  
  1720. const createCheckbox = (id, labelText) => {
  1721. const wrapper = document.createElement('div');
  1722. wrapper.classList.add('wrapper');
  1723. const wrapperInner = document.createElement('div');
  1724. wrapperInner.classList.add('wrapper-inner');
  1725. const checkbox = document.createElement('input');
  1726. checkbox.type = "checkbox";
  1727. checkbox.id = id;
  1728.  
  1729. const label = document.createElement('label');
  1730. label.htmlFor = id;
  1731. label.textContent = labelText;
  1732.  
  1733. wrapperInner.appendChild(checkbox);
  1734. wrapperInner.appendChild(label);
  1735. wrapper.appendChild(wrapperInner);
  1736. return wrapper;
  1737. };
  1738.  
  1739. // Create checkboxes and assign them to constants
  1740. const enableAttachments = createCheckbox("enable-attachments", "Attachments");
  1741. const enableAudio = createCheckbox("enable-audio", "Audio");
  1742. const enableFullSizeImages = createCheckbox("enable-full-size-images", "Full-Size Images");
  1743.  
  1744. // Create checkboxes and assign them to constants
  1745. const enableAttachmentsCheckbox = enableAttachments.querySelector('input');
  1746. const enableAudioCheckbox = enableAudio.querySelector('input');
  1747. const enableFullSizeImagesCheckbox = enableFullSizeImages.querySelector('input');
  1748.  
  1749. // Append the checkboxes to the configDiv
  1750. configDiv.appendChild(enableFullSizeImages);
  1751. configDiv.appendChild(enableAudio);
  1752. configDiv.appendChild(enableAttachments);
  1753.  
  1754. function divide() {
  1755. const divider = document.createElement('div');
  1756. divider.classList.add('divider');
  1757. configDiv.appendChild(divider);
  1758. }
  1759.  
  1760. divide();
  1761.  
  1762. // Button visibility section
  1763. const notUsingText = document.createElement('h3');
  1764. notUsingText.textContent = "When not using the chat saver:";
  1765. configDiv.appendChild(notUsingText);
  1766. const orHideText = document.createElement('p');
  1767. orHideText.textContent = "Or hide the button and open it from:";
  1768.  
  1769.  
  1770. const createRadio = (id, name, labelText) => {
  1771. const wrapper = document.createElement('div');
  1772. wrapper.classList.add('wrapper');
  1773. const wrapperInner = document.createElement('div');
  1774. wrapperInner.classList.add('wrapper-inner');
  1775. const radio = document.createElement('input');
  1776. radio.type = "radio";
  1777. radio.id = id;
  1778. radio.name = name;
  1779.  
  1780. const label = document.createElement('label');
  1781. label.htmlFor = id;
  1782. label.textContent = labelText;
  1783.  
  1784. wrapperInner.appendChild(radio);
  1785. wrapperInner.appendChild(label);
  1786. wrapper.appendChild(wrapperInner);
  1787. return wrapper;
  1788.  
  1789. };
  1790.  
  1791. // Create radio buttons and assign them to constants
  1792. const showButtonCornerRadio = createRadio("show-button-corner", "button-location", "Show the button in the corner");
  1793. const hideButtonDMRadio = createRadio("hide-button-dm", "button-location", "Direct Messages list");
  1794. const hideButtonServersRadio = createRadio("hide-button-servers", "button-location", "Bottom of Servers sidebar");
  1795. const hideButtonSettingsRadio = createRadio("hide-button-settings", "button-location", "Settings page");
  1796.  
  1797. // Append the radio buttons and text to the configDiv
  1798. configDiv.appendChild(showButtonCornerRadio);
  1799. configDiv.appendChild(orHideText);
  1800. configDiv.appendChild(hideButtonDMRadio);
  1801. configDiv.appendChild(hideButtonServersRadio);
  1802. configDiv.appendChild(hideButtonSettingsRadio);
  1803.  
  1804. const hideNote = document.createElement('p');
  1805. hideNote.textContent = 'Note: When you open the button, it will stay in the corner until you hide it again.';
  1806.  
  1807. configDiv.appendChild(hideNote);
  1808.  
  1809. divide();
  1810.  
  1811. const enableLogMinor = createCheckbox("enable-log-minor", "Enable extra logs in the dev console");
  1812. const enableLogMinorCheckbox = enableLogMinor.querySelector('input');
  1813.  
  1814. configDiv.appendChild(enableLogMinor);
  1815.  
  1816. // Close button
  1817. const closeButton = document.createElement('button');
  1818. const closeButtonSpan = document.createElement('span');
  1819. closeButtonSpan.textContent = "Close config";
  1820. closeButton.id = 'close-config-button';
  1821. closeButton.addEventListener('click', () => {
  1822. configOuter.remove();
  1823. if (innerDiv) {
  1824. innerDiv.style.display = "block";
  1825. }
  1826. });
  1827.  
  1828. closeButton.appendChild(closeButtonSpan);
  1829. configDiv.appendChild(closeButton);
  1830.  
  1831. // Save the settings to localStorage
  1832. const saveSettings = () => {
  1833. const settings = {
  1834. enableAttachments: enableAttachmentsCheckbox.checked,
  1835. enableAudio: enableAudioCheckbox.checked,
  1836. enableFullSizeImages: enableFullSizeImagesCheckbox.checked,
  1837. buttonLocation: document.querySelector('input[name="button-location"]:checked')?.id || 'show-button-corner', // default to 'showButtonCorner' if no radio selected
  1838. enableLogMinor: enableLogMinorCheckbox.checked, // Save the new option
  1839. };
  1840.  
  1841. // Update the new variables with the current settings
  1842. attachmentsEnabled = settings.enableAttachments;
  1843. audioEnabled = settings.enableAudio;
  1844. fetchFullSize = settings.enableFullSizeImages;
  1845. buttonGenerationLocation = settings.buttonLocation;
  1846. showMinorLogs = settings.enableLogMinor; // Update the variable
  1847.  
  1848. console.log(settings);
  1849.  
  1850. localStorage.setItem('chatCopySettings', JSON.stringify(settings));
  1851.  
  1852. if (buttonGenerationLocation == 'show-button-corner') {
  1853. handleChatFound();
  1854. buttonPlacer();
  1855. buttonCleanup();
  1856. } else {
  1857. const copyButtonOuter = document.getElementById('copy-button-outer');
  1858.  
  1859. if (copyButtonOuter) {
  1860. // Add the class that triggers the animation
  1861. copyButtonOuter.classList.add('move-fade-out');
  1862.  
  1863. // Optionally, remove the element after the animation ends
  1864. copyButtonOuter.addEventListener('animationend', () => {
  1865. copyButtonOuter.remove(); // Remove the element once the animation is done
  1866. buttonPlacer();
  1867. buttonCleanup();
  1868. });
  1869. } else {
  1870. buttonPlacer();
  1871. buttonCleanup();
  1872. }
  1873. }
  1874. };
  1875.  
  1876. // Add event listeners to checkboxes and radio buttons
  1877. enableAttachmentsCheckbox.addEventListener('change', saveSettings);
  1878. enableAudioCheckbox.addEventListener('change', saveSettings);
  1879. enableFullSizeImagesCheckbox.addEventListener('change', saveSettings);
  1880. document.querySelectorAll('input[name="button-location"]').forEach(radio => {
  1881. radio.addEventListener('change', saveSettings);
  1882. });
  1883. enableLogMinorCheckbox.addEventListener('change', saveSettings);
  1884.  
  1885. // Load settings from localStorage and apply them
  1886. const loadSettings = () => {
  1887. const savedSettings = JSON.parse(localStorage.getItem('chatCopySettings'));
  1888. console.log(savedSettings);
  1889.  
  1890. if (savedSettings) {
  1891. enableAttachmentsCheckbox.checked = savedSettings.enableAttachments;
  1892. enableAudioCheckbox.checked = savedSettings.enableAudio;
  1893. enableFullSizeImagesCheckbox.checked = savedSettings.enableFullSizeImages;
  1894. enableLogMinorCheckbox.checked = savedSettings.enableLogMinor;
  1895.  
  1896. // Set the selected radio button based on saved settings
  1897. const radio = document.getElementById(savedSettings.buttonLocation);
  1898. if (radio) {
  1899. radio.checked = true;
  1900. }
  1901. } else {
  1902. // Set default settings if no saved data exists
  1903. enableAttachmentsCheckbox.checked = true;
  1904. enableAudioCheckbox.checked = true;
  1905. enableFullSizeImagesCheckbox.checked = true;
  1906. document.querySelector('input[name="button-location"][id="show-button-corner"]').checked = true;
  1907. enableLogMinorCheckbox.checked = false;
  1908. }
  1909. };
  1910.  
  1911. // Call loadSettings when the config page is loaded
  1912. loadSettings();
  1913.  
  1914. // Create a button to clear saved settings
  1915. const clearSettingsButton = document.createElement('button');
  1916. clearSettingsButton.id = 'clear-settings-button';
  1917. clearSettingsButton.textContent = "Clear Saved Settings";
  1918. clearSettingsButton.setAttribute('disabled', '');
  1919.  
  1920.  
  1921. // Add an event listener to clear settings on click
  1922. clearSettingsButton.addEventListener('click', () => {
  1923. // Remove settings from localStorage
  1924. localStorage.removeItem('chatCopySettings');
  1925. console.log('Settings have been cleared');
  1926.  
  1927. // Optionally, reload the settings (to reset the UI)
  1928. loadSettings(); // You can call your loadSettings function here to reset UI to defaults
  1929. });
  1930.  
  1931. // Append the button to the configDiv or wherever you'd like to display it
  1932. configDiv.appendChild(clearSettingsButton);
  1933.  
  1934. }
  1935.  
  1936.  
  1937.  
  1938.  
  1939.  
  1940.  
  1941. // Declare the observer variable globally so it can be referenced later
  1942. let chatObserver;
  1943.  
  1944. function copyChatMessages(chatElement) {
  1945. logMinor('Copying chat messages...');
  1946. const innerDiv = document.querySelector('#chat-copy-inner');
  1947. if (!innerDiv) return;
  1948.  
  1949. // Clear existing content
  1950. innerDiv.innerHTML = '';
  1951.  
  1952. // Find all chat messages
  1953. const messages = chatElement.querySelectorAll('li[id^="chat-messages"]');
  1954. if (!messages.length) {
  1955. innerDiv.textContent = 'No messages found.';
  1956. return;
  1957. }
  1958.  
  1959. // Add messages to the innerDiv in order
  1960. const seenMessages = new Set();
  1961. messages.forEach(message => {
  1962. seenMessages.add(message.id);
  1963. const clonedMessage = message.cloneNode(true);
  1964. innerDiv.appendChild(clonedMessage);
  1965. });
  1966.  
  1967. logMinor(`${messages.length} messages copied.`);
  1968.  
  1969.  
  1970.  
  1971. let processTimeout = null; // Timeout for batching updates
  1972.  
  1973. // Function to process all messages in the container
  1974. const processAllMessages = () => {
  1975. const messageNodes = Array.from(document.querySelectorAll('li[id^="chat-messages"]')); // Select all message nodes
  1976.  
  1977. messageNodes.forEach(node => {
  1978. if (!seenMessages.has(node.id)) {
  1979. // New message: Add it
  1980. const clonedMessage = node.cloneNode(true);
  1981. const messageIDs = Array.from(innerDiv.children).map(child => child.id);
  1982.  
  1983. // // Find the correct position based on IDs
  1984. // const index = messageIDs.findIndex(id => id > node.id);
  1985.  
  1986. const messageTimes = Array.from(innerDiv.children).map(child => {
  1987. const timeElement = child.querySelector('time');
  1988. return timeElement ? timeElement.getAttribute('datetime') : null;
  1989. });
  1990.  
  1991. const newMessageTime = node.querySelector('time')?.getAttribute('datetime') || '';
  1992.  
  1993. // Find the correct position based on datetime
  1994. const index = messageTimes.findIndex(existingTime => existingTime && existingTime > newMessageTime);
  1995.  
  1996. if (index === -1) {
  1997. innerDiv.appendChild(clonedMessage); // Append at the end
  1998. } else {
  1999. innerDiv.insertBefore(clonedMessage, innerDiv.children[index]); // Insert at the correct position
  2000. }
  2001.  
  2002. seenMessages.add(node.id);
  2003. logMinor(`New message added: ${node.id}`);
  2004. } else {
  2005. // Existing message: Update it if content differs
  2006. const existingMessage = innerDiv.querySelector(`#${node.id}`);
  2007. if (existingMessage && existingMessage.innerHTML !== node.innerHTML) {
  2008. existingMessage.innerHTML = node.innerHTML;
  2009. logMinor(`Message updated: ${node.id}`);
  2010. }
  2011. }
  2012. });
  2013.  
  2014. processTimeout = null; // Reset timeout
  2015. };
  2016.  
  2017. // Function to handle mutations
  2018. const onNewMessages = (mutations) => {
  2019. mutations.forEach(mutation => {
  2020. if (mutation.type === 'childList') {
  2021. // Set a timeout to process all messages (if not already set)
  2022. if (!processTimeout) {
  2023. processTimeout = setTimeout(processAllMessages, 1000); // Process after 1 second
  2024. }
  2025. }
  2026. });
  2027. };
  2028.  
  2029.  
  2030.  
  2031.  
  2032.  
  2033. // Set up the observer
  2034. if (chatObserver) chatObserver.disconnect(); // Disconnect existing observer if any
  2035. chatObserver = new MutationObserver(onNewMessages);
  2036. chatObserver.observe(chatElement, {
  2037. childList: true,
  2038. subtree: true
  2039. });
  2040. }
  2041.  
  2042.  
  2043.  
  2044.  
  2045.  
  2046.  
  2047. // Ensure JSZip is available
  2048. let jszipAvailable = typeof JSZip !== "undefined";
  2049.  
  2050. // Function to clean the image URL (remove query parameters like ?size=80)
  2051. function cleanUrl(url) {
  2052. return url.split('?')[0]; // Remove anything after '?' in the URL
  2053. }
  2054.  
  2055.  
  2056.  
  2057. // Function to fetch and return the full-size image URL (removing size, width, height, and format parameters)
  2058. function getFullSizeImageUrl(imageUrl) {
  2059. const cleanedUrl = imageUrl.replace(/[?&](size|width|height|format)=[^&]*/g, "");
  2060. return cleanedUrl.replace(/\?$/, ""); // Remove the '?' if it's left hanging after removing parameters
  2061. }
  2062.  
  2063.  
  2064. // Function to extract the image URL from the outerHTML
  2065. function extractImageUrlFromOuterHTML(image) {
  2066. logMinor('image outer html: ' + image.outerHTML);
  2067. const regex = /src=["']([^"']+)["']/; // Matches both " and ' around the URL
  2068. const match = image.outerHTML.match(regex); // Extract the URL
  2069.  
  2070. if (match) {
  2071. return match[1]; // Return the first captured group (the URL)
  2072. } else {
  2073. logError('Image source URL not found in outerHTML.');
  2074. return null;
  2075. }
  2076. }
  2077.  
  2078.  
  2079. // Function to fetch and add images to the ZIP file
  2080. function addImagesToZip(innerDiv, zip, imageMap, htmlContent) {
  2081. logMajor('Fetching and adding images to ZIP...')
  2082. let images;
  2083. if (audioEnabled) {
  2084. images = innerDiv.querySelectorAll('img, video, audio source');
  2085. } else {
  2086. images = innerDiv.querySelectorAll('img, video');
  2087. logMajor('Audio fetching disabled. Skipping.');
  2088. }
  2089.  
  2090. const imagePromises = []; // Track all fetch promises to ensure we wait for them
  2091. let imageIndex = 0;
  2092.  
  2093. images.forEach((image, index) => {
  2094. const imageUrl = image.src; // Use the original image URL without cleaning
  2095. // const imageUrlRaw = image.getAttribute('src');
  2096. let imageUrlRaw = extractImageUrlFromOuterHTML(image);
  2097.  
  2098. // If extraction fails, fallback to image.src
  2099. if (!imageUrlRaw) {
  2100. imageUrlRaw = imageUrl; // Fallback to image.src if extraction fails
  2101. }
  2102.  
  2103. logMinor("Image HTML:", image.outerHTML);
  2104. logMinor('imageUrl: ' + imageUrl + '; imageUrlRaw: ' + imageUrlRaw);
  2105.  
  2106. if (imageUrl && imageUrl.startsWith("http")) { // Ensure valid HTTP/HTTPS link
  2107. if (!imageMap.has(imageUrlRaw)) { // If this image has not been processed
  2108. logMajor(`Downloading image: ${imageUrl}`);
  2109.  
  2110. imageIndex += 1;
  2111.  
  2112. // Remove query parameters, but keep the size parameter
  2113. const urlWithoutParams = imageUrl.split('?')[0]; // Base URL without query parameters
  2114.  
  2115. // Match the "size", "width", and "height" parameters from the URL
  2116. const sizeParamMatch = imageUrl.match(/[?&]size=([^&]+)/);
  2117. const widthParamMatch = imageUrl.match(/[?&]width=([^&]+)/);
  2118. const heightParamMatch = imageUrl.match(/[?&]height=([^&]+)/);
  2119.  
  2120. // Extract the actual file name (e.g., image.jpg) without query params
  2121. const actualFileName = urlWithoutParams.split('/').pop();
  2122.  
  2123. // If the "size", "width", or "height" parameters exist, format them for the file name
  2124. const sizeParam = sizeParamMatch ? `_${sizeParamMatch[1]}` : '';
  2125. const widthParam = widthParamMatch ? `_${widthParamMatch[1]}` : '';
  2126. const heightParam = heightParamMatch ? `_${heightParamMatch[1]}` : '';
  2127.  
  2128. // Get the file extension (default to .jpg if not found)
  2129. const fileExtension = actualFileName.slice(actualFileName.lastIndexOf('.')) || '.jpg';
  2130.  
  2131. // Build the base name without the extension, or fallback to image + index if not found
  2132. const baseName = urlWithoutParams.split('/').pop().split('?')[0].split('.').slice(0, -1).join('.') || `image${index + 1}`;
  2133.  
  2134. // Construct the final file name
  2135. // const fileName = `images_and_media/${baseName}${sizeParam}${widthParam}${heightParam}${fileExtension || '.jpg'}`;
  2136.  
  2137. // imageMap.set(imageUrlRaw, fileName); // Map the URL to the file name
  2138.  
  2139. let fileName = `images_and_media/${baseName}${sizeParam}${widthParam}${heightParam}${fileExtension || ''}`;
  2140. const uniqueFileNameFound = getUniqueFileName(fileName);
  2141.  
  2142. // Check if the file path already exists as a value in the map
  2143. function getUniqueFileName(fileName) {
  2144. let uniqueFileName = fileName;
  2145. let counter = 1;
  2146.  
  2147. // Check if the file name exists as a value in the map
  2148. let filePathExists = false;
  2149. imageMap.forEach((value) => {
  2150. if (value === uniqueFileName) {
  2151. filePathExists = true;
  2152. }
  2153. });
  2154.  
  2155. // If the file path already exists, append a counter to make it unique
  2156. while (filePathExists) {
  2157. uniqueFileName = fileName.replace(/(\.\w+)$/, `_${counter}$1`);
  2158. counter++;
  2159.  
  2160. // Recheck the map with the new uniqueFileName
  2161. filePathExists = false;
  2162. imageMap.forEach((value) => {
  2163. if (value === uniqueFileName) {
  2164. filePathExists = true;
  2165. }
  2166. });
  2167. }
  2168.  
  2169. // Return the unique file name
  2170. return uniqueFileName;
  2171. }
  2172.  
  2173. // Now that we have a unique file name, add it to the map
  2174. imageMap.set(imageUrlRaw, uniqueFileNameFound);
  2175.  
  2176. let firstImageRetriesLeft = 3; // Set retry limit for this image
  2177.  
  2178. const fetchWithRetry = async () => {
  2179. try {
  2180. const response = await fetch(imageUrl);
  2181. if (!response.ok) {
  2182. throw new Error(`HTTP error! Status: ${response.status}`);
  2183. }
  2184. const blob = await response.blob();
  2185. logMajor(`Adding image to ZIP: ${uniqueFileNameFound}`);
  2186. zip.file(uniqueFileNameFound, blob); // Add the image to the ZIP
  2187. } catch (err) {
  2188. if (firstImageRetriesLeft > 0) {
  2189. firstImageRetriesLeft--; // Decrease retry count
  2190. logMajor(`Retrying fetch for ${imageUrl}... (${firstImageRetriesLeft} attempts left)`);
  2191. await new Promise(resolve => setTimeout(resolve, 2500)); // Wait 2.5 seconds
  2192. return fetchWithRetry(); // Retry
  2193. } else {
  2194. logError(`Failed to fetch image ${imageUrl}:`, err);
  2195. }
  2196. }
  2197. };
  2198.  
  2199. // Start the fetch process with retry
  2200. const imagePromise = fetchWithRetry();
  2201.  
  2202. imagePromises.push(imagePromise); // Add the promise to the tracker
  2203.  
  2204. // Now, check if we should fetch the full-size image
  2205. if (fetchFullSize) {
  2206. const fullSizeUrl = getFullSizeImageUrl(imageUrl);
  2207.  
  2208. if (fullSizeUrl !== imageUrl) { // If the full-size URL is different from the original
  2209. const fullSizeBaseName = fullSizeUrl.split('/').pop().split('?')[0];
  2210. const fullSizeFileName = `fullsize_images/${fullSizeBaseName}`;
  2211.  
  2212. logMajor(`Downloading full-size image: ${fullSizeUrl}`);
  2213.  
  2214. let fullImageRetriesLeft = 3; // Set retry limit for this image
  2215.  
  2216. const fetchWithRetry = () => {
  2217. return fetch(fullSizeUrl)
  2218. .then(response => {
  2219. if (!response.ok) {
  2220. logError(`HTTP error! Status: ${response.status}`);
  2221. return; // Exit the current function or promise chain to prevent further action
  2222. }
  2223. return response.blob();
  2224. })
  2225. .then(blob => {
  2226. logMajor(`Adding full-size image to ZIP: ${fullSizeFileName}`);
  2227. zip.file(fullSizeFileName, blob); // Add the full-size image to the ZIP
  2228.  
  2229. // Update HTML to point to the full-size file path
  2230. const imageSrcRegex = new RegExp(imageUrl.replace(/[.*+?^=!:${}()|\[\]\/\\]/g, "\\$&"), 'g');
  2231. // htmlContent = htmlContent.replace(imageSrcRegex, fullSizeFileName);
  2232. })
  2233. .catch(err => {
  2234. if (fullImageRetriesLeft > 0) {
  2235. fullImageRetriesLeft--; // Decrease retry count
  2236. logMajor(`Retrying fetch for ${fullSizeUrl}... (${fullImageRetriesLeft} attempts left)`);
  2237. return new Promise(resolve => setTimeout(resolve, 2500)) // Wait 5 seconds before retry
  2238. .then(fetchWithRetry);
  2239. } else {
  2240. logError(`Failed to fetch full-size image ${fullSizeUrl}:`, err);
  2241. }
  2242. });
  2243. };
  2244.  
  2245. const fullSizeImagePromise = fetchWithRetry(); // Start the fetch process with retry
  2246.  
  2247. imagePromises.push(fullSizeImagePromise); // Add the promise to the tracker
  2248. }
  2249. } else {
  2250. logMajor('Full-size image fetching disabled. Skipping.');
  2251. }
  2252.  
  2253. } else {
  2254. // If already processed, retrieve the stored filename
  2255. // const fileName = imageMap.get(imageUrl);
  2256.  
  2257. // Update HTML to point to the existing file path
  2258. // const imageSrcRegex = new RegExp(imageUrl.replace(/[.*+?^=!:${}()|\[\]\/\\]/g, "\\$&"), 'g');
  2259. // htmlContent = htmlContent.replace(imageSrcRegex, fileName);
  2260. // htmlContent = htmlContent.replace(imageUrl, fileName);
  2261. }
  2262. }
  2263. });
  2264.  
  2265. logMinor(imageMap);
  2266.  
  2267. // Wait for all image fetches to complete and return the modified HTML
  2268. return Promise.all(imagePromises).then(() => {
  2269. logMinor('All images processed. Returning modified HTML content.');
  2270. imageMap.forEach((fileName, originalUrl) => {
  2271. // Escape any special characters in the original URL for the regex
  2272. const escapedUrl = originalUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  2273. const urlRegex = new RegExp(escapedUrl, 'g'); // Create a global regex for the URL
  2274. logMinor('originalUrl: ' + originalUrl + '; escapedUrl: ' + escapedUrl);
  2275. htmlContent = htmlContent.replace(urlRegex, fileName);
  2276. logMinor('Replaced all instances of ' + originalUrl + ' with ' + fileName + '.');
  2277. });
  2278.  
  2279. return htmlContent;
  2280. });
  2281. }
  2282.  
  2283.  
  2284.  
  2285.  
  2286.  
  2287. // Function to add styles to the ZIP file
  2288. function addStylesToZip(innerDiv, zip) {
  2289. const pageTitle = document.title;
  2290. let cssFiles = []; // Array to hold file names for linking in HTML
  2291. logMajor('Adding styles to zip...');
  2292.  
  2293. // Fetch all external CSS files (from <link> tags in the document)
  2294. const linkStyles = document.querySelectorAll('link[rel="stylesheet"]');
  2295. linkStyles.forEach((link, index) => {
  2296. const href = link.href;
  2297. if (href && href.startsWith("http")) { // Make sure it's a valid URL
  2298. let styleRetriesLeft = 3; // Set retry limit for each link
  2299.  
  2300. const fetchWithRetry = () => {
  2301. logMinor(`Fetching external CSS from: ${href}`);
  2302. return fetch(href)
  2303. .then(response => response.text())
  2304. .then(cssContent => {
  2305. const fileName = `styles/style${index + 1}.css`;
  2306. logMajor(`Adding CSS file to ZIP: ${fileName}`);
  2307. zip.file(fileName, cssContent);
  2308. cssFiles.push(fileName); // Keep track of this file for later
  2309. })
  2310. .catch(err => {
  2311. if (styleRetriesLeft > 0) {
  2312. styleRetriesLeft--; // Decrease retry count
  2313. logMajor(`Retrying fetch for ${href}... (${styleRetriesLeft} attempts left)`);
  2314. return new Promise(resolve => setTimeout(resolve, 2500)) // Wait 5 seconds before retry
  2315. .then(fetchWithRetry);
  2316. } else {
  2317. logError("Failed to fetch external CSS:", err);
  2318. }
  2319. });
  2320. };
  2321.  
  2322. fetchWithRetry(); // Start the fetch process with retry
  2323. }
  2324. });
  2325.  
  2326. // Handle inline <style> tags inside the innerDiv
  2327. const inlineStyles = innerDiv.querySelectorAll('style');
  2328. inlineStyles.forEach((style, index) => {
  2329. const cssContent = style.innerHTML;
  2330. const fileName = `inline-style${index + 1}.css`;
  2331. logMinor(`Adding inline CSS to ZIP: ${fileName}`);
  2332. zip.file(fileName, cssContent);
  2333. cssFiles.push(fileName);
  2334. style.innerHTML = ''; // Clear the original inline style tag
  2335. const linkTag = document.createElement('link');
  2336. linkTag.rel = "stylesheet";
  2337. linkTag.href = fileName;
  2338. style.parentNode.insertBefore(linkTag, style);
  2339. logMinor(`Replaced <style> with <link> referencing ${fileName}`);
  2340. });
  2341.  
  2342. logMinor(`Added styles: ${cssFiles.join(', ')}`);
  2343. return cssFiles;
  2344. }
  2345.  
  2346. // Function to add the necessary body style
  2347. function addBodyStyle(htmlContent) {
  2348. const bodyStyle = `
  2349. <style>
  2350.  
  2351. body {
  2352. overflow: unset !important;
  2353. }
  2354.  
  2355. li::marker {
  2356. content: "" !important;
  2357. }
  2358.  
  2359. [class*="messageListItem"]:hover [class*="timestampVisibleOnHover"] {
  2360. opacity: 1;
  2361. }
  2362.  
  2363. [class*="spoilerMarkdownContent"][class*="hidden"] {
  2364. background: hsl(0, 0%, 50%);
  2365. border: 1px solid hsl(0, 0%, 25%);
  2366. }
  2367.  
  2368. </style>
  2369. `;
  2370.  
  2371. return bodyStyle + htmlContent;
  2372. }
  2373.  
  2374. function addUTFTag(htmlContent) {
  2375. const UTFTag = `<meta charset="UTF-8">
  2376. `;
  2377. return UTFTag + htmlContent;
  2378. }
  2379.  
  2380.  
  2381.  
  2382. function addAttachmentsToZip(innerDiv, zip, attachmentMap) {
  2383. if (!attachmentsEnabled) {
  2384. logMajor("Attachment fetching disabled. Skipping.");
  2385. return;
  2386. }
  2387. logMajor("Fetching and adding miscellaneous attachments to ZIP...");
  2388.  
  2389. // Select attachment links
  2390. const attachmentLinks = innerDiv.querySelectorAll('[class*="attachmentInner"] a[class*="fileNameLink"], a[class*="downloadSection"]');
  2391. const fetchPromises = [];
  2392.  
  2393. // Reuse the unique file name generator
  2394. function getUniqueFileName(fileName, fileMap, folder) {
  2395. let uniqueFileName = fileName;
  2396. let counter = 1;
  2397.  
  2398. while (Array.from(fileMap.values()).includes(`${folder}/${uniqueFileName}`)) {
  2399. uniqueFileName = fileName.replace(/(\.\w+)$/, `_${counter}$1`); // Add counter before extension
  2400. counter++;
  2401. }
  2402.  
  2403. return uniqueFileName;
  2404. }
  2405.  
  2406. attachmentLinks.forEach(link => {
  2407. let fileUrl = link.href;
  2408. let baseFileName = fileUrl.split('/').pop().split('?')[0]; // Strip arguments
  2409. let uniqueFileName = getUniqueFileName(baseFileName, attachmentMap, "attachments");
  2410.  
  2411. let fileRetriesLeft = 3; // Set retry limit
  2412.  
  2413. // Add fetch promise for each attachment with retry
  2414. fetchPromises.push(
  2415. (function fetchWithRetry() {
  2416. return fetch(fileUrl)
  2417. .then(response => {
  2418. if (!response.ok) {
  2419. logError(`Failed to fetch ${fileUrl}: ${response.statusText}`);
  2420. return; // Exit the current function or promise chain to prevent further action
  2421. }
  2422. return response.blob();
  2423. })
  2424. .then(blob => {
  2425. const filePath = `attachments/${uniqueFileName}`;
  2426. zip.file(filePath, blob); // Add file to ZIP
  2427. attachmentMap.set(fileUrl, filePath); // Map file URL to unique path
  2428. logMajor(`Attachment added: ${filePath}`);
  2429. })
  2430. .catch(err => {
  2431. // Check if the error is related to CORS
  2432.  
  2433. if (fileRetriesLeft > 0) {
  2434. fileRetriesLeft--; // Decrease retry count
  2435. logMajor(`Retrying fetch for ${fileUrl}... (${fileRetriesLeft} attempts left)`);
  2436. return new Promise(resolve => setTimeout(resolve, 2500)) // Wait 2.5 seconds before retry
  2437. .then(fetchWithRetry);
  2438. } else {
  2439. logError(`Failed to fetch attachment ${fileUrl} after multiple attempts.`, err);
  2440. }
  2441. });
  2442. })() // Immediately invoke the fetch function
  2443. );
  2444. });
  2445.  
  2446. return Promise.all(fetchPromises);
  2447. }
  2448.  
  2449. let removeOnScreenLog;
  2450.  
  2451. // Function to display logging info on screen
  2452. function onScreenLogging(argument1, message) {
  2453. const progressLogOverlay = document.getElementById('progress-log-overlay');
  2454.  
  2455. // Case 1: 'create' - Create the log overlay div and set the remove flag
  2456. if (argument1 === 'create') {
  2457. if (!progressLogOverlay) {
  2458. const newOverlay = document.createElement('div');
  2459. newOverlay.id = 'progress-log-overlay';
  2460. document.getElementById('chat-copy-outer').appendChild(newOverlay); // Add it inside #chat-copy-outer
  2461. }
  2462. removeOnScreenLog = false;
  2463. }
  2464.  
  2465. // Case 2: 'remove' - Remove the overlay div after a delay if conditions are met
  2466. if (argument1 === 'remove') {
  2467. removeOnScreenLog = true;
  2468. setTimeout(() => {
  2469. if (removeOnScreenLog) {
  2470. const overlay = document.getElementById('progress-log-overlay');
  2471. if (overlay) {
  2472. overlay.remove(); // Remove the progress log overlay
  2473. }
  2474. }
  2475. }, 16000); // 16-second delay
  2476. }
  2477.  
  2478. // Case 3: 'log' or 'error' - Add a log message inside the #progress-log-overlay
  2479. if ((argument1 === 'log' || argument1 === 'error') && progressLogOverlay) {
  2480. const lineOuter = document.createElement('span');
  2481. lineOuter.classList.add('line-outer');
  2482.  
  2483. const lineInner = document.createElement('span');
  2484. lineInner.classList.add('line-inner');
  2485.  
  2486. // If it's an error, add the .error class to the inner span
  2487. if (argument1 === 'error') {
  2488. lineInner.classList.add('error');
  2489. }
  2490.  
  2491. // Add the message to the line-inner span
  2492. lineInner.textContent = message;
  2493.  
  2494. // Append the line-inner to line-outer, and line-outer to the progress log
  2495. lineOuter.appendChild(lineInner);
  2496. progressLogOverlay.appendChild(lineOuter);
  2497. }
  2498. }
  2499.  
  2500.  
  2501. // Function to save the chat content
  2502. function saveChatContent(innerDiv) {
  2503. resetLog();
  2504. onScreenLogging('create');
  2505. const pageTitle = document.title;
  2506. const zip = new JSZip(); // Initialize a new JSZip instance
  2507. const imageMap = new Map(); // Map to store image URLs and their corresponding file names
  2508. const attachmentMap = new Map();
  2509. logMajor('Initializing ZIP file creation...');
  2510.  
  2511. innerDiv.querySelectorAll('video').forEach(video => {
  2512. video.setAttribute('autoplay', '');
  2513. video.setAttribute('muted', '');
  2514. video.setAttribute('loop', '');
  2515. });
  2516.  
  2517. // Add HTML file to the ZIP
  2518. let htmlContent = innerDiv.innerHTML;
  2519. // console.log('Adding HTML content to ZIP...');
  2520.  
  2521. const cssFiles = addStylesToZip(innerDiv, zip); // Add styles and get CSS file names
  2522.  
  2523. // Handle images and other assets (if any)
  2524. // logMajor('Fetching and adding images and attachments to ZIP...');
  2525. addImagesToZip(innerDiv, zip, imageMap, htmlContent).then(modifiedHtmlContent => {
  2526. logMajor('All images added to ZIP.');
  2527.  
  2528.  
  2529. // Fetch and add attachments
  2530. addAttachmentsToZip(innerDiv, zip, attachmentMap).then(() => {
  2531. logMajor("All attachments added to ZIP.");
  2532.  
  2533. // Modify HTML content to link to the CSS files in the ZIP
  2534. let htmlWithStyles = modifiedHtmlContent;
  2535. cssFiles.forEach((cssFile) => {
  2536. const linkTag = `<link rel="stylesheet" href="${cssFile}">`;
  2537. htmlWithStyles = linkTag + htmlWithStyles; // Prepend <link> tags to the HTML content
  2538. });
  2539.  
  2540. // Add the body style and prepare the final HTML content
  2541. htmlWithStyles = addBodyStyle(htmlWithStyles);
  2542.  
  2543. htmlWithStyles = addUTFTag(htmlWithStyles);
  2544.  
  2545.  
  2546.  
  2547. // Create and embed the JavaScript file
  2548. const scriptContent = `
  2549. document.querySelectorAll('[class*="spoilerContent"]').forEach(spoiler => {
  2550. const clickHandler = function () {
  2551. // Log the clicked element for debugging
  2552. console.log("Spoiler clicked:", spoiler);
  2553.  
  2554. // Remove all classes from the clicked item that start with "hidden"
  2555. Array.from(spoiler.classList)
  2556. .filter(className => className.startsWith("hidden"))
  2557. .forEach(hiddenClass => spoiler.classList.remove(hiddenClass));
  2558.  
  2559. // Check if the clicked item has children
  2560. if (spoiler.children.length > 0) {
  2561. Array.from(spoiler.querySelectorAll('[class*="spoilerWarning"]')).forEach(child => {
  2562. // Remove elements with a class matching [class*="spoilerWarning"]
  2563. child.remove();
  2564. });
  2565.  
  2566. Array.from(spoiler.querySelectorAll('[class*="hidden"]')).forEach(child => {
  2567. // Remove "hidden" classes from all matching descendants
  2568. Array.from(child.classList)
  2569. .filter(className => className.startsWith("hidden"))
  2570. .forEach(hiddenClass => child.classList.remove(hiddenClass));
  2571. });
  2572.  
  2573. }
  2574.  
  2575. // Remove the event listener to prevent repeated triggering
  2576. spoiler.removeEventListener("click", clickHandler);
  2577. };
  2578.  
  2579. // Attach the click event listener to the spoiler element
  2580. spoiler.addEventListener("click", clickHandler);
  2581. });
  2582. `;
  2583.  
  2584. // Specify the folder path in the ZIP
  2585. zip.file("scripts/spoilerListener.js", scriptContent);
  2586.  
  2587. // Add <script> tag pointing to the folder
  2588. htmlWithStyles += `<script src="scripts/spoilerListener.js"></script>`;
  2589.  
  2590.  
  2591.  
  2592. // Beautify the content (assuming it's HTML or text-based inside the ZIP)
  2593. const beautifiedContent = html_beautify(htmlWithStyles, {
  2594. indent_size: 2
  2595. });
  2596.  
  2597. // Save modified HTML to ZIP
  2598. logMajor('Saving modified HTML to ZIP...');
  2599. const modifiedFileName = `${pageTitle.replace(/^[•\s]+/, '').replace(/[\\\/:*?"<>|]/g, '_')}`; // Remove bullet and invalid chars
  2600. zip.file(modifiedFileName + ".html", beautifiedContent); // Save the page content as an HTML file
  2601.  
  2602. logMinor(outputLog);
  2603. logMajor('Saving log to ZIP...');
  2604. zip.file("log.txt", outputLog);
  2605. logMinor('Logs have been added to zip.');
  2606.  
  2607. // Generate the zip file and trigger download
  2608. logMajor('Generating ZIP file...');
  2609. zip.generateAsync({
  2610. type: "blob"
  2611. })
  2612. .then(content => {
  2613. logMajor('ZIP file created successfully.');
  2614. logMinor(outputLog);
  2615.  
  2616.  
  2617.  
  2618.  
  2619. const link = document.createElement("a");
  2620. link.href = URL.createObjectURL(content);
  2621. link.download = `${modifiedFileName}.zip`; // Set the modified filename
  2622. link.click(); // Trigger the download
  2623. resetLog();
  2624. onScreenLogging('remove');
  2625. })
  2626. .catch(err => {
  2627. logError("Error creating ZIP file:", err);
  2628. });
  2629. }).catch(err => {
  2630. logError("Error adding attachments to ZIP:", err);
  2631. });
  2632. }).catch(err => {
  2633. logError("Error adding images to ZIP:", err);
  2634. });
  2635. }
  2636.  
  2637.  
  2638.  
  2639.  
  2640. function buttonPlacer() {
  2641. // 1. Detect chats
  2642. const chats = document.querySelectorAll(
  2643. 'main[class*="chatContent"] ol[class*="scrollerInner"], section[class*="chatContent"] ol[class*="scrollerInner"]'
  2644. );
  2645.  
  2646. if (buttonGenerationLocation == 'show-button-corner') {
  2647. chats.forEach(chat => {
  2648. if (!foundChats.has(chat)) {
  2649. foundChats.add(chat);
  2650.  
  2651. handleChatFound(chat);
  2652.  
  2653. } else {
  2654. logMinor('Already found chat.');
  2655. }
  2656. });
  2657. }
  2658.  
  2659. // 2. Detect private channels
  2660. const privateChannels = document.querySelectorAll('nav[class*="privateChannels"]');
  2661. if (buttonGenerationLocation == 'hide-button-dm') {
  2662. privateChannels.forEach(nav => handlePrivateChannelsFound(nav));
  2663. }
  2664.  
  2665. // 3. Detect footer
  2666. const footers = document.querySelectorAll('nav ul [class*="footer"]');
  2667. if (buttonGenerationLocation == 'hide-button-servers') {
  2668. footers.forEach(footer => handleFooterFound(footer));
  2669. }
  2670.  
  2671. // 4. Detect sidebar region
  2672. const sidebars = document.querySelectorAll('[class*="sidebarRegion"] nav[class*="sidebar"]');
  2673. if (buttonGenerationLocation == 'hide-button-settings') {
  2674. sidebars.forEach(sidebar => handleSidebarFound(sidebar));
  2675. }
  2676. }
  2677.  
  2678.  
  2679.  
  2680. // Debounced mutation observer callback
  2681. const mutationCallback = (mutations) => {
  2682. if (debounceTimeout) clearTimeout(debounceTimeout);
  2683.  
  2684. debounceTimeout = setTimeout(() => {
  2685. buttonPlacer();
  2686.  
  2687. }, 100);
  2688. };
  2689.  
  2690.  
  2691.  
  2692. // Function to handle chat detection
  2693. function handleChatFound(chat) {
  2694. const presentChat = document.querySelector(
  2695. 'main[class*="chatContent"] ol[class*="scrollerInner"], section[class*="chatContent"] ol[class*="scrollerInner"]'
  2696. );
  2697. if (!presentChat) {
  2698. logMinor('No chat found.');
  2699. return;
  2700. } else if (!chat) {
  2701. chat = presentChat;
  2702. }
  2703.  
  2704. logMinor('New chat found:', chat);
  2705.  
  2706. const messagesWrapper = chat.closest('div[class*="messagesWrapper"]');
  2707. if (messagesWrapper && !messagesWrapper.querySelector('#copy-button-outer')) {
  2708. const outerDiv = document.createElement('div');
  2709. outerDiv.id = 'copy-button-outer';
  2710.  
  2711. const innerDiv = document.createElement('div');
  2712. innerDiv.id = 'copy-button-inner';
  2713.  
  2714. const span = document.createElement('span');
  2715. span.innerHTML = 'Copy<br>Chat';
  2716.  
  2717. innerDiv.appendChild(span);
  2718. outerDiv.appendChild(innerDiv);
  2719. messagesWrapper.appendChild(outerDiv);
  2720.  
  2721. // Add click listener to copy button
  2722. innerDiv.addEventListener('click', () => createChatCopyUI(chat));
  2723.  
  2724. logMinor('Copy button added to:', messagesWrapper);
  2725. }
  2726. buttonCleanup();
  2727. }
  2728.  
  2729. // Function to handle private channels detection
  2730. function handlePrivateChannelsFound(nav) {
  2731. logMinor('Private channel found:', nav);
  2732. const privateChannelsHeader = nav.querySelector('[class*="privateChannelsHeaderContainer"]');
  2733. const showButtonButton = document.createElement('div');
  2734. showButtonButton.id = 'direct-messages-SBB';
  2735. const sBBSpan = document.createElement('span');
  2736. sBBSpan.textContent = 'Show Copy button';
  2737. showButtonButton.appendChild(sBBSpan);
  2738. const existingButton = nav.querySelector('[id="direct-messages-SBB"]');
  2739. if (!existingButton) {
  2740. if (privateChannelsHeader) {
  2741. privateChannelsHeader.parentNode.insertBefore(showButtonButton, privateChannelsHeader);
  2742. attachShowButtonListener(showButtonButton);
  2743. adjustButtonMargin(showButtonButton);
  2744. } else {
  2745. nav.prepend(showButtonButton);
  2746. showButtonButton.classList.add('fallback-position');
  2747. attachShowButtonListener(showButtonButton);
  2748. adjustButtonMargin(showButtonButton);
  2749. }
  2750. } else if (existingButton.classList.contains('fallback-position')) {
  2751. if (privateChannelsHeader) {
  2752. existingButton.remove();
  2753. privateChannelsHeader.parentNode.insertBefore(showButtonButton, privateChannelsHeader);
  2754. attachShowButtonListener(showButtonButton);
  2755. adjustButtonMargin(showButtonButton);
  2756. }
  2757. } else {
  2758. // Check if the button is immediately before the privateChannelsHeader
  2759. if (privateChannelsHeader && existingButton.nextElementSibling !== privateChannelsHeader) {
  2760. privateChannelsHeader.parentNode.insertBefore(existingButton, privateChannelsHeader);
  2761. adjustButtonMargin(existingButton);
  2762. }
  2763. }
  2764.  
  2765. // Function to adjust button margin based on left padding of privateChannelsHeader
  2766. function adjustButtonMargin(button) {
  2767. const paddingLeft = window.getComputedStyle(privateChannelsHeader).getPropertyValue('padding-left');
  2768. button.style.marginLeft = paddingLeft; // Apply paddingLeft as margin-left for button
  2769. }
  2770.  
  2771. buttonCleanup();
  2772. }
  2773.  
  2774. // Function to handle footer detection
  2775. function handleFooterFound(footer) {
  2776. logMinor('Footer found:', footer);
  2777. const showButtonButton = document.createElement('div');
  2778. showButtonButton.id = 'servers-footer-SBB';
  2779. const sBBSpan = document.createElement('span');
  2780. sBBSpan.textContent = 'Show Copy button';
  2781. showButtonButton.appendChild(sBBSpan);
  2782. const existingButton = footer.querySelector('[id="servers-footer-SBB"]');
  2783. if (!existingButton) {
  2784. const listItemWrapper = footer.querySelector('[class*="listItemWrapper"]');
  2785.  
  2786. if (listItemWrapper) {
  2787. // Get the left position of the listItemWrapper relative to the viewport
  2788. // const listItemWrapperPosition = listItemWrapper.getBoundingClientRect().left;
  2789.  
  2790. // Get the width of the window
  2791. // const windowWidth = window.innerWidth;
  2792.  
  2793. // Calculate the marginLeft by subtracting the element's left position from the window's width
  2794. // const marginLeft = windowWidth - listItemWrapperPosition - listItemWrapper.offsetWidth; // Subtract element width to get the remaining space
  2795.  
  2796. // Apply this marginLeft value to align the button correctly
  2797. // showButtonButton.style.marginLeft = `${marginLeft}px`;
  2798. }
  2799.  
  2800. footer.prepend(showButtonButton);
  2801. attachShowButtonListener(showButtonButton);
  2802. }
  2803. buttonCleanup();
  2804. }
  2805.  
  2806. // Function to handle sidebar region detection
  2807. function handleSidebarFound(sidebar) {
  2808. logMinor('Sidebar found:', sidebar);
  2809. const firstSeparator = sidebar.querySelector('[class*="separator"]');
  2810. const showButtonButton = document.createElement('div');
  2811. showButtonButton.id = 'settings-SBB';
  2812. const sBBSpan = document.createElement('span');
  2813. sBBSpan.textContent = 'Show Copy button';
  2814. showButtonButton.appendChild(sBBSpan);
  2815. const existingButton = sidebar.querySelector('[id="settings-SBB"]');
  2816. if (!existingButton) {
  2817. if (firstSeparator) {
  2818. firstSeparator.parentNode.insertBefore(showButtonButton, firstSeparator);
  2819. const marginLeft = window.getComputedStyle(firstSeparator).getPropertyValue('margin-left');
  2820. showButtonButton.style.marginLeft = marginLeft;
  2821. attachShowButtonListener(showButtonButton);
  2822. } else {
  2823. sidebar.prepend(showButtonButton);
  2824. showButtonButton.classList.add('fallback-position');
  2825. attachShowButtonListener(showButtonButton);
  2826. }
  2827. } else if (existingButton.classList.contains('fallback-position')) {
  2828. if (firstSeparator) {
  2829. existingButton.remove();
  2830. firstSeparator.parentNode.insertBefore(showButtonButton, firstSeparator);
  2831. const marginLeft = window.getComputedStyle(firstSeparator).getPropertyValue('margin-left');
  2832. showButtonButton.style.marginLeft = marginLeft;
  2833. attachShowButtonListener(showButtonButton);
  2834. }
  2835. }
  2836. buttonCleanup();
  2837. }
  2838.  
  2839. // Function to attach click listener to the button
  2840. function attachShowButtonListener(showButtonButton) {
  2841. showButtonButton.addEventListener('click', () => {
  2842. buttonGenerationLocation = 'show-button-corner';
  2843. saveButtonLocation(buttonGenerationLocation);
  2844. handleChatFound(); // Call the handleChatFound function when the button is clicked
  2845. showButtonButton.remove();
  2846. });
  2847. }
  2848.  
  2849. // Function declaration to save only the buttonLocation
  2850. function saveButtonLocation(argument) {
  2851. const savedSettings = JSON.parse(localStorage.getItem('chatCopySettings')) || {}; // Get existing settings or default to an empty object
  2852. const settings = {
  2853. ...savedSettings, // Keep the previous settings
  2854. buttonLocation: argument, // Set new buttonLocation
  2855. };
  2856.  
  2857. console.log(settings); // Log updated settings
  2858. localStorage.setItem('chatCopySettings', JSON.stringify(settings)); // Save to localStorage
  2859. }
  2860.  
  2861. function buttonCleanup() {
  2862. logMinor('Button cleanup...');
  2863. const dmButton = document.querySelector('#direct-messages-SBB');
  2864. const serverFooterButton = document.querySelector('#servers-footer-SBB');
  2865. const settingsButton = document.querySelector('#settings-SBB');
  2866. // if(buttonGenerationLocation != 'show-button-corner') {
  2867.  
  2868. // }
  2869.  
  2870. if ((buttonGenerationLocation != 'hide-button-dm') && dmButton) {
  2871. dmButton.remove();
  2872. }
  2873.  
  2874. if ((buttonGenerationLocation != 'hide-button-servers') && serverFooterButton) {
  2875. serverFooterButton.remove();
  2876. }
  2877.  
  2878. if ((buttonGenerationLocation != 'hide-button-settings') && settingsButton) {
  2879. settingsButton.remove();
  2880. }
  2881. }
  2882.  
  2883. // Start observing
  2884. const observer = new MutationObserver(mutationCallback);
  2885. observer.observe(document.body, {
  2886. childList: true,
  2887. subtree: true
  2888. });
  2889. })();