Greasy Fork is available in English.

Jupiter's Discord Chat Saver

A free, full-featured chat saver for Discord.

// ==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
		});
})();