Telegram Private Downloader 1.0

Download.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Telegram Private Downloader 1.0
// @namespace    slon
// @license      MIT
// @version      1.0
// @description  Download.
// @author       slon (modified)
// @match        https://web.telegram.org/*
// @match        https://webk.telegram.org/*
// @match        https://webz.telegram.org/*
// @match        https://web.telegram.org/a/*
// @icon         https://img.icons8.com/ios_filled/1200/elephant.jpg
// @grant        unsafeWindow
// @grant        GM_addStyle
// ==/UserScript==

(function () {
    'use strict';

    // === 1. ПРОВЕРКА ВЕРСИИ TELEGRAM ===
    // Скрипт работает корректно только на версии WebK (https://webk.telegram.org или https://web.telegram.org/k/)
    const isWebK = window.location.href.includes('webk.telegram.org') || window.location.pathname.includes('/k/');

    if (!isWebK) {
        // Если это версия A, Z или другая, выдаем предупреждение
        alert("Перейди на страницу https://webk.telegram.org Скрипт работает только там\nGo to the page https://webk.telegram.org The script only works there");
        return; // Останавливаем выполнение скрипта
    }

    GM_addStyle(`
        #tg-saver-panel {
            position: absolute;
            top: 10px;
            right: 20px;
            z-index: 99999;
            background: rgba(33, 33, 33, 0.95);
            padding: 8px;
            border-radius: 12px;
            display: flex;
            gap: 8px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.5);
            backdrop-filter: blur(5px);
            border: 1px solid rgba(255,255,255,0.1);
            transition: opacity 0.3s;
            align-items: center;
            flex-wrap: wrap;
        }
        #tg-saver-panel:hover {
            opacity: 1;
        }
        .saver-btn {
            background: #3390ec;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 8px;
            cursor: pointer;
            font-weight: bold;
            font-size: 13px;
            display: flex;
            align-items: center;
            gap: 6px;
            transition: transform 0.1s, background 0.2s;
            white-space: nowrap;
        }
        .saver-btn:hover {
            background: #409ced;
            transform: scale(1.05);
        }
        .saver-btn:active {
            transform: scale(0.95);
        }
        .saver-btn.full-mode {
            background: #4caf50;
        }
        .saver-btn.full-mode:hover {
            background: #66bb6a;
        }
        .saver-btn.one-mode {
            background: #ff9800;
        }
        .saver-btn.one-mode:hover {
            background: #ffa726;
        }
        .saver-btn:disabled {
            background: #555;
            cursor: not-allowed;
            opacity: 0.7;
        }
        #saver-progress-container {
            width: 100%;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 8px;
            height: 20px;
            position: relative;
            overflow: hidden;
            margin-top: 5px;
            display: none;
        }
        #saver-progress-bar {
            height: 100%;
            background: linear-gradient(90deg, #3390ec, #409ced);
            width: 0%;
            transition: width 0.3s ease;
            border-radius: 8px;
        }
        #saver-progress-text {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-size: 11px;
            font-weight: bold;
            text-shadow: 0 1px 2px rgba(0,0,0,0.5);
        }
        body.tg-safe-mode a,
        body.tg-safe-mode video,
        body.tg-safe-mode .media-container {
            pointer-events: none !important;
        }
        body.tg-safe-mode .time,
        body.tg-safe-mode .message-time {
            border: 1px solid rgba(0, 255, 0, 0.3);
        }
    `);

    function log(msg) {
        console.log(`%c[TG-SAVER] ${msg}`, 'color: #00ff00; background: #000; padding: 2px 5px; border-radius: 3px;');
    }

    function playCompletionSound() {
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const oscillator = audioContext.createOscillator();
        const gainNode = audioContext.createGain();

        oscillator.connect(gainNode);
        gainNode.connect(audioContext.destination);

        oscillator.frequency.value = 800;
        oscillator.type = 'sine';

        gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
        gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);

        oscillator.start(audioContext.currentTime);
        oscillator.stop(audioContext.currentTime + 0.3);
    }

    function getChatTitle() {
        const el = document.querySelector('.chat-info .peer-title') ||
                   document.querySelector('.top-header .peer-title') ||
                   document.querySelector('.chat-title') ||
                   document.querySelector('.person .peer-title');

        if (el) {
            let title = el.textContent.trim();
            return title.replace(/[\\/:*?"<>|]+/g, '_');
        }
        return 'telegram_export';
    }

    let isRunning = false;
    let isFullMode = false;
    let scrollInterval;
    let processedMessages = new Set();
    let stuckCounter = 0;
    let messageBuffer = new Map();
    let downloadedFilesMap = new Map();

    function createPanel() {
        if (document.getElementById('tg-saver-panel')) return;

        const colCenter = document.querySelector('#column-center');
        if (!colCenter) return;

        const panel = document.createElement('div');
        panel.id = 'tg-saver-panel';
        panel.innerHTML = `
            <button id="saver-full-btn" class="saver-btn full-mode" title="Auto: Scroll → Download → TXT">
                <span>💬</span> FULL CHAT
            </button>
            <button id="saver-one-btn" class="saver-btn one-mode" title="Download single selected file">
                <span>📥</span> ONE MSG
            </button>
            <div id="saver-status" style="color:#aaa; font-size:11px; margin-left:5px;">Ready</div>
            <div id="saver-progress-container">
                <div id="saver-progress-bar"></div>
                <div id="saver-progress-text">0%</div>
            </div>
        `;

        colCenter.appendChild(panel);

        document.getElementById('saver-full-btn').addEventListener('click', runFullMode);
        document.getElementById('saver-one-btn').addEventListener('click', downloadOne);
    }

    function updateProgress(current, total, label = '') {
        const progressContainer = document.getElementById('saver-progress-container');
        const progressBar = document.getElementById('saver-progress-bar');
        const progressText = document.getElementById('saver-progress-text');

        if (!progressContainer || !progressBar || !progressText) return;

        const percentage = Math.round((current / total) * 100);

        progressContainer.style.display = 'block';
        progressBar.style.width = percentage + '%';
        progressText.textContent = label ? `${percentage}% ${label}` : `${percentage}%`;
    }

    function hideProgress() {
        const progressContainer = document.getElementById('saver-progress-container');
        if (progressContainer) {
            progressContainer.style.display = 'none';
        }
    }

    function simulateClick(element) {
        const rect = element.getBoundingClientRect();
        if (rect.width === 0 || rect.height === 0) return;

        const x = rect.left + (rect.width / 2);
        const y = rect.top + (rect.height / 2);

        const eventOptions = {
            bubbles: true,
            cancelable: true,
            detail: 1,
            screenX: x,
            screenY: y,
            clientX: x,
            clientY: y,
            buttons: 1,
            pointerId: 1,
            pointerType: "mouse",
            isPrimary: true
        };

        [
            new PointerEvent('pointerdown', eventOptions),
            new MouseEvent('mousedown', eventOptions),
            new PointerEvent('pointerup', eventOptions),
            new MouseEvent('mouseup', eventOptions),
            new MouseEvent('click', eventOptions)
        ].forEach(evt => {
            evt.preventDefault = function() {};
            evt.stopPropagation = function() {};
            element.dispatchEvent(evt);
        });
    }

    function cleanTextLines(text) {
        if (!text) return '';
        let lines = text.split('\n');

        lines = lines.filter(line => {
            let l = line.trim();
            if (!l) return false;

            // Удаляем только расширения
            if (/^(zip|rar|7z|txt|doc|docx|xls|xlsx|pdf|mp3|ogg|mp4|mov|avi|mkv|jpg|png|jpeg|gif|webp)$/i.test(l)) return false;

            // Удаляем заглушки
            if (/^(🖼️ Фото|🎤 Голосовое сообщение|🎬 Видео|📎 Файл)$/.test(l)) return false;

            // Удаляем тайминги
            if (/^\d{1,2}:\d{2}$/.test(l)) return false;

            // === УДАЛЕНИЕ "Channel created" ===
            if (/^(Channel created|Канал создан|Group created|Группа создана)$/i.test(l)) return false;

            return true;
        });

        return lines.join(' ').trim();
    }

    function extractMessageText(bubble) {
        let author = '';
        const authorEl = bubble.querySelector('.peer-title') || bubble.querySelector('.from-name');
        if (authorEl) {
            author = authorEl.textContent.trim();
        }

        let timeStr = '';
        const timeEl = bubble.querySelector('.time') ||
                       bubble.querySelector('.message-time') ||
                       bubble.querySelector('.bubble-time');
        if (timeEl) {
            timeStr = timeEl.textContent.trim();
            const timeMatch = timeStr.match(/\d{1,2}:\d{2}/);
            if (timeMatch) {
                timeStr = timeMatch[0];
            }
        }

        let text = '';
        const mid = bubble.dataset.mid;
        const midNumber = mid ? parseInt(mid) : 0;

        const fileNamesSet = new Set();

        if (mid && downloadedFilesMap.has(mid)) {
            const storedFiles = downloadedFilesMap.get(mid);
            if (Array.isArray(storedFiles)) {
                storedFiles.forEach(f => fileNamesSet.add(f));
            } else {
                fileNamesSet.add(storedFiles);
            }
        }

        const nameSelectors = ['.document-name', '.file-name', '.audio-title', '.audio-subtitle'];
        nameSelectors.forEach(selector => {
            bubble.querySelectorAll(selector).forEach(el => {
                const name = el.textContent.trim();
                if (name) fileNamesSet.add(name);
            });
        });

        const bubbleClone = bubble.cloneNode(true);
        const elementsToRemove = bubbleClone.querySelectorAll(
            '.time, .message-time, .bubble-time, .peer-title, .from-name, .reply-wrapper, ' +
            '.download-progress, .document-size, .message-transfer-progress, .file-status, ' +
            '.audio-duration, .audio-author, .download-button, .document-ext, .status, .svg-icon, ' +
            '.video-duration, .media-duration'
        );
        elementsToRemove.forEach(el => el.remove());

        text = (bubbleClone.innerText || bubbleClone.textContent || '').trim();

        const links = bubble.querySelectorAll('a');
        let linkTexts = [];
        links.forEach(link => {
            const linkText = link.textContent.trim();
            const href = link.href;
            if (href && href !== linkText && !text.includes(href)) {
                linkTexts.push(`${linkText} => ${href}`);
            }
        });
        if (linkTexts.length > 0) {
            text += ' ' + linkTexts.join(' ');
        }

        if (timeStr) {
            const escapedTime = timeStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            text = text.replace(new RegExp(`(${escapedTime})+`, 'g'), '').trim();
        }

        text = text.replace(/[\uE000-\uF8FF]/g, '');
        text = text.replace(/^\d{1,2}:\d{2}/gm, '');

        const fileNamesArray = Array.from(fileNamesSet);

        if (fileNamesArray.length > 0) {
            fileNamesArray.forEach(fname => {
                text = text.replace(fname, '').trim();
            });
        }

        text = cleanTextLines(text);

        if (fileNamesArray.length > 0) {
            const filesBlock = fileNamesArray.map(f => `📎 ${f}`).join(' /');
            if (text) {
                 text = text + ' /' + filesBlock;
            } else {
                 text = filesBlock;
            }
        }

        const hasMedia = bubble.querySelector('.media-container, .document, .audio, video, img.thumbnail, .photo, .audio-element, .voice-message');
        if (!text && hasMedia && fileNamesSet.size === 0) {
             const isVoice = bubble.querySelector('.audio-element.voice, .audio.voice, .media-round, .is-voice, .voice-message');
             if (isVoice) text = '🎤 Голосовое сообщение';
             else text = '📎 Файл';
        }

        if (text) {
            text = text.replace(/file_(\d+)\.bin/gi, 'photo_$1_y.jpg');
            text = text.replace(/(voice_\d{2}-\d{2}-\d{4}_\d{2}-\d{2}-\d{2})(?!\.)/gi, '$1.ogg');
        }

        if (!text && !hasMedia) return null;

        let line = '';
        if (author) line += author + ': ';
        line += text;

        return { line, timeStr, mid, midNumber };
    }

    function selectLastMessage() {
        const status = document.getElementById('saver-status');
        const bubbles = document.querySelectorAll('.bubble');

        if (!bubbles || bubbles.length === 0) {
            if (!isFullMode) alert('Сообщения не найдены.');
            return;
        }

        document.body.classList.add('tg-safe-mode');

        let found = false;
        for (let i = bubbles.length - 1; i >= 0; i--) {
            const bubble = bubbles[i];

            if (bubble.classList.contains('service-msg') || bubble.classList.contains('is-date')) continue;
            if (bubble.classList.contains('is-selected')) {
                status.innerText = 'Last selected.';
                found = true;
                break;
            }

            let target = bubble.querySelector('.time') ||
                         bubble.querySelector('.message-time') ||
                         bubble.querySelector('.bubble-time') ||
                         bubble.querySelector('.select-checkbox');

            if (target && target.offsetParent !== null) {
                simulateClick(target);
                if (bubble.dataset.mid) processedMessages.add(bubble.dataset.mid);
                status.innerText = 'Last selected.';
                found = true;
                break;
            }
        }

        if (!found) status.innerText = 'Not found.';

        setTimeout(() => {
            if (!isRunning) document.body.classList.remove('tg-safe-mode');
        }, 500);
    }

    function toggleScroll() {
        const status = document.getElementById('saver-status');
        const container = findScrollableElement();

        if (!container) {
            if (!isFullMode) alert('Чат не найден! Откройте чат.');
            return;
        }

        isRunning = true;
        stuckCounter = 0;
        processedMessages.clear();
        messageBuffer.clear();

        document.querySelectorAll('.is-selected').forEach(b => {
            if (b.dataset.mid) processedMessages.add(b.dataset.mid);
        });

        document.body.classList.add('tg-safe-mode');

        status.innerText = 'Scrolling...';

        scrollInterval = setInterval(() => {
            if (!isRunning) return;

            const bubbles = document.querySelectorAll('.bubble');

            bubbles.forEach(bubble => {
                if (bubble.classList.contains('service-msg') || bubble.classList.contains('is-date')) return;

                const mid = bubble.dataset.mid;
                if (!mid) return;

                if (!messageBuffer.has(mid)) {
                    const messageData = extractMessageText(bubble);
                    if (messageData) {
                        messageBuffer.set(mid, messageData);
                    }
                }

                if (processedMessages.has(mid)) return;

                if (bubble.classList.contains('is-selected')) {
                    processedMessages.add(mid);
                    return;
                }

                let target = bubble.querySelector('.time') ||
                             bubble.querySelector('.message-time') ||
                             bubble.querySelector('.bubble-time') ||
                             bubble.querySelector('.select-checkbox');

                if (target && target.offsetParent !== null) {
                    simulateClick(target);
                    processedMessages.add(mid);
                }
            });

            if (container) {
                container.scrollBy({ top: -300, behavior: 'auto' });

                if (container.scrollTop <= 50) {
                    stuckCounter++;
                    status.innerText = `Top? ${stuckCounter}/5`;
                    updateProgress(messageBuffer.size, messageBuffer.size + 10, `(${messageBuffer.size})`);
                    if (stuckCounter >= 5) {
                        stopScroller(true);
                    }
                } else {
                    stuckCounter = 0;
                    status.innerText = `Scrolling...`;
                    updateProgress(processedMessages.size, processedMessages.size + 50, `(${messageBuffer.size})`);
                }
            }
        }, 700);
    }

    function stopScroller(finished = false) {
        isRunning = false;
        clearInterval(scrollInterval);
        document.body.classList.remove('tg-safe-mode');

        const status = document.getElementById('saver-status');

        if (finished) {
            updateProgress(100, 100, '✅ Done!');
            status.innerText = `✅ Buffered: ${messageBuffer.size}`;
            log(`Скролл завершен. В буфере ${messageBuffer.size} сообщений.`);

            if (!isFullMode) {
                setTimeout(hideProgress, 3000);
            } else {
                setTimeout(hideProgress, 1000);
            }
        } else {
            hideProgress();
            status.innerText = `Stopped. Buffered: ${messageBuffer.size}`;
        }
    }

    function findScrollableElement() {
        const colCenter = document.querySelector('#column-center');
        if (!colCenter) return null;
        let el = colCenter.querySelector('.bubbles-container') || colCenter.querySelector('.scrollable-y');
        if (el) return el;
        const anyBubble = colCenter.querySelector('.bubble');
        if (anyBubble) {
            let parent = anyBubble.parentElement;
            while (parent && parent !== colCenter) {
                if (parent.scrollHeight > parent.clientHeight) return parent;
                parent = parent.parentElement;
            }
        }
        return null;
    }

    function getFilePriority(media) {
        const fileName = (media.file_name || '').toLowerCase();
        const mimeType = (media.mime_type || '').toLowerCase();

        if (fileName.endsWith('.txt') || mimeType.includes('text/plain')) return 1;
        if (fileName.endsWith('.doc') || fileName.endsWith('.docx') || mimeType.includes('msword')) return 2;
        if (mimeType.includes('audio') || fileName.endsWith('.ogg') || fileName.endsWith('.mp3')) return 3;
        if (mimeType.includes('zip') || mimeType.includes('rar') || fileName.endsWith('.zip')) return 4;
        if (fileName.endsWith('.pdf')) return 5;
        if (mimeType.includes('image')) return 6;
        if (mimeType.includes('video')) return 7;

        return 8;
    }

    async function downloadOne() {
        const status = document.getElementById('saver-status');
        status.innerText = 'ONE MSG...';
        await new Promise(r => setTimeout(r, 500));
        try {
            const msgs = await unsafeWindow.appImManager.chat.selection.getSelectedMessages();
            if (!msgs || msgs.length === 0) { alert('Нет выделения!'); return; }
            if (msgs.length > 1) { alert('Выделено > 1'); return; }
            const msg = msgs[0];
            if (!msg.media || (!msg.media.document && !msg.media.photo)) { alert('Нет файла.'); return; }
            const media = msg.media.document || msg.media.photo;
            log(`Downloading single: ${media.file_name || media.id}`);
            unsafeWindow.appDownloadManager.downloadToDisc({ media: media });
            status.innerText = '✅ OK';
        } catch (e) {
            console.error(e);
            alert('Ошибка API.');
        }
    }

    async function downloadSelected() {
        const status = document.getElementById('saver-status');
        status.innerText = 'Downloading...';

        await new Promise(r => setTimeout(r, 500));

        try {
            const msgs = await unsafeWindow.appImManager.chat.selection.getSelectedMessages();

            if (!msgs || msgs.length === 0) {
                if (!isFullMode) alert('Нет выделенных сообщений!');
                status.innerText = 'No selection.';
                return;
            }

            msgs.reverse();
            downloadedFilesMap.clear();

            let filesWithMedia = [];
            let groupedFiles = new Map();

            msgs.forEach((msg, originalIndex) => {
                if (msg.media && (msg.media.document || msg.media.photo)) {
                    const media = msg.media.document || msg.media.photo;

                    let ext = 'bin';
                    const mime = (media.mime_type || '').toLowerCase();
                    if (mime.includes('image') || mime.includes('jpg') || mime.includes('jpeg')) ext = 'jpg';
                    else if (mime.includes('audio/ogg')) ext = 'ogg';
                    else if (mime.includes('text/plain')) ext = 'txt';
                    else if (mime === 'application/pdf') ext = 'pdf';
                    else if (mime.includes('/')) ext = mime.split('/')[1];

                    if (media.file_name) {
                        const parts = media.file_name.split('.');
                        if (parts.length > 1) ext = parts.pop();
                    }
                    if (ext === 'jpeg') ext = 'jpg';
                    if (ext === 'quicktime') ext = 'mov';
                    if (ext === 'bin' && mime.includes('image')) ext = 'jpg';

                    let finalName = media.file_name;
                    if (!finalName) {
                        if (ext === 'jpg') finalName = `photo_${media.id}_y.jpg`;
                        else if (ext === 'ogg' || (mime.includes('audio'))) finalName = `voice_${media.id}.ogg`;
                        else finalName = `file_${media.id}.${ext}`;
                    }

                    if (msg.grouped_id) {
                        const gid = msg.grouped_id.toString();
                        if (!groupedFiles.has(gid)) groupedFiles.set(gid, []);
                        groupedFiles.get(gid).push(finalName);
                    }

                    filesWithMedia.push({
                        msg: msg,
                        media: media,
                        originalIndex: originalIndex,
                        priority: getFilePriority(media),
                        mid: msg.mid,
                        finalName: finalName,
                        grouped_id: msg.grouped_id
                    });
                }
            });

            filesWithMedia.sort((a, b) => a.priority - b.priority);

            log(`📋 Download order: ${filesWithMedia.length} files`);
            let count = 0;

            filesWithMedia.forEach((item, downloadIndex) => {
                setTimeout(() => {
                    const media = item.media;
                    const oldFileName = item.finalName;

                    let filesToStore = [oldFileName];
                    if (item.grouped_id) {
                        const gid = item.grouped_id.toString();
                        if (groupedFiles.has(gid)) {
                            filesToStore = [...new Set(groupedFiles.get(gid))];
                        }
                    }

                    if (item.mid) {
                        const mKey = item.mid.toString();
                        downloadedFilesMap.set(mKey, filesToStore);
                    }

                    unsafeWindow.appDownloadManager.downloadToDisc({ media: media });

                    updateProgress(downloadIndex + 1, filesWithMedia.length, `(${downloadIndex + 1}/${filesWithMedia.length})`);
                    status.innerText = `D/L ${downloadIndex + 1}/${filesWithMedia.length}`;

                }, downloadIndex * 250);
                count++;
            });

            setTimeout(() => {
                status.innerText = `Downloaded ${count}`;
                log(`✅ Download finished. Map size: ${downloadedFilesMap.size}`);
                setTimeout(hideProgress, 2000);
            }, filesWithMedia.length * 250 + 1000);

        } catch (e) {
            console.error(e);
            hideProgress();
        }
    }

    function exportTXT() {
        const status = document.getElementById('saver-status');

        if (messageBuffer.size === 0) {
            if (!isFullMode) alert('Буфер пуст! Сначала скролл.');
            return;
        }

        status.innerText = 'Exporting TXT...';
        updateProgress(0, 100, 'Re-processing...');

        const messages = [];

        messageBuffer.forEach((oldMsg, mid) => {
            const bubble = document.querySelector(`.bubble[data-mid="${mid}"]`);
            let finalMsg = oldMsg;

            if (bubble) {
                const reExtracted = extractMessageText(bubble);
                if (reExtracted) finalMsg = reExtracted;
            } else if (downloadedFilesMap.has(mid)) {
                const storedFiles = downloadedFilesMap.get(mid);
                let filesList = Array.isArray(storedFiles) ? storedFiles : [storedFiles];

                const missingFiles = filesList.filter(f => !oldMsg.line.includes(f));

                if (missingFiles.length > 0) {
                     const timePart = oldMsg.timeStr ? `[${oldMsg.timeStr}]` : '';
                     const filesBlock = missingFiles.map(f => `📎 ${f}`).join(' /');

                     let cleanOldLine = oldMsg.line.replace(timePart, '').trim();
                     cleanOldLine = cleanTextLines(cleanOldLine);

                     if (cleanOldLine) {
                         finalMsg.line = `${cleanOldLine} /${filesBlock}`;
                     } else {
                         finalMsg.line = `${filesBlock}`;
                     }
                }
            }

            if (finalMsg.line) {
                 finalMsg.line = finalMsg.line.replace(/file_(\d+)\.bin/gi, 'photo_$1_y.jpg');
                 finalMsg.line = finalMsg.line.replace(/(voice_\d{2}-\d{2}-\d{4}_\d{2}-\d{2}-\d{2})(?!\.)/gi, '$1.ogg');
            }

            messages.push(finalMsg);
        });

        messages.sort((a, b) => a.midNumber - b.midNumber);

        // Нумерация 1) 2) ...
        const lines = messages.map((msg, index) => `${index + 1}) ${msg.line}`);

        const fullText = lines.join('\n__________________________\n\n');

        const blob = new Blob([fullText], { type: 'text/plain;charset=utf-8;' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;

        const chatName = getChatTitle();
        a.download = `${chatName}.txt`;

        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);

        status.innerText = `TXT Saved!`;
    }

    async function runFullMode() {
        const status = document.getElementById('saver-status');
        const fullBtn = document.getElementById('saver-full-btn');

        isFullMode = true;
        fullBtn.disabled = true;

        selectLastMessage();
        await new Promise(r => setTimeout(r, 1000));

        toggleScroll();
        await new Promise(resolve => {
            const checkInterval = setInterval(() => {
                if (!isRunning) {
                    clearInterval(checkInterval);
                    resolve();
                }
            }, 1000);
        });

        await new Promise(r => setTimeout(r, 2000));

        await downloadSelected();

        const msgs = await unsafeWindow.appImManager.chat.selection.getSelectedMessages();
        const filesCount = msgs.filter(m => m.media).length;
        await new Promise(r => setTimeout(r, filesCount * 250 + 3000));

        exportTXT();

        playCompletionSound();

        setTimeout(() => {
            fullBtn.disabled = false;
            isFullMode = false;
            hideProgress();
            status.innerText = 'Ready';
        }, 3000);
    }

    // Запуск панели
    const observer = new MutationObserver(() => {
        createPanel();
    });
    observer.observe(document.body, { childList: true, subtree: true });
    setTimeout(createPanel, 2000);

})();