您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.3.1 // @author Jupiter Liar // @description 12/14/2024, 7:25 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; } </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); // 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 }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址