TTV Auto Upload

Tự động điền form đăng chương trên tangthuvien.net

目前為 2025-03-08 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         TTV Auto Upload
// @namespace    http://tampermonkey.net/
// @version      4.3
// @description  Tự động điền form đăng chương trên tangthuvien.net
// @author       HA
// @match        https://tangthuvien.net/dang-chuong/story/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // CSS tối giản
    const style = document.createElement('style');
    style.textContent = `
        .ttv-notification {position:fixed;top:20px;right:20px;padding:10px 20px;background:#4CAF50;color:white;border-radius:4px;z-index:9999;display:none}
        .ttv-error {background:#f44336}
        .ttv-control-panel {position:fixed;top:50px;right:20px;background:white;padding:25px;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.15);z-index:9998;width:500px;transition:all 0.3s ease}
        .ttv-control-panel.minimized {width:auto;height:auto;padding:10px;opacity:0.8;transform:translateX(calc(100% - 40px))}
        .ttv-control-panel.minimized:hover {opacity:1;transform:translateX(0)}
        .ttv-control-panel.minimized .ttv-button-group,.ttv-control-panel.minimized .ttv-header {display:none}
        .ttv-button-group {display:flex;flex-direction:column;gap:15px}
        .ttv-content-editor {width:100%;height:100px;margin:6px 0;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;resize:vertical}
        .ttv-preview {display:none;width:100%;height:100px;margin:6px 0;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;background:#f9f9f9}
        .ttv-heading {font-size:15px;color:#555;margin:12px 0;display:flex;justify-content:space-between;align-items:center}
        .ttv-word-count {font-size:12px;color:#888;padding:3px 8px;background:#f5f5f5;border-radius:4px}
        .ttv-chapter-list {width:100%;margin:10px 0;max-height:200px;overflow-y:auto;border:1px solid #eee;border-radius:6px;padding:8px}
        .ttv-chapter-item {padding:8px;border-bottom:1px solid #eee;cursor:pointer;font-size:12px;color:#666}
        .ttv-chapter-item .chapter-title {font-weight:bold;margin-bottom:4px}
        .ttv-chapter-item .chapter-name {color:#888;padding-left:10px;border-left:2px solid #ddd;margin:4px 0}
        .ttv-chapter-item .chapter-stats {font-size:11px;color:#999}
        .ttv-chapter-item:last-child {border-bottom:none}
        .ttv-chapter-item.selected {background:#f0f8ff;border-left:2px solid #5bc0de}
        button.btn-warning {background:#f0ad4e;color:white;border:none;padding:12px;border-radius:6px;width:100%;font-size:14px}
        button.btn-warning:hover {background:#ec971f}
        button.ttv-minimize {padding:2px 8px;background:none;border:none;cursor:pointer;font-size:16px;color:#666}
    `;
    document.head.appendChild(style);

    // State management
    const dăngnhanhTTV = {
        STATE: {
            CHAP_NUMBER: 1,
            CHAP_STT: 1,
            CHAP_SERIAL: 1,
            CHAP_NUMBER_ORIGINAL: 1,
            CHAP_STT_ORIGINAL: 1,
            CHAP_SERIAL_ORIGINAL: 1
        },
        initializeChapterValues: function() {
            try {
                const chap_number = parseInt(jQuery('#chap_number').val());
                let chap_stt = parseInt(jQuery('.chap_stt1').val());
                let chap_serial = parseInt(jQuery('.chap_serial').val());

                if (parseInt(jQuery('#chap_stt').val()) > chap_stt) {
                    chap_stt = parseInt(jQuery('#chap_stt').val());
                }
                if (parseInt(jQuery('#chap_serial').val()) > chap_serial) {
                    chap_serial = parseInt(jQuery('#chap_serial').val());
                }

                this.STATE.CHAP_NUMBER = this.STATE.CHAP_NUMBER_ORIGINAL = chap_number || 1;
                this.STATE.CHAP_STT = this.STATE.CHAP_STT_ORIGINAL = chap_stt || 1;
                this.STATE.CHAP_SERIAL = this.STATE.CHAP_SERIAL_ORIGINAL = chap_serial || 1;
            } catch (e) {
                console.error("Error initializing chapter values:", e);
            }
        }
    };

    // UI Elements
    const notification = document.createElement('div');
    notification.className = 'ttv-notification';
    document.body.appendChild(notification);

    // Hiển thị thông báo
    function showNotification(message, isError = false) {
        notification.textContent = message;
        notification.className = 'ttv-notification' + (isError ? ' ttv-error' : '');
        notification.style.display = 'block';
        setTimeout(() => notification.style.display = 'none', 3000);
    }

    // Phân tích chương
    function parseChapters(content) {
        const lines = content.split('\n');
        const chapters = [];
        let currentChapter = {title: '', name: '', content: []};
        const chapterPattern = /^\s*Chương\s+\d+:/;

        // Biến để kiểm soát tiêu đề trùng lặp
        let chapterTitles = new Set();
        let chapterNumbers = new Map(); // Lưu số chương đã gặp
        let duplicateCount = 0;

        // Mảng lưu nội dung của các dòng trống gần nhất
        let emptyLineBuffer = [];
        // Biến để theo dõi xem ta có đang ở trong một khối tiêu đề lặp không
        let inDuplicatedTitleBlock = false;
        // Theo dõi tiêu đề chương trước đó
        let previousChapterLine = '';
        // Khoảng cách dòng tối thiểu giữa các tiêu đề hợp lệ
        const MIN_LINES_BETWEEN_CHAPTERS = 5;
        // Số dòng đã qua kể từ tiêu đề cuối
        let linesSinceLastTitle = 0;

        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            const trimmedLine = line.trim();
            linesSinceLastTitle++;

            // Xử lý dòng trống
            if (trimmedLine === '') {
                emptyLineBuffer.push(line);
                continue;
            }

            if (chapterPattern.test(trimmedLine)) {
                // Trích xuất số chương từ tiêu đề
                const chapterMatch = trimmedLine.match(/Chương\s+(\d+):/);
                const chapterNum = chapterMatch ? parseInt(chapterMatch[1]) : 0;
                const chapterName = trimmedLine.split(':')[1]?.trim() || '';

                // Kiểm tra các điều kiện để xác định tiêu đề chương hợp lệ
                const isPotentialChapter = linesSinceLastTitle > MIN_LINES_BETWEEN_CHAPTERS || chapters.length === 0;
                const isFullTitle = chapterName.length > 2; // Tiêu đề đầy đủ phải có ít nhất 3 ký tự sau dấu :
                const isDuplicateOfPrevious = trimmedLine === previousChapterLine;

                if (isPotentialChapter && isFullTitle && !isDuplicateOfPrevious) {
                    // Lưu chương hiện tại nếu có
                    if (currentChapter.title && currentChapter.content.length > 0) {
                        chapters.push({...currentChapter});
                        currentChapter = {title: '', name: '', content: []};
                    }

                    // Khởi tạo chương mới
                    currentChapter.title = trimmedLine;
                    let name = chapterName;
                    name = name.replace(/^[.,;'"]+|[.,;'"]+$/g, '').trim();
                    currentChapter.name = name;
                    
                    // Lưu tiêu đề chương vào nội dung để giữ nguyên định dạng gốc
                    currentChapter.content.push(line);

                    // Đánh dấu đã gặp tiêu đề và số chương này
                    chapterTitles.add(trimmedLine);
                    chapterNumbers.set(chapterNum, true);

                    // Đặt lại các biến theo dõi
                    inDuplicatedTitleBlock = false;
                    linesSinceLastTitle = 0;
                    previousChapterLine = trimmedLine;
                    emptyLineBuffer = [];
                } else {
                    // Đây là một tiêu đề bị lặp, xử lý như nội dung chương thông thường
                    if (currentChapter.title) {
                        // Thêm các dòng trống đã lưu trữ
                        currentChapter.content.push(...emptyLineBuffer);
                        emptyLineBuffer = [];
                        // Thêm dòng hiện tại
                        currentChapter.content.push(line);
                    }
                    inDuplicatedTitleBlock = true;
                    duplicateCount++;
                }
            } else if (currentChapter.title) {
                // Giữ nguyên định dạng gốc của dòng (không trim), 
                // bao gồm cả khoảng trắng, xuống dòng và định dạng đặc biệt
                currentChapter.content.push(line);
            }
        }

        if (currentChapter.title && currentChapter.content.length > 0) {
            chapters.push({...currentChapter});
        }

        if (chapters.length > 0) {
            displayChapters(chapters);
            if (duplicateCount > 0) {
                showNotification(`Đã tìm thấy ${chapters.length} chương và bỏ qua ${duplicateCount} tiêu đề lặp lại.`);
            } else {
                showNotification(`Đã tìm thấy ${chapters.length} chương và tự động điền vào form.`);
            }

            setTimeout(() => {
                try {
                    transferContent();
                } catch (error) {
                    console.error('Lỗi khi tự động điền form:', error);
                    showNotification('Có lỗi xảy ra khi tự động điền form!', true);
                }
            }, 1000);
        }

        return chapters;
    }

    // Tạo HTML cho form chương
    function createChapterHTML(chapNum) {
        const chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
        const chap_vol_name = jQuery('.chap_vol_name').val() || '';
        return `
        <div data-gen="MK_GEN" id="COUNT_CHAP_${chapNum}_MK">
            <div class="col-xs-12 form-group"></div>
            <div class="form-group">
                <label class="col-sm-2" for="chap_stt">STT</label>
                <div class="col-sm-8">
                    <input class="form-control" required name="chap_stt[${chapNum}]" value="${dăngnhanhTTV.STATE.CHAP_STT}" placeholder="Số thứ tự của chương" type="text"/>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="chap_number">Chương thứ..</label>
                <div class="col-sm-8">
                    <input value="${dăngnhanhTTV.STATE.CHAP_SERIAL}" required class="form-control" name="chap_number[${chapNum}]" placeholder="Chương thứ.. (1,2,3..)" type="text"/>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="chap_name">Quyển số</label>
                <div class="col-sm-8">
                    <input class="form-control" name="vol[${chapNum}]" placeholder="Quyển số" type="number" value="${chap_vol}" required/>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="chap_name">Tên quyển</label>
                <div class="col-sm-8">
                    <input class="form-control chap_vol_name" name="vol_name[${chapNum}]" placeholder="Tên quyển" type="text" value="${chap_vol_name}" />
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="chap_name">Tên chương</label>
                <div class="col-sm-8">
                    <input required class="form-control" name="chap_name[${chapNum}]" placeholder="Tên chương" type="text"/>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="introduce">Nội dung</label>
                <div class="col-sm-8">
                    <textarea maxlength="75000" style="color:#000;font-weight: 400;" required class="form-control" name="introduce[${chapNum}]" rows="20" placeholder="Nội dung" type="text"></textarea>
                    <div class="chapter-character-count"></div>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="adv">Quảng cáo</label>
                <div class="col-sm-8">
                    <textarea maxlength="1000" class="form-control" name="adv[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea>
                </div>
            </div>
        </div>`;
    }

    // Chuyển nội dung vào form
    async function transferContent() {
        try {
            const chapterItems = document.querySelectorAll('.ttv-chapter-item');
            if (!chapterItems.length) {
                throw new Error('Không tìm thấy chương nào để điền vào form');
            }

            const form = document.querySelector('form[name="postChapForm"]');
            if (!form) {
                throw new Error('Không tìm thấy form đăng chương');
            }

            let chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
            let chap_vol_name = jQuery('.chap_vol_name').val() || '';

            let maxChapStt = 0;
            let maxChapSerial = 0;

            const existingForms = form.querySelectorAll('[id^="COUNT_CHAP_"]');

            existingForms.forEach(formElem => {
                const formIdMatch = formElem.id.match(/COUNT_CHAP_(\d+)_MK/);
                if (formIdMatch && formIdMatch[1]) {
                    const formIndex = parseInt(formIdMatch[1]);

                    const sttInput = formElem.querySelector(`input[name="chap_stt[${formIndex}]"]`);
                    if (sttInput && sttInput.value && !isNaN(parseInt(sttInput.value))) {
                        const sttVal = parseInt(sttInput.value);
                        if (sttVal > maxChapStt) {
                            maxChapStt = sttVal;
                        }
                    }

                    const serialInput = formElem.querySelector(`input[name="chap_number[${formIndex}]"]`);
                    if (serialInput && serialInput.value && !isNaN(parseInt(serialInput.value))) {
                        const serialVal = parseInt(serialInput.value);
                        if (serialVal > maxChapSerial) {
                            maxChapSerial = serialVal;
                        }
                    }
                }
            });

            if (maxChapStt > 0) {
                dăngnhanhTTV.STATE.CHAP_STT = maxChapStt;
            }

            if (maxChapSerial > 0) {
                dăngnhanhTTV.STATE.CHAP_SERIAL = maxChapSerial;
            }

            // Chỉ lấy form đầu tiên hiện có hoặc tạo form mới nếu chưa có
            const existingFormCount = existingForms.length;
            
            // Chỉ xử lý chương đầu tiên
            if (chapterItems.length > 0) {
                // Sử dụng form hiện có nếu đã có, tạo mới nếu chưa có
                let formIndex = 1;
                let formElement = null;
                
                if (existingFormCount === 0) {
                    // Tạo form mới nếu chưa có form nào
                    dăngnhanhTTV.STATE.CHAP_STT++;
                    dăngnhanhTTV.STATE.CHAP_SERIAL++;
                    
                    const chapterHTML = createChapterHTML(formIndex);
                    const tempDiv = document.createElement('div');
                    tempDiv.innerHTML = chapterHTML;
                    formElement = tempDiv.firstElementChild;
                    
                    if (!formElement) {
                        throw new Error(`Không thể tạo element form cho chương ${formIndex}`);
                    }
                    
                    form.appendChild(formElement);
                } else {
                    // Sử dụng form đầu tiên đã có
                    formElement = existingForms[0];
                    const formIdMatch = formElement.id.match(/COUNT_CHAP_(\d+)_MK/);
                    if (formIdMatch && formIdMatch[1]) {
                        formIndex = parseInt(formIdMatch[1]);
                    }
                }

                const chapterItem = chapterItems[0];
                const titleElement = chapterItem.querySelector('.chapter-title');
                const nameElement = chapterItem.querySelector('.chapter-name');

                if (!titleElement || !nameElement) {
                    throw new Error(`Thiếu thông tin tiêu đề hoặc tên cho chương ${formIndex}`);
                }

                const chapterTitle = titleElement.textContent;
                const chapterName = nameElement.textContent.replace('Tên chương: ', '');

                const formFields = {
                    chapterName: form.querySelector(`input[name="chap_name[${formIndex}]"]`),
                    content: form.querySelector(`textarea[name="introduce[${formIndex}]"]`),
                    chapterNumber: form.querySelector(`input[name="chap_number[${formIndex}]"]`),
                    chapterOrder: form.querySelector(`input[name="chap_stt[${formIndex}]"]`),
                    volume: form.querySelector(`input[name="vol[${formIndex}]"]`),
                    volumeName: form.querySelector(`input[name="vol_name[${formIndex}]"]`),
                    advertisement: form.querySelector(`textarea[name="adv[${formIndex}]"]`)
                };

                formFields.chapterName.value = chapterName;
                if (existingFormCount === 0) {
                    formFields.chapterNumber.value = dăngnhanhTTV.STATE.CHAP_SERIAL.toString();
                    formFields.chapterOrder.value = dăngnhanhTTV.STATE.CHAP_STT.toString();
                }
                formFields.volume.value = chap_vol;
                formFields.volumeName.value = chap_vol_name;

                if (chapterItem._content) {
                    // Đảm bảo giữ nguyên định dạng gốc của nội dung, không trim
                    formFields.content.value = chapterItem._content;
                } else {
                    formFields.content.value = '';
                }

                formFields.advertisement.value = '';

                const inputEvent = new Event('input', { bubbles: true });
                formFields.content.dispatchEvent(inputEvent);
                
                showNotification(`Đã tự động điền nội dung chương vào form!`);
            }

        } catch (error) {
            console.error('Lỗi khi điền form:', error);
            showNotification('Có lỗi xảy ra khi điền form: ' + error.message, true);
        }
    }

    // Hiển thị danh sách chương
    function displayChapters(chapters) {
        const chapterList = document.createElement('div');
        chapterList.className = 'ttv-chapter-list';

        chapters.forEach((chapter, index) => {
            const chapterItem = document.createElement('div');
            chapterItem.className = 'ttv-chapter-item';
            // Đảm bảo giữ nguyên định dạng gốc bằng cách không trim giá trị
            // Giữ nguyên toàn bộ nội dung bao gồm cả tiêu đề
            chapterItem._content = chapter.content.join('\n');

            // Trích xuất số chương để hiển thị
            const chapterMatch = chapter.title.match(/Chương\s+(\d+):/);
            const chapterNum = chapterMatch ? chapterMatch[1] : '?';

            const titleDiv = document.createElement('div');
            titleDiv.className = 'chapter-title';
            titleDiv.textContent = chapter.title;

            const nameDiv = document.createElement('div');
            nameDiv.className = 'chapter-name';
            nameDiv.textContent = `Tên chương: ${chapter.name}`;

            const statsDiv = document.createElement('div');
            statsDiv.className = 'chapter-stats';
            statsDiv.textContent = `Chương ${chapterNum} | ${chapter.content.length} dòng`;

            chapterItem.appendChild(titleDiv);
            chapterItem.appendChild(nameDiv);
            chapterItem.appendChild(statsDiv);
            chapterItem.onclick = () => selectChapter(chapter, index);
            chapterList.appendChild(chapterItem);

            if (index === 0) {
                chapterItem.classList.add('selected');
            }
        });

        const existingList = document.querySelector('.ttv-chapter-list');
        if (existingList) {
            existingList.remove();
        }

        const contentEditor = document.querySelector('.ttv-content-editor');
        contentEditor.parentNode.insertBefore(chapterList, contentEditor);

        if (chapters.length > 0) {
            selectChapter(chapters[0], 0);
        }
    }

    // Chọn chương
    function selectChapter(chapter, index) {
        const contentEditor = document.querySelector('.ttv-content-editor');
        // Giữ nguyên định dạng gốc của nội dung
        contentEditor.value = chapter.title + '\n' + chapter.content.join('\n');

        const wordCountSpan = document.querySelector('.ttv-word-count');
        if (wordCountSpan) {
            wordCountSpan.textContent = updateWordCount(contentEditor.value);
        }

        const chapterItems = document.querySelectorAll('.ttv-chapter-item');
        chapterItems.forEach(item => item.classList.remove('selected'));
        chapterItems[index]?.classList.add('selected');

        try {
            transferContent();
        } catch (error) {
            console.error('Lỗi khi tự động điền form:', error);
            showNotification('Có lỗi xảy ra khi tự động điền form!', true);
        }
    }

    // Tính số từ
    function updateWordCount(content) {
        const wordCount = content.trim().split(/\s+/).length;
        const charCount = content.length;
        return `${wordCount} từ | ${charCount} ký tự`;
    }

    // Xem trước
    function togglePreview() {
        const contentEditor = document.querySelector('.ttv-content-editor');
        const preview = document.querySelector('.ttv-preview');
        const previewBtn = document.querySelector('.ttv-preview-btn');

        if (contentEditor.style.display !== 'none') {
            contentEditor.style.display = 'none';
            preview.style.display = 'block';
            preview.innerHTML = contentEditor.value.replace(/\n/g, '<br>');
            previewBtn.textContent = 'Soạn thảo';
        } else {
            contentEditor.style.display = 'block';
            preview.style.display = 'none';
            previewBtn.textContent = 'Xem trước';
        }
    }

    // Toàn màn hình
    function toggleFullscreen() {
        const panel = document.querySelector('.ttv-control-panel');
        const fullscreenBtn = document.querySelector('.ttv-fullscreen-btn');
        panel.classList.toggle('fullscreen');
        fullscreenBtn.textContent = panel.classList.contains('fullscreen') ? 'Thu nhỏ' : 'Toàn màn hình';
    }

    // Tạo panel điều khiển
    function addControlPanel() {
        const panel = document.createElement('div');
        panel.className = 'ttv-control-panel';

        const header = document.createElement('div');
        header.className = 'ttv-header';
        header.innerHTML = `
            <div>Soạn Thảo Nội Dung</div>
            <div class="ttv-toolbar">
                <button class="ttv-preview-btn" onclick="togglePreview()">Xem trước</button>
                <button class="ttv-fullscreen-btn" onclick="toggleFullscreen()">Toàn màn hình</button>
                <button class="ttv-minimize">−</button>
            </div>
        `;

        const buttonGroup = document.createElement('div');
        buttonGroup.className = 'ttv-button-group';

        const contentEditorLabel = document.createElement('div');
        contentEditorLabel.className = 'ttv-heading';
        contentEditorLabel.innerHTML = `
            Nội dung chương:
            <span class="ttv-word-count">0 từ | 0 ký tự</span>
        `;

        const contentEditor = document.createElement('textarea');
        contentEditor.className = 'ttv-content-editor';
        contentEditor.placeholder = 'Nhập hoặc dán nội dung chương vào đây...';

        const preview = document.createElement('div');
        preview.className = 'ttv-preview';

        contentEditor.oninput = () => {
            const wordCountSpan = document.querySelector('.ttv-word-count');
            if (wordCountSpan) {
                wordCountSpan.textContent = updateWordCount(contentEditor.value);
            }
        };

        contentEditor.onpaste = (e) => {
            setTimeout(() => {
                const content = contentEditor.value;
                const chapters = parseChapters(content);
                if (chapters.length > 0) {
                    displayChapters(chapters);
                    setTimeout(() => {
                        try {
                            transferContent();
                        } catch (error) {
                            console.error('Lỗi khi tự động điền form:', error);
                            showNotification('Có lỗi xảy ra khi tự động điền form!', true);
                        }
                    }, 1000);
                }
            }, 0);
        };

        const transferBtn = document.createElement('button');
        transferBtn.type = 'button';
        transferBtn.className = 'btn btn-warning';
        transferBtn.innerHTML = '<span>Chuyển nội dung sang form</span>';
        transferBtn.onclick = transferContent;

        panel.appendChild(header);
        buttonGroup.appendChild(contentEditorLabel);
        buttonGroup.appendChild(contentEditor);
        buttonGroup.appendChild(preview);
        buttonGroup.appendChild(transferBtn);
        panel.appendChild(buttonGroup);

        document.body.appendChild(panel);

        const minimizeBtn = panel.querySelector('.ttv-minimize');
        minimizeBtn.onclick = () => {
            panel.classList.toggle('minimized');
            minimizeBtn.innerHTML = panel.classList.contains('minimized') ? '+' : '−';
        };

        window.togglePreview = togglePreview;
        window.toggleFullscreen = toggleFullscreen;
    }

    window.addEventListener('load', function() {
        dăngnhanhTTV.initializeChapterValues();
        addControlPanel();
    });
})();