- // ==UserScript==
- // @name Jupiter's Discord Chat Saver
- // @description A free, full-featured chat saver for Discord.
- // @namespace Violentmonkey Scripts
- // @match https://discord.com/channels/*
- // @grant none
- // @version 1.4
- // @author Jupiter Liar
- // @description 01/04/2024, 11:40 AM
- // @license CC BY-SA
- // @grant GM_download
- // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
- // @require https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.9/beautify-html.min.js
- // ==/UserScript==
-
- (function () {
- 'use strict';
-
- const logInitial = 'Here is a record of the download process.\n' +
- 'Certain kinds of errors cannot be overcome, or be detected by this script.\n' +
- 'Files which failed to download can be downloaded manually.\n';
- let outputLog = logInitial;
-
-
-
- function resetLog() {
- outputLog = logInitial;
- }
-
- let showMinorLogs = false;
- let showMajorLogs = true;
-
-
- // Declare variables for settings
- let attachmentsEnabled = true;
- let audioEnabled = true;
- let fetchFullSize = true;
- let buttonGenerationLocation = 'show-button-corner'; // Default to 'show-button-corner'
- let stopScrollingVar = false;
-
- // Load settings from localStorage and apply them
- const loadSettings = () => {
- const savedSettings = JSON.parse(localStorage.getItem('chatCopySettings'));
- console.log(savedSettings);
-
- if (savedSettings) {
- // Apply saved settings to variables
- attachmentsEnabled = savedSettings.enableAttachments;
- audioEnabled = savedSettings.enableAudio;
- fetchFullSize = savedSettings.enableFullSizeImages;
- buttonGenerationLocation = savedSettings.buttonLocation;
- showMinorLogs = savedSettings.enableLogMinor;
- } else {
- // Set default settings if no saved data exists
- attachmentsEnabled = true;
- audioEnabled = true;
- fetchFullSize = true;
- buttonGenerationLocation = 'show-button-corner';
- showMinorLogs = false;
- }
- };
-
- // Call loadSettings to initialize values when the script loads
- loadSettings();
-
-
- const instructions = `
- <h1><span>How does this thing work?</span></h1>
-
- <p>This may be a little different from other chat savers out there, so pay attention.</p>
-
- <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>
-
- <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>
-
- <h2>How much of the chat will I save?</h2>
-
- <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>
-
- <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>
-
- <p><strong>NOTE: Some resources cannot be fetched. Do an internet search for "CORS", and you will understand why.</strong></p>
-
- <p>Once everything that can be fetched has been fetched, your browser will download the chat as a ZIP. The zip will contain:</p>
-
- <ul>
- <li>an HTML file</li>
- <li>the images in the chat, including any animations</li>
- <li>audio (if enabled)</li>
- <li>the full-size versions of images (if enabled)</li>
- <li>the styles that determine how the chat looks</li>
- <li>attachments (if enabled)</li>
- <li>a log of the download process</li>
- </ul>
-
- <p>As said above, some files may not download, for various reasons. After downloading, check to make sure you got everything you wanted.</p>
-
- <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>
-
- <h2>How does <strong>Autoscroll</strong> work?</h2>
-
- <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>
-
- <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>
-
- <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>
-
- <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>
-
- <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>
-
- <h2>How many messages can I save for free?</h2>
-
- <p><strong>All</strong> of them.</p>
-
- <h2>Is there a premium version with more features?</h2>
-
- <p>No. All the features I could make, I included in this version, for free.</p>
-
- <h2>Can I support your work with a donation?</h2>
-
- <p>You can reach my Buy Me a Coffee page through my <a href="https://linktr.ee/jupiterliar">Linktree.</a></p>
- `;
-
- function logMinor(message) {
- if (showMinorLogs) {
- console.log(message); // No need to record these.
- }
- }
-
- function logMajor(message) {
- if (showMajorLogs) {
- console.log(message);
- outputLog += `\n${message}`;
- onScreenLogging('log', message);
- }
- }
-
- function logError(message) {
- const errorMessage = 'ERROR: ' + message;
- console.error(errorMessage);
- outputLog += `\n\n${errorMessage}\n`;
- onScreenLogging('error', errorMessage);
- }
-
- // Add styles
- const style = document.createElement('style');
- style.id = "discord-chat-saver-styles";
- style.textContent = `
- :root {
- --dcs-box-shadow: .1em .1em .2em inset hsla(0, 0%, 100%, .35),
- -.1em -.1em .2em inset hsla(0, 0%, 0%, .5);
- --dcs-opposite-box-shadow: .1em .1em .1em inset hsla(0, 0%, 0%, .25),
- -.1em -.1em .1em inset hsla(0, 0%, 100%, .175);
- /* --dcs-blue: hsl(235, 85%, 65%); */
- --dcs-hs: 235, 100%;
- --dcs-blue: hsl(var(--dcs-hs), 60%);
- --dcs-drop-shadow: drop-shadow(.1em .1em .1em black);
- --dcs-yellow: hsl(60, 100%, 35%);
- --dcs-box-shadow-2: .2em .2em .4em inset hsla(0, 0%, 100%, .35),
- -.2em -.2em .4em inset hsla(0, 0%, 0%, .5);
- --scale-factor: 1;
- }
-
- @media (max-height: 600px) {
- :root {
- --scale-factor: 0.75;
- }
- }
-
- #copy-button-outer {
- margin: .5em 1.5em auto auto;
- position: sticky;
- width: fit-content;
- height: 0;
- z-index: 1;
- font-weight: 700;
- }
-
- #copy-button-inner {
- padding: 1em;
- background: var(--dcs-blue);
- border-radius: 50%;
- position: sticky;
- color: white;
- filter: var(--dcs-drop-shadow);
- box-shadow: var(--dcs-box-shadow);
- cursor: pointer;
- aspect-ratio: 1;
- display: flex;
- align-items: center;
- }
-
- #copy-button-inner span {
- filter: inherit;
- display: block;
- text-align: center;
- }
-
- #chat-copy-outer {
- position: fixed;
- z-index: 101;
- background: var(--bg-overlay-chat, var(--background-primary));
- border-radius: 1.5em;
- width: 33.3vw;
- max-height: calc(100vh - 8em);
- overflow: hidden;
- display: flex;
- flex-direction: column;
- box-shadow: var(--dcs-box-shadow);
- background: #DDD;
- filter: var(--dcs-drop-shadow);
- font-weight: 500;
- color: var(--text-normal);
- min-height: 17em;
- }
-
- #chat-copy-outer .drag-bar {
- cursor: move;
- background: var(--dcs-blue);
- padding: 0.5em;
- border-radius: 1em 1em 0 0;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- box-shadow: var(--dcs-box-shadow);
- }
-
- #chat-copy-outer .drag-bar span {
- font-size: 16px;
- font-weight: bold;
- color: white;
- z-index: 1; /* Ensure the text is on top of the bars */
- display: flex;
- align-items: center;
- width: 100%;
- filter: var(--dcs-drop-shadow);
- font-family: Arial;
- }
-
- /* Left and right bars before and after the text using pseudo-elements */
- #chat-copy-outer .drag-bar span::before,
- #chat-copy-outer .drag-bar span::after {
- content: '';
- flex-grow: 1;
- background-color: #DDD;
- margin: 0 0.5em;
- min-height: 1px;
- box-shadow: 0 .25em #DDD, 0 -.25em #DDD;
- }
-
- /* Left bar (before the text) */
- #chat-copy-outer .drag-bar span::before {
- flex-basis: 1em;
- }
-
- /* Right bar (after the text) */
- #chat-copy-outer .drag-bar span::after {
- margin-right: 2em;
- }
-
- #chat-copy-outer .close-box {
- position: absolute;
- /* top: 0.5em; */
- right: 0.5em;
- cursor: pointer;
- /* color: red; */
- font-weight: bold;
- z-index: 5;
- font-size: 2em;
- }
-
- #chat-copy-inner {
- flex: 1;
- overflow: auto;
- padding: 1em;
- /* border: 1px solid rgba(255, 255, 255, 0.2); */
- margin: 0 .2em;
- background: var(--bg-overlay-chat, var(--background-primary));
- box-shadow: var(--dcs-opposite-box-shadow);
- }
-
- #chat-copy-outer .resize-handle {
- width: 1.5em;
- height: 1.5em;
- position: absolute;
- right: 0;
- bottom: 0;
- cursor: se-resize;
- background: var(--dcs-blue);
- border-radius: 0 0 1em 0;
- z-index: 1;
- box-shadow: var(--dcs-box-shadow);
- overflow: hidden;
- }
-
- #chat-copy-outer .resize-handle::before {
- z-index: 1;
- content: '';
- position: absolute;
- width: 100%;
- height: 100%;
- box-shadow: var(--dcs-box-shadow), inset 2px 2px var(--dcs-blue),
- inset -3px -3px var(--dcs-blue);
- }
-
- /* Add diagonal lines to the resize handle */
- #chat-copy-outer .resize-handle::after {
- content: '';
- flex-grow: 1;
- background-color: #DDD;
- min-height: 1px;
- box-shadow: 0 .3em #DDD, 0 -.3em #DDD;
- width: 200%;
- position: absolute;
- transform: rotate(-45deg);
- filter: var(--dcs-drop-shadow);
- left: -50%;
- top: 20%;
- }
-
- #chat-copy-inner li::marker {
- content: '';
- }
-
- /* Recording bar styling */
- .recording-bar {
- background-color: #444;
- color: #fff;
- text-align: center;
- padding: 5px;
- font-size: 14px;
- font-family: Arial, sans-serif;
- position: relative;
- padding-right: calc(5px + 1em);
- box-shadow: var(--dcs-box-shadow);
- }
-
- .recording-bar span::after {
- content: '...';
- position: absolute;
- animation: recording-dots 1.5s steps(4, end) infinite;
- }
-
- /* Animation for the dots */
- @keyframes recording-dots {
- 0% {
- content: '';
- }
- 33% {
- content: '.';
- }
- 66% {
- content: '..';
- }
- 100% {
- content: '...';
- }
- }
-
- .chat-copy-big-button {
- position: absolute;
- border-radius: 50%;
- z-index: 1;
- color: white;
- padding: 0.8em;
- aspect-ratio: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: var(--dcs-box-shadow);
- filter: drop-shadow(1px 1px 1px black);
- cursor: pointer;
- scale: var(--scale-factor);
- }
-
- .chat-copy-big-button span {
- filter: var(--dcs-drop-shadow);
- font-weight: 700;
- }
-
- #chat-copy-save-button {
-
- background: hsl(150, 100%, 35%);
-
- right: calc(2em * var(--scale-factor));
- bottom: calc(2em * var(--scale-factor));
- font-family: Arial;
-
- }
-
- #chat-copy-config-button {
- top: calc(3em * var(--scale-factor));
- left: calc(1em * var(--scale-factor));
- background: hsl(00, 100%, 50%);
- }
-
- #chat-copy-config-button span:not(.save-size-span) {
- font-size: 2.5em;
- position: absolute;
- }
-
- #chat-copy-instruction-button {
- top: calc(3em * var(--scale-factor));
- right: calc(2em * var(--scale-factor));
- background: hsl(330, 100%, 50%);
- }
-
- #chat-copy-instruction-button span:not(.save-size-span) {
- font-size: 1.75em;
- position: absolute;
- }
-
-
- #chat-copy-instruction-button span {
- filter: brightness(8) var(--dcs-drop-shadow);
- }
-
- .save-size-span {
- opacity: 0;
- }
-
- div#progress-log-overlay {
- pointer-events: none;
- height: 32em;
- max-height: 32em;
- width: 100%;
- position: absolute;
- z-index: 9;
- margin-bottom: 8em;
- bottom: 0;
- padding: 1em 2em;
- box-sizing: border-box;
- mask-image: linear-gradient(to top, rgba(0, 0, 0, 1) 75%, rgba(0, 0, 0, 0) 100%);
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- filter: drop-shadow(.5em 0 0 white) drop-shadow(-.5em 0 0 white);
- }
-
- div#progress-log-overlay span.line-outer {
- text-align: center;
- line-height: normal;
- font-size: .95em;
- }
-
- div#progress-log-overlay span.line-inner {
- background: white;
- color: black;
- animation: fadeOut 10s forwards;
- word-break: break-word;
- font-size: inherit;
- }
-
- div#progress-log-overlay span.line-inner.error {
- color: red;
- }
-
- @keyframes fadeOut {
- 0% {
- opacity: 1;
- }
- 25% {
- opacity: 1;
- }
- 100% {
- opacity: 0;
- }
- }
-
- #chat-copy-settings-outer, #chat-copy-instruction-outer {
- background: var(--bg-overlay-chat, var(--background-primary));
- box-shadow: var(--dcs-opposite-box-shadow);
- margin: 0 .2em;
- padding: 1em;
- overflow: auto;
- flex: 1;
- max-height: calc(100vh - 16em);
- z-index: 2;
- min-height: 12em;
- display: flex;
- }
-
- #chat-copy-settings {
-
-
-
- /* border: 1px solid rgba(255, 255, 255, 0.2); */
-
-
- width: fit-content;
- margin: auto;
- }
-
- #chat-copy-settings .wrapper {
- margin-left: 1em;
- }
-
- #chat-copy-settings .wrapper .wrapper-inner {
- display: inline;
- }
-
- #chat-copy-settings .wrapper .wrapper-inner * {
- line-height: normal;
- }
-
- #chat-copy-settings h2 {
- font-size: 1.5em;
- font-weight: bold;
- margin-bottom: 0.65em;
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- color: white;
- }
-
- #chat-copy-settings h2 span {
- margin-right: auto;
- filter: var(--dcs-drop-shadow);
- }
-
- #chat-copy-settings h2::before {
- content: "";
- position: absolute;
- background: linear-gradient(to right, hsl(00, 100%, 40%), transparent);
- z-index: -1;
- width: 100%;
- height: 100%;
- --bottom-extra: 0.1em;
- padding: .25em 0.5em calc(0.25em + var(--bottom-extra));
- margin-top: var(--bottom-extra);
- }
-
- #chat-copy-settings h3 {
- margin-bottom: .5em;
- font-weight: 600;
- font-size: 1.1em;
- }
-
- #chat-copy-settings p {
- margin: .5em 0;
- max-width: 20em;
- }
-
- #chat-copy-settings .divider {
- height: 1px;
- background: black;
- margin: 1em 0;
- }
-
- #chat-copy-settings #close-config-button {
- display: block;
- margin-top: 1em;
- margin-left: auto;
- font-size: 1rem;
- font-weight: 600;
- padding: .5em 1em;
- background: var(--dcs-blue);
- color: white;
- box-shadow: var(--dcs-box-shadow);
- border-radius: 1em;
- }
-
- #chat-copy-settings #close-config-button span {
- filter: var(--dcs-drop-shadow);
- }
-
- #clear-settings-button {
- display: none;
- }
-
- #direct-messages-SBB, #servers-footer-SBB, #settings-SBB {
- background: var(--dcs-blue);
- color: white;
- text-align: center;
- box-shadow: var(--dcs-box-shadow);
-
- }
-
- #direct-messages-SBB span, #servers-footer-SBB span, #settings-SBB span {
- filter: var(--dcs-drop-shadow);
-
- }
-
- #direct-messages-SBB {
- width: fit-content;
- line-height: normal;
- padding: .5em 1em;
- box-shadow: var(--dcs-box-shadow-2);
- margin-top: .25em;
- border-radius: 2em;
- }
-
- #servers-footer-SBB {
- /* margin-left: 12px; */
- padding: .5em;
- border-radius: 1em;
- margin-right: 1px;
- margin-bottom: .33em;
- box-shadow: var(--dcs-box-shadow-2);
- }
-
- #servers-footer-SBB span {
-
- }
-
- #settings-SBB {
- line-height: normal;
- width: fit-content;
- padding: .5em 1em;
- border-radius: 2em;
- translate: -.5em 0;
-
- }
-
-
- @keyframes moveAndFadeOut {
- 0% {
- transform: translate(0, 0);
- opacity: 1;
- }
- 25% {
- opacity: 0.5;
- }
- 50% {
- opacity: 0;
- }
- 100% {
- transform: translate(-100vw, 100vh); /* Moving to bottom left off-screen */
- opacity: 0;
- visibility: hidden; /* Hides the element completely after animation */
- }
- }
-
- #copy-button-outer.move-fade-out {
- animation: moveAndFadeOut 2s ease-out forwards; /* Adjust duration as needed */
- }
-
- #chat-copy-instruction {
- margin-bottom: 1em;
- height: fit-content;
- }
-
- #chat-copy-instruction h1 {
- font-size: 1.5em;
- font-weight: bold;
- line-height: 1.15;
- display: flex;
- justify-content: center;
- align-items: center;
- position: relative;
- color: white;
- margin-bottom: 0.65em;
- }
-
- #chat-copy-instruction h1::before {
- content: '';
- z-index: -1;
- width: 100%;
- height: 100%;
- --bottom-extra: 0.1em;
- padding: .25em 0.5em calc(0.25em + var(--bottom-extra));
- margin-top: var(--bottom-extra);
- background: linear-gradient(to right, hsl(330, 100%, 40%), hsla(330, 100%, 40%, 0.5));
- position: absolute;
- }
-
- #chat-copy-instruction h1 span {
- margin-right: auto;
- filter: var(--dcs-drop-shadow);
- }
-
- #chat-copy-instruction h2 {
- font-size: 1.2em;
- font-weight: bold;
- }
-
- #chat-copy-instruction ul {
- list-style-type: disc; /* Ensures bullets are shown */
- padding-left: 20px; /* Optional: Adds indentation to the list */
- }
-
- #chat-copy-instruction p {
- line-height: 1.15em;
- }
-
- #chat-copy-instruction #close-instruction-button {
- position: absolute;
- display: block;
- margin-top: 1em;
- margin-left: auto;
- font-size: 1rem;
- font-weight: 600;
- padding: .5em 1em;
- background: var(--dcs-blue);
- color: white;
- box-shadow: var(--dcs-box-shadow);
- border-radius: 1em;
- bottom: 2em;
- right: 2em;
- }
-
- #chat-copy-instruction #close-instruction-button span {
- filter: var(--dcs-drop-shadow);
- }
-
- #chat-copy-instruction strong {
- font-weight: bold;
- }
-
- #chat-copy-instruction a {
- font-weight: bold;
- }
-
- #autoscroll-button {
- aspect-ratio: unset;
- background: var(--dcs-yellow);
- }
-
- #autoscroll-div {
- position: absolute;
- bottom: 0;
- right: calc(2em * var(--scale-factor));
- margin-bottom: calc(5em * var(--scale-factor) + 24px);
- scale: var(--scale-factor);
- transform-origin: bottom center;
- }
-
- #autoscroll-div > * {
- position: relative;
- }
-
- #autoscroll-option-now radio, #autoscroll-option-date radio {
- margin-left: 0;
- }
-
- #autoscroll-option-now, #autoscroll-option-date,
- #autoscroll-stop-button, #autoscroll-return-button {
- background: var(--dcs-yellow);
- padding: .25em 0.5em;
- border-radius: .5em;
- box-shadow: var(--dcs-box-shadow);
- }
-
- #autoscroll-control {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
- }
-
- #autoscroll-control .arrow, #autoscroll-option-date, #autoscroll-option-now,
- #autoscroll-option-date *, #autoscroll-option-now *,
- #autoscroll-stop-button, #autoscroll-return-button {
- cursor: pointer;
- color: black;
- }
-
- #autoscroll-option-now, #autoscroll-option-date {
- gap: .33em;
- display: flex;
- align-items: center;
- }
-
- #date-div {
- color: black;
- }
-
- #autoscroll-option-now, #autoscroll-option-now label {
- display: flex;
- }
-
- #autoscroll-option-date input, #autoscroll-option-now input {
- margin: 0;
- }
-
- #autoscroll-control .arrow {
- filter: var(--dcs-drop-shadow);
- height: 4em;
- width: 3em;
- }
-
- #autoscroll-control .arrow * {
- background: var(--dcs-yellow);
- height: 100%;
- width: 100%;
- position: absolute;
- }
-
- #up-arrow-level-2 {
- 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%);
- }
-
- #up-arrow-level-4A, #up-arrow-level-4A-copy {
- 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);
- }
-
- #up-arrow-level-4B, #up-arrow-level-4B-copy {
- 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%);
- }
-
- #down-arrow-level-2 {
- 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%);
- }
-
- #down-arrow-level-2, #up-arrow-level-2 {
- --numberval: .1em;
- --negnum: calc(var(--numberval) * -1);
- --dubnum: calc(var(--numberval)* 2);
- --factor: 1.5;
- --hilight: calc(0.35 * var(--factor));
- --shadow: calc(0.25 * var(--factor));
- }
-
- #down-arrow-level-4A, #down-arrow-level-4A-copy {
- 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%);
- }
-
- #down-arrow-level-4B, #down-arrow-level-4B-copy {
- 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);
- }
-
- #up-arrow-level-3, #down-arrow-level-3 {
- background: none !important;
- filter: drop-shadow(var(--numberval) var(--numberval) var(--numberval) hsla(0, 0%, 100%, var(--hilight)));
- }
-
- #up-arrow-level-3-copy, #down-arrow-level-3-copy {
- background: none !important;
-
- filter: drop-shadow(var(--negnum) var(--negnum) var(--numberval) hsla(0, 0%, 0%, var(--shadow)));
- }
-
- #date-div {
- display: grid;
- grid-template-columns: auto auto auto;
- grid-template-rows: auto auto;
- gap: 0.25em;
- background: white;
- }
-
- #date-div * {
- text-align: center;
- font-size: 0.75em;
- max-width: 9.5em;
- }
-
- #date-div span {
- text-align: center;
- width: fit-content;
- margin: auto;
- }
-
- #date-div input {
- width: 2.5em;
- }
-
- #date-div input:first-of-type {
- width: 4em;
- }
-
- #date-div #time-zone-warning {
- grid-column: span 3;
- }
-
- #autoscroll-stop-button {
-
- }
-
- @keyframes blink {
- 0% {
- background-color: var(--dcs-yellow);
- }
- 50% {
- background-color: yellow; /* Change this to your desired color */
- }
- 100% {
- background-color: var(--dcs-yellow);
- }
- }
-
- .blinking-arrow-button {
- animation: blink 1s infinite; /* 1s duration and infinite loop */
- }
-
- #current-date {
- font-size: 0.8em;
- display: flex;
- flex-direction: column;
- background: white;
- padding: 0.5em;
- text-align: center;
- }
-
- #time-zone-warning {
- max-width: 9.5em;
- }
-
- `;
- document.head.appendChild(style);
-
- // Variables
- const foundChats = new WeakSet();
- let debounceTimeout;
-
- // Drag-and-Drop Functionality
- function makeDraggable(element) {
- const dragBar = element.querySelector('.drag-bar');
- let isDragging = false,
- offsetX, offsetY;
-
- dragBar.addEventListener('mousedown', (e) => {
- isDragging = true;
- offsetX = e.clientX - element.offsetLeft;
- offsetY = e.clientY - element.offsetTop;
- document.addEventListener('mousemove', onDrag);
- document.addEventListener('mouseup', stopDrag);
- });
-
- function onDrag(e) {
- if (!isDragging) return;
-
- // Calculate new position
- let newX = e.clientX - offsetX;
- let newY = e.clientY - offsetY;
-
- // Constrain position to prevent going out of bounds
- newX = Math.max(0, Math.min(newX, window.innerWidth - element.offsetWidth));
- newY = Math.max(0, Math.min(newY, window.innerHeight - element.offsetHeight));
-
- element.style.left = `${newX}px`;
- element.style.top = `${newY}px`;
- }
-
- function stopDrag() {
- isDragging = false;
- document.removeEventListener('mousemove', onDrag);
- document.removeEventListener('mouseup', stopDrag);
- }
-
- // Adjust position if the viewport size changes
- window.addEventListener('resize', () => {
- const currentLeft = parseInt(element.style.left, 10) || 0;
- const currentTop = parseInt(element.style.top, 10) || 0;
-
- // Constrain position based on new viewport dimensions
- const newLeft = Math.min(currentLeft, window.innerWidth - element.offsetWidth);
- const newTop = Math.min(currentTop, window.innerHeight - element.offsetHeight);
-
- element.style.left = `${Math.max(0, newLeft)}px`;
- element.style.top = `${Math.max(0, newTop)}px`;
- });
- }
-
- // Add Resizable Functionality
- function makeResizable(element) {
- const resizeHandle = element.querySelector('.resize-handle');
- let isResizing = false,
- startWidth, startHeight, startX, startY;
-
- resizeHandle.addEventListener('mousedown', (e) => {
- isResizing = true;
- startWidth = element.offsetWidth;
- startHeight = element.offsetHeight;
- startX = e.clientX;
- startY = e.clientY;
- document.addEventListener('mousemove', onResize);
- document.addEventListener('mouseup', stopResize);
- });
-
- function onResize(e) {
- if (!isResizing) return;
- element.style.width = `${startWidth + (e.clientX - startX)}px`;
- element.style.height = `${startHeight + (e.clientY - startY)}px`;
- }
-
- function stopResize() {
- isResizing = false;
- document.removeEventListener('mousemove', onResize);
- document.removeEventListener('mouseup', stopResize);
- }
- }
-
- // Handle Button Click
- function createChatCopyUI(chatElement) {
- logMinor('Creating chat copy div...');
-
- let main = document.querySelector('main[class*="chatContent"]') ||
- document.querySelector('section[class*="chatContent"]');
-
- // Check if the UI already exists
- if (main.querySelector('#chat-copy-outer')) return;
-
- // Create outer container
- const outerDiv = document.createElement('div');
- outerDiv.id = 'chat-copy-outer';
-
- // Create drag bar
- const dragBar = document.createElement('div');
- dragBar.className = 'drag-bar';
- const dragBarSpan = document.createElement('span');
- dragBarSpan.textContent = 'Copy';
- dragBar.appendChild(dragBarSpan);
-
- // Create close box
- const closeBox = document.createElement('div');
- closeBox.className = 'close-box';
- closeBox.textContent = '×';
- closeBox.addEventListener('click', () => {
- // stopScrolling();
- outerDiv.remove();
- stopScrollingVar = true;
- if (chatObserver) {
- chatObserver.disconnect();
- logMinor('Observer disconnected.');
- }
- });
-
- // Create resizable handle
- const resizeHandle = document.createElement('div');
- resizeHandle.className = 'resize-handle';
-
- // Create inner content area
- const innerDiv = document.createElement('div');
- innerDiv.id = 'chat-copy-inner';
-
- logMinor('Reached append stage.');
-
- // Append elements
- outerDiv.appendChild(dragBar);
- outerDiv.appendChild(closeBox);
- outerDiv.appendChild(innerDiv);
- outerDiv.appendChild(resizeHandle);
- main.appendChild(outerDiv);
-
- const copyButton = document.getElementById('copy-button-inner');
- const rect = copyButton.getBoundingClientRect();
-
- outerDiv.style.top = `${rect.top}px`; // Align with the top of the button
- 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
-
- // Create recording bar
- const recordingBar = document.createElement('div');
- recordingBar.className = 'recording-bar';
- const recordingBarSpan = document.createElement('span');
-
- recordingBarSpan.textContent = 'Recording';
-
- // Append the recording bar to the outerDiv
- outerDiv.appendChild(recordingBar);
- recordingBar.appendChild(recordingBarSpan);
-
- // Make it draggable and resizable
- makeDraggable(outerDiv);
- makeResizable(outerDiv);
-
- copyChatMessages(chatElement);
-
- const saveButton = document.createElement('div');
- saveButton.id = "chat-copy-save-button";
- saveButton.classList.add('chat-copy-big-button');
- const saveButtonSpan = document.createElement('span');
- saveButtonSpan.textContent = "Save";
-
- const configButton = document.createElement('div');
- configButton.id = "chat-copy-config-button";
- configButton.classList.add('chat-copy-big-button');
- const configButtonSpan = document.createElement('span');
- configButtonSpan.textContent = "⚙";
-
- const instructionButton = document.createElement('div');
- instructionButton.id = "chat-copy-instruction-button";
- instructionButton.classList.add('chat-copy-big-button');
- const instructionButtonSpan = document.createElement('span');
- instructionButtonSpan.textContent = "❓";
-
- const saveSizeSpan = document.createElement('span');
- saveSizeSpan.textContent = "Save";
- saveSizeSpan.classList.add('save-size-span');
- const saveSizeSpan2 = saveSizeSpan.cloneNode(true);
-
-
-
- saveButton.appendChild(saveButtonSpan);
- outerDiv.appendChild(saveButton);
-
- configButton.appendChild(configButtonSpan);
- configButton.appendChild(saveSizeSpan);
- outerDiv.appendChild(configButton);
-
- instructionButton.appendChild(instructionButtonSpan);
- instructionButton.appendChild(saveSizeSpan2);
- outerDiv.appendChild(instructionButton);
-
- saveButton.addEventListener('click', () => {
- saveChatContent(innerDiv);
- });
-
- configButton.addEventListener('click', () => {
- openconfig(innerDiv);
- });
-
- instructionButton.addEventListener('click', () => {
- openInstructions(innerDiv);
- });
-
- // Create autoscroll button
- const autoscrollDiv = document.createElement('div');
- autoscrollDiv.id = 'autoscroll-div';
- const autoscrollButton = document.createElement('div');
- autoscrollButton.id = "autoscroll-button";
- autoscrollButton.classList.add('chat-copy-big-button');
- const autoscrollSpan = document.createElement('span');
- autoscrollSpan.textContent = "Autoscroll...";
- autoscrollButton.appendChild(autoscrollSpan);
- autoscrollDiv.appendChild(autoscrollButton);
- outerDiv.appendChild(autoscrollDiv);
-
- autoscrollButton.addEventListener('click', () => {
- openAutoscrollControl(autoscrollDiv, autoscrollButton);
- });
-
- }
-
- let date = false;
- let dateStored = false;
- let autoscrollStage = 1;
-
- function resetDate() {
- date = false;
- logMinor('Date has been reset.');
- }
-
-
-
- function openAutoscrollControl(autoscrollDiv, autoscrollButton) {
- autoscrollStage = 2;
- resetDate();
- // Hide the autoscroll button
- autoscrollButton.style.display = 'none';
-
- // Create autoscroll control container
- const autoscrollControl = document.createElement('div');
- autoscrollControl.id = "autoscroll-control";
- // autoscrollControl.style.display = 'flex';
- // autoscrollControl.style.flexDirection = 'column';
- // autoscrollControl.style.alignItems = 'center';
- // autoscrollControl.style.gap = '10px';
-
- // Create up arrow
- const upArrow = document.createElement('div');
- upArrow.id = 'up-arrow';
- upArrow.className = 'arrow';
- // upArrow.innerHTML = `
- // <svg width="36" height="48" viewBox="10 4 4 12" xmlns="http://www.w3.org/2000/svg">
- // <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>
- // </svg>`;
- const upArrowL2 = document.createElement('div');
- upArrowL2.id = 'up-arrow-level-2';
- const upArrowL3 = document.createElement('div');
- upArrowL3.id = 'up-arrow-level-3';
- const upArrowL3copy = document.createElement('div');
- upArrowL3copy.id = 'up-arrow-level-3-copy';
- const upArrowL4A = document.createElement('div');
- upArrowL4A.id = 'up-arrow-level-4A';
- const upArrowL4Acopy = document.createElement('div');
- upArrowL4Acopy.id = 'up-arrow-level-4A-copy';
- const upArrowL4B = document.createElement('div');
- upArrowL4B.id = 'up-arrow-level-4B';
- const upArrowL4Bcopy = document.createElement('div');
- upArrowL4Bcopy.id = 'up-arrow-level-4B-copy';
-
- upArrow.appendChild(upArrowL2);
- upArrowL2.appendChild(upArrowL3);
- upArrowL2.appendChild(upArrowL3copy);
- upArrowL3.appendChild(upArrowL4A);
- upArrowL3.appendChild(upArrowL4B);
- upArrowL3copy.appendChild(upArrowL4Acopy);
- upArrowL3copy.appendChild(upArrowL4Bcopy);
-
- // Create radio group
- const radioGroupNow = document.createElement('div');
- radioGroupNow.id = 'autoscroll-option-now';
- const radioGroupDate = document.createElement('div');
- radioGroupDate.id = 'autoscroll-option-date';
-
- // First radio: "Now"
- const nowRadio = document.createElement('input');
- nowRadio.type = 'radio';
- nowRadio.name = 'autoscroll';
- nowRadio.id = 'autoscroll-now';
- nowRadio.checked = true;
-
- const nowLabel = document.createElement('label');
- nowLabel.htmlFor = 'autoscroll-now';
- nowLabel.innerHTML = 'First/<br>Last';
-
- // Second radio: "Date"
- const dateRadio = document.createElement('input');
- dateRadio.type = 'radio';
- dateRadio.name = 'autoscroll';
- dateRadio.id = 'autoscroll-date';
-
- const dateLabel = document.createElement('label');
- dateLabel.htmlFor = 'autoscroll-date';
- dateLabel.textContent = 'Date';
-
- // Append radios and labels
- radioGroupNow.appendChild(nowRadio);
- radioGroupNow.appendChild(nowLabel);
- radioGroupDate.appendChild(dateRadio);
- radioGroupDate.appendChild(dateLabel);
-
- // Create down arrow
- const downArrow = document.createElement('div');
- downArrow.id = 'down-arrow';
- downArrow.className = 'arrow';
- // downArrow.innerHTML = `
- // <svg width="36" height="48" viewBox="10 8 4 12" xmlns="http://www.w3.org/2000/svg">
- // <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>
- // </svg>`;
- const downArrowL2 = document.createElement('div');
- downArrowL2.id = 'down-arrow-level-2';
- const downArrowL3 = document.createElement('div');
- downArrowL3.id = 'down-arrow-level-3';
- const downArrowL3copy = document.createElement('div');
- downArrowL3copy.id = 'down-arrow-level-3-copy';
- const downArrowL4A = document.createElement('div');
- downArrowL4A.id = 'down-arrow-level-4A';
- const downArrowL4Acopy = document.createElement('div');
- downArrowL4Acopy.id = 'down-arrow-level-4A-copy';
- const downArrowL4B = document.createElement('div');
- downArrowL4B.id = 'down-arrow-level-4B';
- const downArrowL4Bcopy = document.createElement('div');
- downArrowL4Bcopy.id = 'down-arrow-level-4B-copy';
-
- downArrow.appendChild(downArrowL2);
- downArrowL2.appendChild(downArrowL3);
- downArrowL2.appendChild(downArrowL3copy);
- downArrowL3.appendChild(downArrowL4A);
- downArrowL3.appendChild(downArrowL4B);
- downArrowL3copy.appendChild(downArrowL4Acopy);
- downArrowL3copy.appendChild(downArrowL4Bcopy);
-
- // Add Return button
- const returnButtonDiv = document.createElement('div');
- returnButtonDiv.id = 'autoscroll-return-button';
- const returnButtonSpan = document.createElement('span');
- returnButtonSpan.textContent = 'Return';
- returnButtonDiv.appendChild(returnButtonSpan);
-
-
-
- // Append all elements to autoscroll control
- autoscrollControl.appendChild(upArrow);
- autoscrollControl.appendChild(radioGroupNow);
- autoscrollControl.appendChild(radioGroupDate);
- autoscrollControl.appendChild(returnButtonDiv);
- autoscrollControl.appendChild(downArrow);
-
- // Append autoscroll control to the div
- autoscrollDiv.appendChild(autoscrollControl);
-
- // Add listeners for upArrow and downArrow
- upArrow.addEventListener('click', () => autoscroll('up', upArrowL2, downArrowL2));
- downArrow.addEventListener('click', () => autoscroll('down', upArrowL2, downArrowL2));
-
- // Add listeners for radio groups
- radioGroupNow.addEventListener('change', () => displayDate('remove'));
- radioGroupDate.addEventListener('change', () => displayDate('show', radioGroupDate));
-
- // Add event listener for the return button
- returnButtonDiv.addEventListener('click', () => autoscrollReturn(autoscrollControl, autoscrollButton));
- }
-
-
-
-
- let activeScroll = null; // Declare the autoscroll interval globally
-
- function autoscrollReturn(autoscrollControl, autoscrollButton) {
- // Remove autoscroll control
- autoscrollControl.remove();
-
- // Show the autoscroll button again
- autoscrollButton.style.removeProperty('display');
-
- // Stop any active autoscroll
- if (activeScroll) {
- clearInterval(activeScroll);
- activeScroll = null;
- }
- resetDate();
-
- if (autoscrollStage === 3) {
- const autoscrollDiv = document.querySelector('#chat-copy-outer #autoscroll-div');
- openAutoscrollControl(autoscrollDiv, autoscrollButton)
- autoscrollStage = 2;
- } else {
- autoscrollStage = 1;
- }
- }
-
-
- function autoscroll(direction, upArrow, downArrow) {
- stopScrollingVar = false;
- autoscrollStage = 3;
- // Find the chat container element using the provided selectors
- const chatElement = document.querySelector('main[class*="chatContent"] [class*="scrollerBase"], section[class*="chatContent"] [class*="scrollerBase"]');
-
- if (!chatElement) {
- console.error('Chat element not found!');
- return;
- } else {
- logMinor(chatElement);
- }
-
- if (direction === 'up') {
- upArrow.classList.add('blinking-arrow-button'); // Add blinking class to upArrow
- downArrow.classList.remove('blinking-arrow-button'); // Remove blinking class from downArrow
- } else if (direction === 'down') {
- downArrow.classList.add('blinking-arrow-button'); // Add blinking class to downArrow
- upArrow.classList.remove('blinking-arrow-button'); // Remove blinking class from upArrow
- }
-
- const timeZoneWarning = document.querySelector('#date-div #time-zone-warning');
- if (timeZoneWarning) {
- timeZoneWarning.remove();
- }
-
- // Check if a stop button already exists
- let stopButtonDiv = document.querySelector('#autoscroll-stop-button');
- if (stopButtonDiv) {
- // If the button exists, clear the existing scroll to allow a new one to start
- clearInterval(window.activeScroll);
- window.activeScroll = null;
- clearInterval(activeScroll);
- activeScroll = null;
- } else {
- // If the stop button does not exist, create it
- stopButtonDiv = document.createElement('div');
- stopButtonDiv.id = 'autoscroll-stop-button'; // Add ID for styling
- const stopButtonSpan = document.createElement('span');
- stopButtonSpan.textContent = 'Stop';
- stopButtonDiv.appendChild(stopButtonSpan);
-
- // Insert Stop button between the arrows
- const autoscrollControl = document.getElementById('autoscroll-control'); // Assuming this is the container for the arrows and the stop button
- const downArrow = document.getElementById('down-arrow'); // Assuming IDs for upArrow and downArrow
- autoscrollControl.insertBefore(stopButtonDiv, downArrow);
- }
-
- // Remove #autoscroll-option-now and #autoscroll-option-date if present
- const nowOption = document.getElementById('autoscroll-option-now');
- const dateOption = document.getElementById('autoscroll-option-date');
- if (nowOption) nowOption.remove();
- if (!date) {
- if (dateOption) dateOption.remove();
- } else {
- // Create the #current-date div if it doesn't exist
- let currentDateDiv = document.querySelector('#current-date');
- if (!currentDateDiv) {
- currentDateDiv = document.createElement('div');
- currentDateDiv.id = 'current-date';
-
- const currentDateLabel = document.createElement('span');
- currentDateLabel.textContent = 'Current date:';
-
- const currentDateSpan = document.createElement('span');
- currentDateSpan.id = 'current-date-span';
- currentDateSpan.textContent = '...'; // Placeholder content
-
- currentDateDiv.appendChild(currentDateLabel);
- currentDateDiv.appendChild(currentDateSpan);
-
- const dateDiv = document.getElementById('date-div'); // Assuming this is where the new div should be inserted
- // Append currentDateDiv after dateDiv
- dateDiv.parentNode.insertBefore(currentDateDiv, dateDiv.nextSibling);
- }
- }
-
- let dateTime;
-
- // Function to update the #current-date-span with the current date
- function updateCurrentDateSpan(currentMessage) {
-
- if (currentMessage) {
-
- const timeElement = currentMessage.querySelector('time');
- if (timeElement && timeElement.hasAttribute('datetime')) {
-
- // Get the datetime attribute and hack off the 'T' and everything after it
- dateTime = timeElement.getAttribute('datetime').split('T')[0]; // Take only the date part
- const currentDateSpan = document.getElementById('current-date-span');
- // logMinor('Extracting date from current message: ' + datetime);
- if (currentDateSpan) {
- currentDateSpan.textContent = dateTime; // Update the span content with the date
- }
- } else {
- // logMinor('timeElement: ' + timeElement.outerHTML + '\ntimeElement.datetime: ' + timeElement.datetime);
- }
- }
- }
-
- // Scroll setup
- const scrollSpeed = 100; // Pixels per scroll interval
- const interval = 50; // Interval in milliseconds
- const timeoutDuration = 5000; // 2.5 seconds of inactivity
-
- // Check and clear any active scroll to avoid stacking
- if (activeScroll) {
- clearInterval(activeScroll);
- activeScroll = null;
- }
-
- let lastMessage = null;
- let lastMessagePosition = 0;
- // let lastChangeTime = Date.now(); // Initialize with the current time
- let timeoutIntervals = timeoutDuration / interval;
- let currentInterval = 0;
- let lastChangeInterval = 0;
- let dateReached = false;
-
- // Start the autoscroll routine
- activeScroll = setInterval(() => {
- if (stopScrollingVar) {
- stopScrolling();
- }
-
- let currentMessage = null;
- let currentMessagePosition = 0;
- currentInterval += 1;
-
- if (direction === 'up') {
- chatElement.scrollBy(0, -scrollSpeed); // Scroll up
- currentMessage = chatElement.querySelector('li[id^="chat-messages"]');
- if (currentMessage) {
- currentMessagePosition = currentMessage.getBoundingClientRect().top;
- }
- } else if (direction === 'down') {
- chatElement.scrollBy(0, scrollSpeed); // Scroll down
- const messages = chatElement.querySelectorAll('li[id^="chat-messages"]');
- currentMessage = messages[messages.length - 1];
- if (currentMessage) {
- currentMessagePosition = currentMessage.getBoundingClientRect().bottom;
- }
- }
-
- if (date) {
- // Update current date span
- updateCurrentDateSpan(currentMessage); // Call the function to update the current date
- } else {
- // logMinor('No date.');
- }
-
- // Check if the message has changed or if the position has changed
- if (currentMessage !== lastMessage || currentMessagePosition !== lastMessagePosition) {
- lastMessage = currentMessage;
- lastMessagePosition = currentMessagePosition;
- lastChangeInterval = currentInterval;
- // lastChangeTime = Date.now(); // Reset the timeout when the message or position changes
- }
-
- // Stop scrolling after 2.5 seconds of no change
- // if (Date.now() - lastChangeTime >= timeoutDuration) {
- // stopScrolling();
- // logMinor('Autoscroll stopped due to inactivity.');
- // }
- if (currentInterval - lastChangeInterval >= timeoutIntervals) {
- stopScrolling();
- logMinor('Autoscroll stopped due to inactivity.');
- }
-
- if (date) {
- const [year, month, day] = dateTime.split('-'); // Destructure the split result
- const dateMap = {
- year: parseInt(year, 10),
- month: parseInt(month, 10),
- day: parseInt(day, 10)
- };
-
- logMinor('date: ' + JSON.stringify(date) + '; dateMap: ' + JSON.stringify(dateMap));
-
- if (direction === 'up') {
- if (dateMap.year < date.year) {
- dateReached = true;
- } else if (dateMap.year === date.year && dateMap.month < date.month) {
- dateReached = true;
- } else if (
- dateMap.year === date.year &&
- dateMap.month === date.month &&
- dateMap.day < date.day
- ) {
- dateReached = true;
- }
- } else if (direction === 'down') {
- if (dateMap.year > date.year) {
- dateReached = true;
- } else if (dateMap.year === date.year && dateMap.month > date.month) {
- dateReached = true;
- } else if (
- dateMap.year === date.year &&
- dateMap.month === date.month &&
- dateMap.day > date.day
- ) {
- dateReached = true;
- }
- }
- }
-
- if (dateReached) {
- stopScrolling();
- logMinor('Target date has been included.');
- }
-
- }, interval);
-
- // Add listener to stop button
- stopButtonDiv.addEventListener('click', () => {
- stopScrolling();
- // Remove the stop button
- });
-
- function stopScrolling() {
- clearInterval(activeScroll); // Stop the autoscroll
- activeScroll = null; // Clear the activeScroll variable to ensure no active autoscroll routine is left
- downArrow.classList.remove('blinking-arrow-button');
- upArrow.classList.remove('blinking-arrow-button');
- let currentInterval = 0;
- let lastChangeInterval = 0;
- if (stopButtonDiv) {
- stopButtonDiv.remove();
- }
- dateReached = false;
- stopScrollingVar = false;
-
- }
-
- }
-
-
- function displayDate(argument, radioGroupDate) {
- logMinor('Creating date div...');
- const existingDateDiv = document.getElementById('date-div');
- if (argument === 'show') {
- if (existingDateDiv) {
- return;
- }
-
- // Create the date div
- const dateDiv = document.createElement('div');
- dateDiv.id = 'date-div';
-
- // Create the first row (YYYY, MM, DD)
- const labels = ['YYYY', 'MM', 'DD'];
- labels.forEach(label => {
- const span = document.createElement('span');
- span.textContent = label;
- dateDiv.appendChild(span);
- });
-
- let values;
- date = {
- year: null,
- month: null,
- day: null
- };
-
- // If dateStored exists, use those values; otherwise, use the current date
- if (dateStored) {
- values = [
- dateStored.year, // Use stored year
- String(dateStored.month).padStart(2, '0'), // Use stored month
- String(dateStored.day).padStart(2, '0') // Use stored day
- ];
- date = {
- year: parseInt(dateStored.year, 10), // Use stored year
- month: parseInt(dateStored.month, 10), // Use stored month (formatted as 2 digits)
- day: parseInt(dateStored.day, 10) // Use stored day (formatted as 2 digits)
- };
- } else {
- const currentDate = new Date();
- values = [
- currentDate.getFullYear(), // Current year
- String(currentDate.getMonth() + 1).padStart(2, '0'), // Current month
- String(currentDate.getDate()).padStart(2, '0') // Current day
- ];
- date = {
- year: currentDate.getFullYear(), // Current year
- month: currentDate.getMonth() + 1, // Current month
- day: currentDate.getDate() // Current day
- };
- }
-
- // logMinor(`Date updated to: ${date.year}-${date.month}-${date.day}`);
- logMinor('Date updated to: ' + JSON.stringify(date));
-
- values.forEach((value, index) => {
- const input = document.createElement('input');
- input.type = 'number';
- input.value = value;
-
- // Configure input box sizes
- if (index === 0) input.maxLength = 4; // YYYY
- else input.maxLength = 2; // MM or DD
-
- // Prevent decimals
- input.step = '1';
- if (index === 0) {
- input.min = 1000; // Min for year is 1000
- input.max = 9999; // Max for year is 9999
- } else if (index === 1) {
- input.min = 1; // Min for month is 1
- input.max = 12; // Max for month is 12
- } else if (index === 2) {
- input.min = 1; // Min for day is 1
- input.max = 31; // Max for day is 31
- }
-
-
-
- input.addEventListener('input', () => {
- // Update the date object whenever an input value changes
- date.year = parseInt(document.querySelector('#date-div input:nth-of-type(1)').value, 10);
- date.month = parseInt(document.querySelector('#date-div input:nth-of-type(2)').value, 10);
- date.day = parseInt(document.querySelector('#date-div input:nth-of-type(3)').value, 10);
-
- // Update dateStored with the new date
- dateStored = date;
- // logMinor(`Date updated to: ${date.year}-${date.month}-${date.day}`);
- logMinor('Date updated to: ' + JSON.stringify(date));
- });
-
- dateDiv.appendChild(input);
- });
-
- const timeZoneWarning = document.createElement('span');
- timeZoneWarning.id = 'time-zone-warning';
- timeZoneWarning.textContent = 'Note: Due to different time zones, date may be imprecise.';
-
- dateDiv.appendChild(timeZoneWarning);
-
- logMinor('Inserting date div...');
-
- // Insert the date div immediately after the radioGroupDate
- radioGroupDate.parentNode.insertBefore(dateDiv, radioGroupDate.nextSibling);
-
- }
-
- if (argument === 'remove') {
- if (existingDateDiv) {
- existingDateDiv.remove();
- }
- resetDate();
- }
- }
-
-
-
-
- // Open the instructions
- function openInstructions(innerDiv) {
- const instructionDiv = document.createElement('div');
- instructionDiv.id = "chat-copy-instruction";
- const instructionOuter = document.createElement('div');
- instructionOuter.id = "chat-copy-instruction-outer";
- instructionDiv.innerHTML = instructions;
-
- instructionOuter.appendChild(instructionDiv);
-
- if (innerDiv) {
- innerDiv.style.display = "none";
- innerDiv.insertAdjacentElement('afterend', instructionOuter);
- }
-
- // Close button
- const closeButton = document.createElement('button');
- const closeButtonSpan = document.createElement('span');
- closeButtonSpan.textContent = "Close";
- closeButton.appendChild(closeButtonSpan);
- closeButton.id = 'close-instruction-button';
- closeButton.addEventListener('click', () => {
- instructionOuter.remove();
- if (innerDiv) {
- innerDiv.style.display = "block";
- }
- });
- instructionDiv.appendChild(closeButton);
-
- }
-
- // Open the config options
- function openconfig(innerDiv) {
- const configDiv = document.createElement('div');
- configDiv.id = "chat-copy-settings";
- const configOuter = document.createElement('div');
- configOuter.id = "chat-copy-settings-outer";
-
- configOuter.appendChild(configDiv);
-
- if (innerDiv) {
- innerDiv.style.display = "none";
- innerDiv.insertAdjacentElement('afterend', configOuter);
- }
-
- // Heading
- const heading = document.createElement('h2');
- const headingSpan = document.createElement('span');
- headingSpan.textContent = "Options";
- heading.appendChild(headingSpan);
- configDiv.appendChild(heading);
-
- // Enable checkboxes section
- const enableText = document.createElement('h3');
- enableText.textContent = "Check these boxes to enable:";
- configDiv.appendChild(enableText);
-
- const createCheckbox = (id, labelText) => {
- const wrapper = document.createElement('div');
- wrapper.classList.add('wrapper');
- const wrapperInner = document.createElement('div');
- wrapperInner.classList.add('wrapper-inner');
- const checkbox = document.createElement('input');
- checkbox.type = "checkbox";
- checkbox.id = id;
-
- const label = document.createElement('label');
- label.htmlFor = id;
- label.textContent = labelText;
-
- wrapperInner.appendChild(checkbox);
- wrapperInner.appendChild(label);
- wrapper.appendChild(wrapperInner);
- return wrapper;
- };
-
- // Create checkboxes and assign them to constants
- const enableAttachments = createCheckbox("enable-attachments", "Attachments");
- const enableAudio = createCheckbox("enable-audio", "Audio");
- const enableFullSizeImages = createCheckbox("enable-full-size-images", "Full-Size Images");
-
- // Create checkboxes and assign them to constants
- const enableAttachmentsCheckbox = enableAttachments.querySelector('input');
- const enableAudioCheckbox = enableAudio.querySelector('input');
- const enableFullSizeImagesCheckbox = enableFullSizeImages.querySelector('input');
-
- // Append the checkboxes to the configDiv
- configDiv.appendChild(enableFullSizeImages);
- configDiv.appendChild(enableAudio);
- configDiv.appendChild(enableAttachments);
-
- function divide() {
- const divider = document.createElement('div');
- divider.classList.add('divider');
- configDiv.appendChild(divider);
- }
-
- divide();
-
- // Button visibility section
- const notUsingText = document.createElement('h3');
- notUsingText.textContent = "When not using the chat saver:";
- configDiv.appendChild(notUsingText);
- const orHideText = document.createElement('p');
- orHideText.textContent = "Or hide the button and open it from:";
-
-
- const createRadio = (id, name, labelText) => {
- const wrapper = document.createElement('div');
- wrapper.classList.add('wrapper');
- const wrapperInner = document.createElement('div');
- wrapperInner.classList.add('wrapper-inner');
- const radio = document.createElement('input');
- radio.type = "radio";
- radio.id = id;
- radio.name = name;
-
- const label = document.createElement('label');
- label.htmlFor = id;
- label.textContent = labelText;
-
- wrapperInner.appendChild(radio);
- wrapperInner.appendChild(label);
- wrapper.appendChild(wrapperInner);
- return wrapper;
-
- };
-
- // Create radio buttons and assign them to constants
- const showButtonCornerRadio = createRadio("show-button-corner", "button-location", "Show the button in the corner");
- const hideButtonDMRadio = createRadio("hide-button-dm", "button-location", "Direct Messages list");
- const hideButtonServersRadio = createRadio("hide-button-servers", "button-location", "Bottom of Servers sidebar");
- const hideButtonSettingsRadio = createRadio("hide-button-settings", "button-location", "Settings page");
-
- // Append the radio buttons and text to the configDiv
- configDiv.appendChild(showButtonCornerRadio);
- configDiv.appendChild(orHideText);
- configDiv.appendChild(hideButtonDMRadio);
- configDiv.appendChild(hideButtonServersRadio);
- configDiv.appendChild(hideButtonSettingsRadio);
-
- const hideNote = document.createElement('p');
- hideNote.textContent = 'Note: When you open the button, it will stay in the corner until you hide it again.';
-
- configDiv.appendChild(hideNote);
-
- divide();
-
- const enableLogMinor = createCheckbox("enable-log-minor", "Enable extra logs in the dev console");
- const enableLogMinorCheckbox = enableLogMinor.querySelector('input');
-
- configDiv.appendChild(enableLogMinor);
-
- // Close button
- const closeButton = document.createElement('button');
- const closeButtonSpan = document.createElement('span');
- closeButtonSpan.textContent = "Close config";
- closeButton.id = 'close-config-button';
- closeButton.addEventListener('click', () => {
- configOuter.remove();
- if (innerDiv) {
- innerDiv.style.display = "block";
- }
- });
-
- closeButton.appendChild(closeButtonSpan);
- configDiv.appendChild(closeButton);
-
- // Save the settings to localStorage
- const saveSettings = () => {
- const settings = {
- enableAttachments: enableAttachmentsCheckbox.checked,
- enableAudio: enableAudioCheckbox.checked,
- enableFullSizeImages: enableFullSizeImagesCheckbox.checked,
- buttonLocation: document.querySelector('input[name="button-location"]:checked')?.id || 'show-button-corner', // default to 'showButtonCorner' if no radio selected
- enableLogMinor: enableLogMinorCheckbox.checked, // Save the new option
- };
-
- // Update the new variables with the current settings
- attachmentsEnabled = settings.enableAttachments;
- audioEnabled = settings.enableAudio;
- fetchFullSize = settings.enableFullSizeImages;
- buttonGenerationLocation = settings.buttonLocation;
- showMinorLogs = settings.enableLogMinor; // Update the variable
-
- console.log(settings);
-
- localStorage.setItem('chatCopySettings', JSON.stringify(settings));
-
- if (buttonGenerationLocation == 'show-button-corner') {
- handleChatFound();
- buttonPlacer();
- buttonCleanup();
- } else {
- const copyButtonOuter = document.getElementById('copy-button-outer');
-
- if (copyButtonOuter) {
- // Add the class that triggers the animation
- copyButtonOuter.classList.add('move-fade-out');
-
- // Optionally, remove the element after the animation ends
- copyButtonOuter.addEventListener('animationend', () => {
- copyButtonOuter.remove(); // Remove the element once the animation is done
- buttonPlacer();
- buttonCleanup();
- });
- } else {
- buttonPlacer();
- buttonCleanup();
- }
- }
- };
-
- // Add event listeners to checkboxes and radio buttons
- enableAttachmentsCheckbox.addEventListener('change', saveSettings);
- enableAudioCheckbox.addEventListener('change', saveSettings);
- enableFullSizeImagesCheckbox.addEventListener('change', saveSettings);
- document.querySelectorAll('input[name="button-location"]').forEach(radio => {
- radio.addEventListener('change', saveSettings);
- });
- enableLogMinorCheckbox.addEventListener('change', saveSettings);
-
- // Load settings from localStorage and apply them
- const loadSettings = () => {
- const savedSettings = JSON.parse(localStorage.getItem('chatCopySettings'));
- console.log(savedSettings);
-
- if (savedSettings) {
- enableAttachmentsCheckbox.checked = savedSettings.enableAttachments;
- enableAudioCheckbox.checked = savedSettings.enableAudio;
- enableFullSizeImagesCheckbox.checked = savedSettings.enableFullSizeImages;
- enableLogMinorCheckbox.checked = savedSettings.enableLogMinor;
-
- // Set the selected radio button based on saved settings
- const radio = document.getElementById(savedSettings.buttonLocation);
- if (radio) {
- radio.checked = true;
- }
- } else {
- // Set default settings if no saved data exists
- enableAttachmentsCheckbox.checked = true;
- enableAudioCheckbox.checked = true;
- enableFullSizeImagesCheckbox.checked = true;
- document.querySelector('input[name="button-location"][id="show-button-corner"]').checked = true;
- enableLogMinorCheckbox.checked = false;
- }
- };
-
- // Call loadSettings when the config page is loaded
- loadSettings();
-
- // Create a button to clear saved settings
- const clearSettingsButton = document.createElement('button');
- clearSettingsButton.id = 'clear-settings-button';
- clearSettingsButton.textContent = "Clear Saved Settings";
- clearSettingsButton.setAttribute('disabled', '');
-
-
- // Add an event listener to clear settings on click
- clearSettingsButton.addEventListener('click', () => {
- // Remove settings from localStorage
- localStorage.removeItem('chatCopySettings');
- console.log('Settings have been cleared');
-
- // Optionally, reload the settings (to reset the UI)
- loadSettings(); // You can call your loadSettings function here to reset UI to defaults
- });
-
- // Append the button to the configDiv or wherever you'd like to display it
- configDiv.appendChild(clearSettingsButton);
-
- }
-
-
-
-
-
-
- // Declare the observer variable globally so it can be referenced later
- let chatObserver;
-
- function copyChatMessages(chatElement) {
- logMinor('Copying chat messages...');
- const innerDiv = document.querySelector('#chat-copy-inner');
- if (!innerDiv) return;
-
- // Clear existing content
- innerDiv.innerHTML = '';
-
- // Find all chat messages
- const messages = chatElement.querySelectorAll('li[id^="chat-messages"]');
- if (!messages.length) {
- innerDiv.textContent = 'No messages found.';
- return;
- }
-
- // Add messages to the innerDiv in order
- const seenMessages = new Set();
- messages.forEach(message => {
- seenMessages.add(message.id);
- const clonedMessage = message.cloneNode(true);
- innerDiv.appendChild(clonedMessage);
- });
-
- logMinor(`${messages.length} messages copied.`);
-
-
-
- let processTimeout = null; // Timeout for batching updates
-
- // Function to process all messages in the container
- const processAllMessages = () => {
- const messageNodes = Array.from(document.querySelectorAll('li[id^="chat-messages"]')); // Select all message nodes
-
- messageNodes.forEach(node => {
- if (!seenMessages.has(node.id)) {
- // New message: Add it
- const clonedMessage = node.cloneNode(true);
- const messageIDs = Array.from(innerDiv.children).map(child => child.id);
-
- // // Find the correct position based on IDs
- // const index = messageIDs.findIndex(id => id > node.id);
-
- const messageTimes = Array.from(innerDiv.children).map(child => {
- const timeElement = child.querySelector('time');
- return timeElement ? timeElement.getAttribute('datetime') : null;
- });
-
- const newMessageTime = node.querySelector('time')?.getAttribute('datetime') || '';
-
- // Find the correct position based on datetime
- const index = messageTimes.findIndex(existingTime => existingTime && existingTime > newMessageTime);
-
- if (index === -1) {
- innerDiv.appendChild(clonedMessage); // Append at the end
- } else {
- innerDiv.insertBefore(clonedMessage, innerDiv.children[index]); // Insert at the correct position
- }
-
- seenMessages.add(node.id);
- logMinor(`New message added: ${node.id}`);
- } else {
- // Existing message: Update it if content differs
- const existingMessage = innerDiv.querySelector(`#${node.id}`);
- if (existingMessage && existingMessage.innerHTML !== node.innerHTML) {
- existingMessage.innerHTML = node.innerHTML;
- logMinor(`Message updated: ${node.id}`);
- }
- }
- });
-
- processTimeout = null; // Reset timeout
- };
-
- // Function to handle mutations
- const onNewMessages = (mutations) => {
- mutations.forEach(mutation => {
- if (mutation.type === 'childList') {
- // Set a timeout to process all messages (if not already set)
- if (!processTimeout) {
- processTimeout = setTimeout(processAllMessages, 1000); // Process after 1 second
- }
- }
- });
- };
-
-
-
-
-
- // Set up the observer
- if (chatObserver) chatObserver.disconnect(); // Disconnect existing observer if any
- chatObserver = new MutationObserver(onNewMessages);
- chatObserver.observe(chatElement, {
- childList: true,
- subtree: true
- });
- }
-
-
-
-
-
-
- // Ensure JSZip is available
- let jszipAvailable = typeof JSZip !== "undefined";
-
- // Function to clean the image URL (remove query parameters like ?size=80)
- function cleanUrl(url) {
- return url.split('?')[0]; // Remove anything after '?' in the URL
- }
-
-
-
- // Function to fetch and return the full-size image URL (removing size, width, height, and format parameters)
- function getFullSizeImageUrl(imageUrl) {
- const cleanedUrl = imageUrl.replace(/[?&](size|width|height|format)=[^&]*/g, "");
- return cleanedUrl.replace(/\?$/, ""); // Remove the '?' if it's left hanging after removing parameters
- }
-
-
- // Function to extract the image URL from the outerHTML
- function extractImageUrlFromOuterHTML(image) {
- logMinor('image outer html: ' + image.outerHTML);
- const regex = /src=["']([^"']+)["']/; // Matches both " and ' around the URL
- const match = image.outerHTML.match(regex); // Extract the URL
-
- if (match) {
- return match[1]; // Return the first captured group (the URL)
- } else {
- logError('Image source URL not found in outerHTML.');
- return null;
- }
- }
-
-
- // Function to fetch and add images to the ZIP file
- function addImagesToZip(innerDiv, zip, imageMap, htmlContent) {
- logMajor('Fetching and adding images to ZIP...')
- let images;
- if (audioEnabled) {
- images = innerDiv.querySelectorAll('img, video, audio source');
- } else {
- images = innerDiv.querySelectorAll('img, video');
- logMajor('Audio fetching disabled. Skipping.');
- }
-
- const imagePromises = []; // Track all fetch promises to ensure we wait for them
- let imageIndex = 0;
-
- images.forEach((image, index) => {
- const imageUrl = image.src; // Use the original image URL without cleaning
- // const imageUrlRaw = image.getAttribute('src');
- let imageUrlRaw = extractImageUrlFromOuterHTML(image);
-
- // If extraction fails, fallback to image.src
- if (!imageUrlRaw) {
- imageUrlRaw = imageUrl; // Fallback to image.src if extraction fails
- }
-
- logMinor("Image HTML:", image.outerHTML);
- logMinor('imageUrl: ' + imageUrl + '; imageUrlRaw: ' + imageUrlRaw);
-
- if (imageUrl && imageUrl.startsWith("http")) { // Ensure valid HTTP/HTTPS link
- if (!imageMap.has(imageUrlRaw)) { // If this image has not been processed
- logMajor(`Downloading image: ${imageUrl}`);
-
- imageIndex += 1;
-
- // Remove query parameters, but keep the size parameter
- const urlWithoutParams = imageUrl.split('?')[0]; // Base URL without query parameters
-
- // Match the "size", "width", and "height" parameters from the URL
- const sizeParamMatch = imageUrl.match(/[?&]size=([^&]+)/);
- const widthParamMatch = imageUrl.match(/[?&]width=([^&]+)/);
- const heightParamMatch = imageUrl.match(/[?&]height=([^&]+)/);
-
- // Extract the actual file name (e.g., image.jpg) without query params
- const actualFileName = urlWithoutParams.split('/').pop();
-
- // If the "size", "width", or "height" parameters exist, format them for the file name
- const sizeParam = sizeParamMatch ? `_${sizeParamMatch[1]}` : '';
- const widthParam = widthParamMatch ? `_${widthParamMatch[1]}` : '';
- const heightParam = heightParamMatch ? `_${heightParamMatch[1]}` : '';
-
- // Get the file extension (default to .jpg if not found)
- const fileExtension = actualFileName.slice(actualFileName.lastIndexOf('.')) || '.jpg';
-
- // Build the base name without the extension, or fallback to image + index if not found
- const baseName = urlWithoutParams.split('/').pop().split('?')[0].split('.').slice(0, -1).join('.') || `image${index + 1}`;
-
- // Construct the final file name
- // const fileName = `images_and_media/${baseName}${sizeParam}${widthParam}${heightParam}${fileExtension || '.jpg'}`;
-
- // imageMap.set(imageUrlRaw, fileName); // Map the URL to the file name
-
- let fileName = `images_and_media/${baseName}${sizeParam}${widthParam}${heightParam}${fileExtension || ''}`;
- const uniqueFileNameFound = getUniqueFileName(fileName);
-
- // Check if the file path already exists as a value in the map
- function getUniqueFileName(fileName) {
- let uniqueFileName = fileName;
- let counter = 1;
-
- // Check if the file name exists as a value in the map
- let filePathExists = false;
- imageMap.forEach((value) => {
- if (value === uniqueFileName) {
- filePathExists = true;
- }
- });
-
- // If the file path already exists, append a counter to make it unique
- while (filePathExists) {
- uniqueFileName = fileName.replace(/(\.\w+)$/, `_${counter}$1`);
- counter++;
-
- // Recheck the map with the new uniqueFileName
- filePathExists = false;
- imageMap.forEach((value) => {
- if (value === uniqueFileName) {
- filePathExists = true;
- }
- });
- }
-
- // Return the unique file name
- return uniqueFileName;
- }
-
- // Now that we have a unique file name, add it to the map
- imageMap.set(imageUrlRaw, uniqueFileNameFound);
-
- let firstImageRetriesLeft = 3; // Set retry limit for this image
-
- const fetchWithRetry = async () => {
- try {
- const response = await fetch(imageUrl);
- if (!response.ok) {
- throw new Error(`HTTP error! Status: ${response.status}`);
- }
- const blob = await response.blob();
- logMajor(`Adding image to ZIP: ${uniqueFileNameFound}`);
- zip.file(uniqueFileNameFound, blob); // Add the image to the ZIP
- } catch (err) {
- if (firstImageRetriesLeft > 0) {
- firstImageRetriesLeft--; // Decrease retry count
- logMajor(`Retrying fetch for ${imageUrl}... (${firstImageRetriesLeft} attempts left)`);
- await new Promise(resolve => setTimeout(resolve, 2500)); // Wait 2.5 seconds
- return fetchWithRetry(); // Retry
- } else {
- logError(`Failed to fetch image ${imageUrl}:`, err);
- }
- }
- };
-
- // Start the fetch process with retry
- const imagePromise = fetchWithRetry();
-
- imagePromises.push(imagePromise); // Add the promise to the tracker
-
- // Now, check if we should fetch the full-size image
- if (fetchFullSize) {
- const fullSizeUrl = getFullSizeImageUrl(imageUrl);
-
- if (fullSizeUrl !== imageUrl) { // If the full-size URL is different from the original
- const fullSizeBaseName = fullSizeUrl.split('/').pop().split('?')[0];
- const fullSizeFileName = `fullsize_images/${fullSizeBaseName}`;
-
- logMajor(`Downloading full-size image: ${fullSizeUrl}`);
-
- let fullImageRetriesLeft = 3; // Set retry limit for this image
-
- const fetchWithRetry = () => {
- return fetch(fullSizeUrl)
- .then(response => {
- if (!response.ok) {
- logError(`HTTP error! Status: ${response.status}`);
- return; // Exit the current function or promise chain to prevent further action
- }
- return response.blob();
- })
- .then(blob => {
- logMajor(`Adding full-size image to ZIP: ${fullSizeFileName}`);
- zip.file(fullSizeFileName, blob); // Add the full-size image to the ZIP
-
- // Update HTML to point to the full-size file path
- const imageSrcRegex = new RegExp(imageUrl.replace(/[.*+?^=!:${}()|\[\]\/\\]/g, "\\$&"), 'g');
- // htmlContent = htmlContent.replace(imageSrcRegex, fullSizeFileName);
- })
- .catch(err => {
- if (fullImageRetriesLeft > 0) {
- fullImageRetriesLeft--; // Decrease retry count
- logMajor(`Retrying fetch for ${fullSizeUrl}... (${fullImageRetriesLeft} attempts left)`);
- return new Promise(resolve => setTimeout(resolve, 2500)) // Wait 5 seconds before retry
- .then(fetchWithRetry);
- } else {
- logError(`Failed to fetch full-size image ${fullSizeUrl}:`, err);
- }
- });
- };
-
- const fullSizeImagePromise = fetchWithRetry(); // Start the fetch process with retry
-
- imagePromises.push(fullSizeImagePromise); // Add the promise to the tracker
- }
- } else {
- logMajor('Full-size image fetching disabled. Skipping.');
- }
-
- } else {
- // If already processed, retrieve the stored filename
- // const fileName = imageMap.get(imageUrl);
-
- // Update HTML to point to the existing file path
- // const imageSrcRegex = new RegExp(imageUrl.replace(/[.*+?^=!:${}()|\[\]\/\\]/g, "\\$&"), 'g');
- // htmlContent = htmlContent.replace(imageSrcRegex, fileName);
- // htmlContent = htmlContent.replace(imageUrl, fileName);
- }
- }
- });
-
- logMinor(imageMap);
-
- // Wait for all image fetches to complete and return the modified HTML
- return Promise.all(imagePromises).then(() => {
- logMinor('All images processed. Returning modified HTML content.');
- imageMap.forEach((fileName, originalUrl) => {
- // Escape any special characters in the original URL for the regex
- const escapedUrl = originalUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- const urlRegex = new RegExp(escapedUrl, 'g'); // Create a global regex for the URL
- logMinor('originalUrl: ' + originalUrl + '; escapedUrl: ' + escapedUrl);
- htmlContent = htmlContent.replace(urlRegex, fileName);
- logMinor('Replaced all instances of ' + originalUrl + ' with ' + fileName + '.');
- });
-
- return htmlContent;
- });
- }
-
-
-
-
-
- // Function to add styles to the ZIP file
- function addStylesToZip(innerDiv, zip) {
- const pageTitle = document.title;
- let cssFiles = []; // Array to hold file names for linking in HTML
- logMajor('Adding styles to zip...');
-
- // Fetch all external CSS files (from <link> tags in the document)
- const linkStyles = document.querySelectorAll('link[rel="stylesheet"]');
- linkStyles.forEach((link, index) => {
- const href = link.href;
- if (href && href.startsWith("http")) { // Make sure it's a valid URL
- let styleRetriesLeft = 3; // Set retry limit for each link
-
- const fetchWithRetry = () => {
- logMinor(`Fetching external CSS from: ${href}`);
- return fetch(href)
- .then(response => response.text())
- .then(cssContent => {
- const fileName = `styles/style${index + 1}.css`;
- logMajor(`Adding CSS file to ZIP: ${fileName}`);
- zip.file(fileName, cssContent);
- cssFiles.push(fileName); // Keep track of this file for later
- })
- .catch(err => {
- if (styleRetriesLeft > 0) {
- styleRetriesLeft--; // Decrease retry count
- logMajor(`Retrying fetch for ${href}... (${styleRetriesLeft} attempts left)`);
- return new Promise(resolve => setTimeout(resolve, 2500)) // Wait 5 seconds before retry
- .then(fetchWithRetry);
- } else {
- logError("Failed to fetch external CSS:", err);
- }
- });
- };
-
- fetchWithRetry(); // Start the fetch process with retry
- }
- });
-
- // Handle inline <style> tags inside the innerDiv
- const inlineStyles = innerDiv.querySelectorAll('style');
- inlineStyles.forEach((style, index) => {
- const cssContent = style.innerHTML;
- const fileName = `inline-style${index + 1}.css`;
- logMinor(`Adding inline CSS to ZIP: ${fileName}`);
- zip.file(fileName, cssContent);
- cssFiles.push(fileName);
- style.innerHTML = ''; // Clear the original inline style tag
- const linkTag = document.createElement('link');
- linkTag.rel = "stylesheet";
- linkTag.href = fileName;
- style.parentNode.insertBefore(linkTag, style);
- logMinor(`Replaced <style> with <link> referencing ${fileName}`);
- });
-
- logMinor(`Added styles: ${cssFiles.join(', ')}`);
- return cssFiles;
- }
-
- // Function to add the necessary body style
- function addBodyStyle(htmlContent) {
- const bodyStyle = `
- <style>
-
- body {
- overflow: unset !important;
- }
-
- li::marker {
- content: "" !important;
- }
-
- [class*="messageListItem"]:hover [class*="timestampVisibleOnHover"] {
- opacity: 1;
- }
-
- [class*="spoilerMarkdownContent"][class*="hidden"] {
- background: hsl(0, 0%, 50%);
- border: 1px solid hsl(0, 0%, 25%);
- }
-
- </style>
- `;
-
- return bodyStyle + htmlContent;
- }
-
- function addUTFTag(htmlContent) {
- const UTFTag = `<meta charset="UTF-8">
- `;
- return UTFTag + htmlContent;
- }
-
-
-
- function addAttachmentsToZip(innerDiv, zip, attachmentMap) {
- if (!attachmentsEnabled) {
- logMajor("Attachment fetching disabled. Skipping.");
- return;
- }
- logMajor("Fetching and adding miscellaneous attachments to ZIP...");
-
- // Select attachment links
- const attachmentLinks = innerDiv.querySelectorAll('[class*="attachmentInner"] a[class*="fileNameLink"], a[class*="downloadSection"]');
- const fetchPromises = [];
-
- // Reuse the unique file name generator
- function getUniqueFileName(fileName, fileMap, folder) {
- let uniqueFileName = fileName;
- let counter = 1;
-
- while (Array.from(fileMap.values()).includes(`${folder}/${uniqueFileName}`)) {
- uniqueFileName = fileName.replace(/(\.\w+)$/, `_${counter}$1`); // Add counter before extension
- counter++;
- }
-
- return uniqueFileName;
- }
-
- attachmentLinks.forEach(link => {
- let fileUrl = link.href;
- let baseFileName = fileUrl.split('/').pop().split('?')[0]; // Strip arguments
- let uniqueFileName = getUniqueFileName(baseFileName, attachmentMap, "attachments");
-
- let fileRetriesLeft = 3; // Set retry limit
-
- // Add fetch promise for each attachment with retry
- fetchPromises.push(
- (function fetchWithRetry() {
- return fetch(fileUrl)
- .then(response => {
- if (!response.ok) {
- logError(`Failed to fetch ${fileUrl}: ${response.statusText}`);
- return; // Exit the current function or promise chain to prevent further action
- }
- return response.blob();
- })
- .then(blob => {
- const filePath = `attachments/${uniqueFileName}`;
- zip.file(filePath, blob); // Add file to ZIP
- attachmentMap.set(fileUrl, filePath); // Map file URL to unique path
- logMajor(`Attachment added: ${filePath}`);
- })
- .catch(err => {
- // Check if the error is related to CORS
-
- if (fileRetriesLeft > 0) {
- fileRetriesLeft--; // Decrease retry count
- logMajor(`Retrying fetch for ${fileUrl}... (${fileRetriesLeft} attempts left)`);
- return new Promise(resolve => setTimeout(resolve, 2500)) // Wait 2.5 seconds before retry
- .then(fetchWithRetry);
- } else {
- logError(`Failed to fetch attachment ${fileUrl} after multiple attempts.`, err);
- }
- });
- })() // Immediately invoke the fetch function
- );
- });
-
- return Promise.all(fetchPromises);
- }
-
- let removeOnScreenLog;
-
- // Function to display logging info on screen
- function onScreenLogging(argument1, message) {
- const progressLogOverlay = document.getElementById('progress-log-overlay');
-
- // Case 1: 'create' - Create the log overlay div and set the remove flag
- if (argument1 === 'create') {
- if (!progressLogOverlay) {
- const newOverlay = document.createElement('div');
- newOverlay.id = 'progress-log-overlay';
- document.getElementById('chat-copy-outer').appendChild(newOverlay); // Add it inside #chat-copy-outer
- }
- removeOnScreenLog = false;
- }
-
- // Case 2: 'remove' - Remove the overlay div after a delay if conditions are met
- if (argument1 === 'remove') {
- removeOnScreenLog = true;
- setTimeout(() => {
- if (removeOnScreenLog) {
- const overlay = document.getElementById('progress-log-overlay');
- if (overlay) {
- overlay.remove(); // Remove the progress log overlay
- }
- }
- }, 16000); // 16-second delay
- }
-
- // Case 3: 'log' or 'error' - Add a log message inside the #progress-log-overlay
- if ((argument1 === 'log' || argument1 === 'error') && progressLogOverlay) {
- const lineOuter = document.createElement('span');
- lineOuter.classList.add('line-outer');
-
- const lineInner = document.createElement('span');
- lineInner.classList.add('line-inner');
-
- // If it's an error, add the .error class to the inner span
- if (argument1 === 'error') {
- lineInner.classList.add('error');
- }
-
- // Add the message to the line-inner span
- lineInner.textContent = message;
-
- // Append the line-inner to line-outer, and line-outer to the progress log
- lineOuter.appendChild(lineInner);
- progressLogOverlay.appendChild(lineOuter);
- }
- }
-
-
- // Function to save the chat content
- function saveChatContent(innerDiv) {
- resetLog();
- onScreenLogging('create');
- const pageTitle = document.title;
- const zip = new JSZip(); // Initialize a new JSZip instance
- const imageMap = new Map(); // Map to store image URLs and their corresponding file names
- const attachmentMap = new Map();
- logMajor('Initializing ZIP file creation...');
-
- innerDiv.querySelectorAll('video').forEach(video => {
- video.setAttribute('autoplay', '');
- video.setAttribute('muted', '');
- video.setAttribute('loop', '');
- });
-
- // Add HTML file to the ZIP
- let htmlContent = innerDiv.innerHTML;
- // console.log('Adding HTML content to ZIP...');
-
- const cssFiles = addStylesToZip(innerDiv, zip); // Add styles and get CSS file names
-
- // Handle images and other assets (if any)
- // logMajor('Fetching and adding images and attachments to ZIP...');
- addImagesToZip(innerDiv, zip, imageMap, htmlContent).then(modifiedHtmlContent => {
- logMajor('All images added to ZIP.');
-
-
- // Fetch and add attachments
- addAttachmentsToZip(innerDiv, zip, attachmentMap).then(() => {
- logMajor("All attachments added to ZIP.");
-
- // Modify HTML content to link to the CSS files in the ZIP
- let htmlWithStyles = modifiedHtmlContent;
- cssFiles.forEach((cssFile) => {
- const linkTag = `<link rel="stylesheet" href="${cssFile}">`;
- htmlWithStyles = linkTag + htmlWithStyles; // Prepend <link> tags to the HTML content
- });
-
- // Add the body style and prepare the final HTML content
- htmlWithStyles = addBodyStyle(htmlWithStyles);
-
- htmlWithStyles = addUTFTag(htmlWithStyles);
-
-
-
- // Create and embed the JavaScript file
- const scriptContent = `
- document.querySelectorAll('[class*="spoilerContent"]').forEach(spoiler => {
- const clickHandler = function () {
- // Log the clicked element for debugging
- console.log("Spoiler clicked:", spoiler);
-
- // Remove all classes from the clicked item that start with "hidden"
- Array.from(spoiler.classList)
- .filter(className => className.startsWith("hidden"))
- .forEach(hiddenClass => spoiler.classList.remove(hiddenClass));
-
- // Check if the clicked item has children
- if (spoiler.children.length > 0) {
- Array.from(spoiler.querySelectorAll('[class*="spoilerWarning"]')).forEach(child => {
- // Remove elements with a class matching [class*="spoilerWarning"]
- child.remove();
- });
-
- Array.from(spoiler.querySelectorAll('[class*="hidden"]')).forEach(child => {
- // Remove "hidden" classes from all matching descendants
- Array.from(child.classList)
- .filter(className => className.startsWith("hidden"))
- .forEach(hiddenClass => child.classList.remove(hiddenClass));
- });
-
- }
-
- // Remove the event listener to prevent repeated triggering
- spoiler.removeEventListener("click", clickHandler);
- };
-
- // Attach the click event listener to the spoiler element
- spoiler.addEventListener("click", clickHandler);
- });
- `;
-
- // Specify the folder path in the ZIP
- zip.file("scripts/spoilerListener.js", scriptContent);
-
- // Add <script> tag pointing to the folder
- htmlWithStyles += `<script src="scripts/spoilerListener.js"></script>`;
-
-
-
- // Beautify the content (assuming it's HTML or text-based inside the ZIP)
- const beautifiedContent = html_beautify(htmlWithStyles, {
- indent_size: 2
- });
-
- // Save modified HTML to ZIP
- logMajor('Saving modified HTML to ZIP...');
- const modifiedFileName = `${pageTitle.replace(/^[•\s]+/, '').replace(/[\\\/:*?"<>|]/g, '_')}`; // Remove bullet and invalid chars
- zip.file(modifiedFileName + ".html", beautifiedContent); // Save the page content as an HTML file
-
- logMinor(outputLog);
- logMajor('Saving log to ZIP...');
- zip.file("log.txt", outputLog);
- logMinor('Logs have been added to zip.');
-
- // Generate the zip file and trigger download
- logMajor('Generating ZIP file...');
- zip.generateAsync({
- type: "blob"
- })
- .then(content => {
- logMajor('ZIP file created successfully.');
- logMinor(outputLog);
-
-
-
-
- const link = document.createElement("a");
- link.href = URL.createObjectURL(content);
- link.download = `${modifiedFileName}.zip`; // Set the modified filename
- link.click(); // Trigger the download
- resetLog();
- onScreenLogging('remove');
- })
- .catch(err => {
- logError("Error creating ZIP file:", err);
- });
- }).catch(err => {
- logError("Error adding attachments to ZIP:", err);
- });
- }).catch(err => {
- logError("Error adding images to ZIP:", err);
- });
- }
-
-
-
-
- function buttonPlacer() {
- // 1. Detect chats
- const chats = document.querySelectorAll(
- 'main[class*="chatContent"] ol[class*="scrollerInner"], section[class*="chatContent"] ol[class*="scrollerInner"]'
- );
-
- if (buttonGenerationLocation == 'show-button-corner') {
- chats.forEach(chat => {
- if (!foundChats.has(chat)) {
- foundChats.add(chat);
-
- handleChatFound(chat);
-
- } else {
- logMinor('Already found chat.');
- }
- });
- }
-
- // 2. Detect private channels
- const privateChannels = document.querySelectorAll('nav[class*="privateChannels"]');
- if (buttonGenerationLocation == 'hide-button-dm') {
- privateChannels.forEach(nav => handlePrivateChannelsFound(nav));
- }
-
- // 3. Detect footer
- const footers = document.querySelectorAll('nav ul [class*="footer"]');
- if (buttonGenerationLocation == 'hide-button-servers') {
- footers.forEach(footer => handleFooterFound(footer));
- }
-
- // 4. Detect sidebar region
- const sidebars = document.querySelectorAll('[class*="sidebarRegion"] nav[class*="sidebar"]');
- if (buttonGenerationLocation == 'hide-button-settings') {
- sidebars.forEach(sidebar => handleSidebarFound(sidebar));
- }
- }
-
-
-
- // Debounced mutation observer callback
- const mutationCallback = (mutations) => {
- if (debounceTimeout) clearTimeout(debounceTimeout);
-
- debounceTimeout = setTimeout(() => {
- buttonPlacer();
-
- }, 100);
- };
-
-
-
- // Function to handle chat detection
- function handleChatFound(chat) {
- const presentChat = document.querySelector(
- 'main[class*="chatContent"] ol[class*="scrollerInner"], section[class*="chatContent"] ol[class*="scrollerInner"]'
- );
- if (!presentChat) {
- logMinor('No chat found.');
- return;
- } else if (!chat) {
- chat = presentChat;
- }
-
- logMinor('New chat found:', chat);
-
- const messagesWrapper = chat.closest('div[class*="messagesWrapper"]');
- if (messagesWrapper && !messagesWrapper.querySelector('#copy-button-outer')) {
- const outerDiv = document.createElement('div');
- outerDiv.id = 'copy-button-outer';
-
- const innerDiv = document.createElement('div');
- innerDiv.id = 'copy-button-inner';
-
- const span = document.createElement('span');
- span.innerHTML = 'Copy<br>Chat';
-
- innerDiv.appendChild(span);
- outerDiv.appendChild(innerDiv);
- messagesWrapper.appendChild(outerDiv);
-
- // Add click listener to copy button
- innerDiv.addEventListener('click', () => createChatCopyUI(chat));
-
- logMinor('Copy button added to:', messagesWrapper);
- }
- buttonCleanup();
- }
-
- // Function to handle private channels detection
- function handlePrivateChannelsFound(nav) {
- logMinor('Private channel found:', nav);
- const privateChannelsHeader = nav.querySelector('[class*="privateChannelsHeaderContainer"]');
- const showButtonButton = document.createElement('div');
- showButtonButton.id = 'direct-messages-SBB';
- const sBBSpan = document.createElement('span');
- sBBSpan.textContent = 'Show Copy button';
- showButtonButton.appendChild(sBBSpan);
- const existingButton = nav.querySelector('[id="direct-messages-SBB"]');
- if (!existingButton) {
- if (privateChannelsHeader) {
- privateChannelsHeader.parentNode.insertBefore(showButtonButton, privateChannelsHeader);
- attachShowButtonListener(showButtonButton);
- adjustButtonMargin(showButtonButton);
- } else {
- nav.prepend(showButtonButton);
- showButtonButton.classList.add('fallback-position');
- attachShowButtonListener(showButtonButton);
- adjustButtonMargin(showButtonButton);
- }
- } else if (existingButton.classList.contains('fallback-position')) {
- if (privateChannelsHeader) {
- existingButton.remove();
- privateChannelsHeader.parentNode.insertBefore(showButtonButton, privateChannelsHeader);
- attachShowButtonListener(showButtonButton);
- adjustButtonMargin(showButtonButton);
- }
- } else {
- // Check if the button is immediately before the privateChannelsHeader
- if (privateChannelsHeader && existingButton.nextElementSibling !== privateChannelsHeader) {
- privateChannelsHeader.parentNode.insertBefore(existingButton, privateChannelsHeader);
- adjustButtonMargin(existingButton);
- }
- }
-
- // Function to adjust button margin based on left padding of privateChannelsHeader
- function adjustButtonMargin(button) {
- const paddingLeft = window.getComputedStyle(privateChannelsHeader).getPropertyValue('padding-left');
- button.style.marginLeft = paddingLeft; // Apply paddingLeft as margin-left for button
- }
-
- buttonCleanup();
- }
-
- // Function to handle footer detection
- function handleFooterFound(footer) {
- logMinor('Footer found:', footer);
- const showButtonButton = document.createElement('div');
- showButtonButton.id = 'servers-footer-SBB';
- const sBBSpan = document.createElement('span');
- sBBSpan.textContent = 'Show Copy button';
- showButtonButton.appendChild(sBBSpan);
- const existingButton = footer.querySelector('[id="servers-footer-SBB"]');
- if (!existingButton) {
- const listItemWrapper = footer.querySelector('[class*="listItemWrapper"]');
-
- if (listItemWrapper) {
- // Get the left position of the listItemWrapper relative to the viewport
- // const listItemWrapperPosition = listItemWrapper.getBoundingClientRect().left;
-
- // Get the width of the window
- // const windowWidth = window.innerWidth;
-
- // Calculate the marginLeft by subtracting the element's left position from the window's width
- // const marginLeft = windowWidth - listItemWrapperPosition - listItemWrapper.offsetWidth; // Subtract element width to get the remaining space
-
- // Apply this marginLeft value to align the button correctly
- // showButtonButton.style.marginLeft = `${marginLeft}px`;
- }
-
- footer.prepend(showButtonButton);
- attachShowButtonListener(showButtonButton);
- }
- buttonCleanup();
- }
-
- // Function to handle sidebar region detection
- function handleSidebarFound(sidebar) {
- logMinor('Sidebar found:', sidebar);
- const firstSeparator = sidebar.querySelector('[class*="separator"]');
- const showButtonButton = document.createElement('div');
- showButtonButton.id = 'settings-SBB';
- const sBBSpan = document.createElement('span');
- sBBSpan.textContent = 'Show Copy button';
- showButtonButton.appendChild(sBBSpan);
- const existingButton = sidebar.querySelector('[id="settings-SBB"]');
- if (!existingButton) {
- if (firstSeparator) {
- firstSeparator.parentNode.insertBefore(showButtonButton, firstSeparator);
- const marginLeft = window.getComputedStyle(firstSeparator).getPropertyValue('margin-left');
- showButtonButton.style.marginLeft = marginLeft;
- attachShowButtonListener(showButtonButton);
- } else {
- sidebar.prepend(showButtonButton);
- showButtonButton.classList.add('fallback-position');
- attachShowButtonListener(showButtonButton);
- }
- } else if (existingButton.classList.contains('fallback-position')) {
- if (firstSeparator) {
- existingButton.remove();
- firstSeparator.parentNode.insertBefore(showButtonButton, firstSeparator);
- const marginLeft = window.getComputedStyle(firstSeparator).getPropertyValue('margin-left');
- showButtonButton.style.marginLeft = marginLeft;
- attachShowButtonListener(showButtonButton);
- }
- }
- buttonCleanup();
- }
-
- // Function to attach click listener to the button
- function attachShowButtonListener(showButtonButton) {
- showButtonButton.addEventListener('click', () => {
- buttonGenerationLocation = 'show-button-corner';
- saveButtonLocation(buttonGenerationLocation);
- handleChatFound(); // Call the handleChatFound function when the button is clicked
- showButtonButton.remove();
- });
- }
-
- // Function declaration to save only the buttonLocation
- function saveButtonLocation(argument) {
- const savedSettings = JSON.parse(localStorage.getItem('chatCopySettings')) || {}; // Get existing settings or default to an empty object
- const settings = {
- ...savedSettings, // Keep the previous settings
- buttonLocation: argument, // Set new buttonLocation
- };
-
- console.log(settings); // Log updated settings
- localStorage.setItem('chatCopySettings', JSON.stringify(settings)); // Save to localStorage
- }
-
- function buttonCleanup() {
- logMinor('Button cleanup...');
- const dmButton = document.querySelector('#direct-messages-SBB');
- const serverFooterButton = document.querySelector('#servers-footer-SBB');
- const settingsButton = document.querySelector('#settings-SBB');
- // if(buttonGenerationLocation != 'show-button-corner') {
-
- // }
-
- if ((buttonGenerationLocation != 'hide-button-dm') && dmButton) {
- dmButton.remove();
- }
-
- if ((buttonGenerationLocation != 'hide-button-servers') && serverFooterButton) {
- serverFooterButton.remove();
- }
-
- if ((buttonGenerationLocation != 'hide-button-settings') && settingsButton) {
- settingsButton.remove();
- }
- }
-
- // Start observing
- const observer = new MutationObserver(mutationCallback);
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
- })();