LingQ Addon

Provides custom LingQ layouts

当前为 2025-05-11 提交的版本,查看 最新版本

// ==UserScript==
// @name         LingQ Addon
// @description  Provides custom LingQ layouts
// @match        https://www.lingq.com/*/learn/*/web/reader/*
// @match        https://www.lingq.com/*/learn/*/web/library/course/*
// @exclude      https://www.lingq.com/*/learn/*/web/editor/*
// @version      4.5.1
// @grant       GM_setValue
// @grant       GM_getValue
// @namespace https://gf.qytechs.cn/users/1458847
// ==/UserScript==

(function () {
    "use strict";

    const storage = {
        get: (key, defaultValue) => {
            const value = GM_getValue(key);
            return value === undefined ? defaultValue : value;
        },
        set: (key, value) => GM_setValue(key, value)
    };

    const defaults = {
        styleType: "video",
        colorMode: "dark",
        fontSize: 1.1,
        lineHeight: 1.7,
        heightBig: 400,
        sentenceHeight: 400,
        darkColors: {
            fontColor: "#e0e0e0",
            lingqBackground: "rgba(109, 89, 44, 0.7)",
            lingqBorder: "rgba(254, 203, 72, 0.3)",
            lingqBorderLearned: "rgba(254, 203, 72, 0.5)",
            blueBorder: "rgba(72, 154, 254, 0.5)",
            playingUnderline: "#ffffff"
        },
        whiteColors: {
            fontColor: "#000000",
            lingqBackground: "rgba(255, 200, 0, 0.4)",
            lingqBorder: "rgba(255, 200, 0, 0.3)",
            lingqBorderLearned: "rgba(255, 200, 0, 1)",
            blueBorder: "rgba(0, 111, 255, 0.3)",
            playingUnderline: "#000000"
        },
        librarySortOption: 0,
        autoFinishing: false
    };

    const settings = {
        styleType: storage.get("styleType", defaults.styleType),
        colorMode: storage.get("colorMode", defaults.colorMode),
        fontSize: storage.get("fontSize", defaults.fontSize),
        lineHeight: storage.get("lineHeight", defaults.lineHeight),
        heightBig: storage.get("heightBig", defaults.heightBig),
        sentenceHeight: storage.get("sentenceHeight", defaults.sentenceHeight),
        librarySortOption: storage.get("librarySortOption", defaults.librarySortOption),
        autoFinishing: storage.get("autoFinishing", defaults.autoFinishing)
    };

    const colorSettings = getColorSettings(settings.colorMode);

    let styleElement = null;

    function getColorSettings(colorMode) {
        const prefix = colorMode === "dark" ? "dark_" : "white_";
        const defaultColors = colorMode === "dark" ? defaults.darkColors : defaults.whiteColors;

        return {
            fontColor: storage.get(prefix + "fontColor", defaultColors.fontColor),
            lingqBackground: storage.get(prefix + "lingqBackground", defaultColors.lingqBackground),
            lingqBorder: storage.get(prefix + "lingqBorder", defaultColors.lingqBorder),
            lingqBorderLearned: storage.get(prefix + "lingqBorderLearned", defaultColors.lingqBorderLearned),
            blueBorder: storage.get(prefix + "blueBorder", defaultColors.blueBorder),
            playingUnderline: storage.get(prefix + "playingUnderline", defaultColors.playingUnderline)
        };
    }

    function createElement(tag, props = {}) {
        const element = document.createElement(tag);
        Object.entries(props).forEach(([key, value]) => {
            if (key === "style" && typeof value === "string") {
                element.style.cssText = value;
            } else if (key === "textContent") {
                element.textContent = value;
            } else {
                element[key] = value;
            }
        });
        return element;
    }

    function createSettingsPopup() {
        const popup = createElement("div", {id: "lingqAddonSettingsPopup"});

        // drag handle
        const dragHandle = createElement("div", {id: "lingqAddonSettingsDragHandle"});

        const dragHandleTitle = createElement("h3", {textContent: "LingQ Addon Settings"});
        dragHandle.appendChild(dragHandleTitle);

        // popup content
        const content = createElement("div", {style: `padding: 0 10px;`});
        const popupContentElement = generatePopupContent();
        content.appendChild(popupContentElement);

        popup.appendChild(dragHandle);
        popup.appendChild(content);

        return popup;
    }

    function generatePopupContent() {
        function addSelect(parent, id, labelText, options, selectedValue) {
            const container = createElement("div", {className: "popup-row"});
            container.appendChild(createElement("label", {htmlFor: id, textContent: labelText}));

            const select = createElement("select", {id, style: "width: 100%; margin-top: 5px; padding: 5px;"});
            options.forEach(option => {
                select.appendChild(createElement("option", {value: option.value, textContent: option.text, selected: selectedValue === option.value}));
            });

            container.appendChild(select);
            parent.appendChild(container);
            return container;
        }

        function addSlider(parent, id, labelText, valueId, value, unit, min, max, step) {
            const container = createElement("div", {className: "popup-row"});

            const label = createElement("label", { htmlFor: id });
            label.appendChild(document.createTextNode(labelText + " "));
            label.appendChild(createElement("span", { id: valueId, textContent: value }));
            if (unit) label.appendChild(document.createTextNode(unit));

            container.appendChild(label);
            container.appendChild(createElement("input", {type: "range", id, min, max, step, value, style: "width: 100%;"}));

            parent.appendChild(container);
            return container;
        }

        function addColorPicker(parent, id, labelText, value) {
            const container = createElement("div", {className: "popup-row"});
            container.appendChild(createElement("label", {htmlFor: id + "Text", textContent: labelText}));

            const flexContainer = createElement("div", {style: "display: flex; align-items: center;"});
            flexContainer.appendChild(createElement("div", {id: id + "Picker", className: "color-picker" }));
            flexContainer.appendChild(createElement("input", {type: "text", id: id + "Text", value, style: "flex-grow: 1; margin-left: 10px;"}));

            container.appendChild(flexContainer);
            parent.appendChild(container);
            return container;
        }

        function addCheckbox(parent, id, labelText, checked) {
            const container = createElement("div", {className: "popup-row"});
            const label = createElement("label", {htmlFor: id, textContent: labelText});
            const checkbox = createElement("input", {type: "checkbox", id, checked, style: "margin-left: 10px;"});

            label.style.display = "flex";
            label.style.alignItems = "center";
            container.appendChild(label);
            label.appendChild(checkbox);
            parent.appendChild(container);

            return container;
        }

        const container = createElement("div");

        addSelect(container, "styleTypeSelector", "Layout Style:", [
            { value: "video", text: "Video" },
            { value: "video2", text: "Video2" },
            { value: "audio", text: "Audio" },
            { value: "off", text: "Off" }
        ], settings.styleType);

        const videoSettings = createElement("div", {
            id: "videoSettings",
            style: `${settings.styleType === "video" ? "" : "display: none"}`
        });
        addSlider(videoSettings, "heightBigSlider", "Video Height:", "heightBigValue", settings.heightBig, "px", 300, 800, 10);
        container.appendChild(videoSettings);

        const sentenceVideoSettings = createElement("div", {
            id: "sentenceVideoSettings",
            style: `${settings.styleType === "off" ? "" : "display: none"}`
        });
        addSlider(sentenceVideoSettings, "sentenceHeightSlider", "Sentence Video Height:", "sentenceHeightValue", settings.heightBig, "px", 300, 600, 10);
        container.appendChild(sentenceVideoSettings);

        addSlider(container, "fontSizeSlider", "Font Size:", "fontSizeValue", settings.fontSize, "rem", 0.8, 1.8, 0.05);
        addSlider(container, "lineHeightSlider", "Line Height:", "lineHeightValue", settings.lineHeight, "", 1.2, 3.0, 0.1);

        const colorSection = createElement("div", {
            style: "border: 1px solid var(--font_color, #e0e0e0); padding: 0 10px; border-radius: 5px;"
        });

        addSelect(colorSection, "colorModeSelector", "Color Mode:", [
            { value: "dark", text: "Dark" },
            { value: "white", text: "White" }
        ], settings.colorMode);

        [
            { id: "fontColor", label: "Font Color:", value: colorSettings.fontColor },
            { id: "lingqBackground", label: "LingQ Background:", value: colorSettings.lingqBackground },
            { id: "lingqBorder", label: "LingQ Border:", value: colorSettings.lingqBorder },
            { id: "lingqBorderLearned", label: "LingQ Border Learned:", value: colorSettings.lingqBorderLearned },
            { id: "blueBorder", label: "Blue Border:", value: colorSettings.blueBorder },
            { id: "playingUnderline", label: "Playing Underline:", value: colorSettings.playingUnderline }
        ].forEach(config => addColorPicker(colorSection, config.id, config.label, config.value));

        container.appendChild(colorSection);

        addCheckbox(container, "autoFinishingCheckbox", "Finish Lesson Automatically", settings.autoFinishing);

        const buttonContainer = createElement("div", {style: "display: flex; justify-content: space-between;", className: "popup-row"});
        [
            {id: "resetSettingsBtn", textContent: "Reset", className: "popup-button"},
            {id: "closeSettingsBtn", textContent: "Close", className: "popup-button"}
        ].forEach((prop) => {
            buttonContainer.appendChild(createElement("button", prop));
        });

        container.appendChild(buttonContainer);
        return container;
    }

    function createDownloadWordsPopup() {
        const popup = createElement("div", {id: "lingqDownloadWordsPopup"});

        // drag handle
        const dragHandle = createElement("div", {id: "lingqDownloadWordsDragHandle"});

        const dragHandleTitle = createElement("h3", {textContent: "Download Words"});
        dragHandle.appendChild(dragHandleTitle);

        const content = createElement("div", {style: `padding: 0 10px;`});

        [
            {id: "downloadUnknownLingqsBtn", textContent: "Download Unknown LingQs (words + phrases)", className: "popup-button"},
            {id: "downloadUnknownLingqWordsBtn", textContent: "Download Unknown LingQ Words (1, 2, 3, 4)", className: "popup-button"},
            {id: "downloadUnknownLingqPhrasesBtn", textContent: "Download Unknown LingQ Phrases (1, 2, 3, 4)", className: "popup-button"},
            {id: "downloadKnownLingqsBtn", textContent: "Download Known LingQs (✓)", className: "popup-button"},
            {id: "downloadKnownWordsBtn", textContent: "Download Known Words ", className: "popup-button"}
        ].forEach((prop) => {
            let rowContainer = createElement("div", {className: "popup-row"});
            rowContainer.appendChild(createElement("button", prop))
            content.appendChild(rowContainer);
        });

        // Progress Bar Elements
        const progressContainer = createElement("div", {id: "downloadProgressContainer", className: "popup-row"});
        const progressText = createElement("div", {id: "downloadProgressText"});
        const progressBar = createElement("progress", {id: "downloadProgressBar", value: "0", max: "100"});

        progressContainer.appendChild(progressText);
        progressContainer.appendChild(progressBar);
        content.appendChild(progressContainer);

        const buttonContainer = createElement("div", {style: "display: flex; justify-content: flex-end;", className: "popup-row"});
        const closeButton = createElement("button", {id: "closeDownloadWordsBtn", textContent: "Close", className: "popup-button"});
        buttonContainer.appendChild(closeButton);
        content.appendChild(buttonContainer);

        popup.appendChild(dragHandle);
        popup.appendChild(content);

        return popup;
    }

    function createUI() {
        // Create settings button
        const settingsButton = createElement("button", {
            id: "lingqAddonSettings",
            textContent: "⚙️",
            title: "LingQ Addon Settings",
            className: "nav-button"
        });

        // Create lesson complete button
        const completeLessonButton = createElement("button", {
            id: "lingqLessonComplete",
            textContent: "✔",
            title: "Complete Lesson Button",
            className: "nav-button"
        });

        // Create download words button
        const downloadWordsButton = createElement("button", {
            id: "lingqDownloadWords",
            textContent: "💾",
            title: "Download Words",
            className: "nav-button"
        });

        // Find the #main-nav element
        let mainNav = document.querySelector("#main-nav > nav > div:nth-child(2) > div:nth-child(1)");

        if (mainNav) {
            mainNav.appendChild(settingsButton);
            mainNav.appendChild(downloadWordsButton);
            mainNav.appendChild(completeLessonButton);
        } else {
            console.error("#main-nav element not found. Buttons not inserted.");
        }

        // Create settings popup
        const settingsPopup = createSettingsPopup();
        document.body.appendChild(settingsPopup);

        // Create download words popup
        const downloadWordsPopup = createDownloadWordsPopup();
        document.body.appendChild(downloadWordsPopup);

        // Add event listeners
        setupSettingEventListeners(settingsButton, settingsPopup);
        setupDownloadWordsEventListeners(downloadWordsButton, downloadWordsPopup);
        setupEventListeners()
    }

    function makeDraggable(element, handle) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;

        if (handle) {
            handle.onmousedown = dragMouseDown;
        } else {
            element.onmousedown = dragMouseDown;
        }

        function dragMouseDown(e) {
            e = e || window.event;
            e.preventDefault();

            if (element.style.transform && element.style.transform.includes('translate')) {
                const rect = element.getBoundingClientRect();

                element.style.transform = 'none';
                element.style.top = rect.top + 'px';
                element.style.left = rect.left + 'px';
            }

            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();

            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;

            element.style.top = (element.offsetTop - pos2) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }

    function setupSettingEventListeners(settingsButton, settingsPopup) {
        function initializePickrs() {
            function setupRGBAPickr(pickerId, textId, settingKey, cssVar) {
                function saveColorSetting(key, value) {
                    const currentColorMode = document.getElementById("colorModeSelector").value;
                    const prefix = currentColorMode === "dark" ? "dark_" : "white_";
                    storage.set(prefix + key, value);
                }

                const pickerElement = document.getElementById(pickerId);
                const textElement = document.getElementById(textId);

                if (!pickerElement || !textElement) return;

                pickerElement.style.backgroundColor = textElement.value;

                const pickr = Pickr.create({
                    el: pickerElement,
                    theme: 'nano',
                    useAsButton: true,
                    default: textElement.value,
                    components: {preview: true, opacity: true, hue: true}
                });

                pickr.on('change', (color) => {
                    const rgbaColor = color.toRGBA();

                    const r = Math.round(rgbaColor[0]);
                    const g = Math.round(rgbaColor[1]);
                    const b = Math.round(rgbaColor[2]);
                    const a = rgbaColor[3];

                    const roundedRGBA = `rgba(${r}, ${g}, ${b}, ${a})`;

                    textElement.value = roundedRGBA;
                    pickerElement.style.backgroundColor = roundedRGBA;
                    document.documentElement.style.setProperty(cssVar, roundedRGBA);

                    saveColorSetting(settingKey, roundedRGBA);
                });

                textElement.addEventListener('change', function () {
                    const rgbaColor = this.value;

                    pickr.setColor(this.value);
                    saveColorSetting(settingKey, rgbaColor);
                    document.documentElement.style.setProperty(cssVar, rgbaColor);
                    pickerElement.style.backgroundColor = rgbaColor;
                });

                pickr.on('hide', () => {
                    const rgbaColor = pickr.getColor().toRGBA().toString();
                    pickerElement.style.backgroundColor = rgbaColor;
                });
            }

            return new Promise((resolve) => {
                const pickrCss = createElement('link', {
                    rel: 'stylesheet',
                    href: 'https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/nano.min.css'
                });
                document.head.appendChild(pickrCss);

                const pickrScript = createElement('script', {
                    src: 'https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js',
                    onload: () => resolve() // Pass function reference directly
                });
                document.head.appendChild(pickrScript);
            }).then(() => {
                setupRGBAPickr('lingqBackgroundPicker', 'lingqBackgroundText', 'lingqBackground', '--lingq_background');
                setupRGBAPickr('lingqBorderPicker', 'lingqBorderText', 'lingqBorder', '--lingq_border');
                setupRGBAPickr('lingqBorderLearnedPicker', 'lingqBorderLearnedText', 'lingqBorderLearned', '--lingq_border_learned');
                setupRGBAPickr('blueBorderPicker', 'blueBorderText', 'blueBorder', '--blue_border');
                setupRGBAPickr('fontColorPicker', 'fontColorText', 'fontColor', '--font_color');
                setupRGBAPickr('playingUnderlinePicker', 'playingUnderlineText', 'playingUnderline', '--is_playing_underline');
            });
        }

        settingsButton.addEventListener("click", () => {
            settingsPopup.style.display = "block";
            initializePickrs();

            const dragHandle = document.getElementById("lingqAddonSettingsDragHandle");
            makeDraggable(settingsPopup, dragHandle);
        });

        // Style type selector
        const styleTypeSelector = document.getElementById("styleTypeSelector");
        styleTypeSelector.addEventListener("change", (event) => {
            const selectedStyleType = event.target.value;
            storage.set("styleType", selectedStyleType);
            document.getElementById("videoSettings").style.display = selectedStyleType === "video" ? "block" : "none";
            document.getElementById("sentenceVideoSettings").style.display = selectedStyleType === "off" ? "block" : "none";
            applyStyles(selectedStyleType, document.getElementById("colorModeSelector").value);
        });

        function updateColorInputs(colorSettings) {
            document.getElementById("fontColorText").value = colorSettings.fontColor;
            document.getElementById("lingqBackgroundText").value = colorSettings.lingqBackground;
            document.getElementById("lingqBorderText").value = colorSettings.lingqBorder;
            document.getElementById("lingqBorderLearnedText").value = colorSettings.lingqBorderLearned;
            document.getElementById("blueBorderText").value = colorSettings.blueBorder;
            document.getElementById("playingUnderlineText").value = colorSettings.playingUnderline;

            const fontColorPicker = document.getElementById("fontColorPicker");
            if (fontColorPicker) fontColorPicker.style.backgroundColor = colorSettings.fontColor;

            const playingUnderlinePicker = document.getElementById("playingUnderlinePicker");
            if (playingUnderlinePicker) playingUnderlinePicker.style.backgroundColor = colorSettings.playingUnderline;
        }

        function updateColorPickerBackgrounds(colorSettings) {
            const pickerIds = [
                { id: "lingqBackgroundPicker", color: colorSettings.lingqBackground },
                { id: "lingqBorderPicker", color: colorSettings.lingqBorder },
                { id: "lingqBorderLearnedPicker", color: colorSettings.lingqBorderLearned },
                { id: "blueBorderPicker", color: colorSettings.blueBorder },
                { id: "fontColorPicker", color: colorSettings.fontColor },
                { id: "playingUnderlinePicker", color: colorSettings.playingUnderline }
            ];

            pickerIds.forEach(item => {
                const picker = document.getElementById(item.id);
                if (picker) {
                    picker.style.backgroundColor = item.color;
                }
            });
        }

        function updateCssColorVariables(colorSettings) {
            document.documentElement.style.setProperty("--font_color", colorSettings.fontColor);
            document.documentElement.style.setProperty("--lingq_background", colorSettings.lingqBackground);
            document.documentElement.style.setProperty("--lingq_border", colorSettings.lingqBorder);
            document.documentElement.style.setProperty("--lingq_border_learned", colorSettings.lingqBorderLearned);
            document.documentElement.style.setProperty("--blue_border", colorSettings.blueBorder);
            document.documentElement.style.setProperty("--is_playing_underline", colorSettings.playingUnderline);
        }

        function updateColorMode(event) {
            event.stopPropagation();

            const selectedColorMode = this.value;
            const settingsPopup = document.getElementById("lingqAddonSettingsPopup");
            settingsPopup.style.backgroundColor = selectedColorMode === "dark" ? "#2a2c2e" : "#ffffff";

            storage.set("colorMode", selectedColorMode);

            const colorSettings = getColorSettings(selectedColorMode);

            updateColorInputs(colorSettings);

            document.documentElement.style.setProperty(
                "--background-color",
                selectedColorMode === "dark" ? "#2a2c2e" : "#ffffff"
            );
            updateCssColorVariables(colorSettings);

            applyStyles(document.getElementById("styleTypeSelector").value, selectedColorMode);

            updateColorPickerBackgrounds(colorSettings);
        }

        document.getElementById("colorModeSelector").addEventListener("change", updateColorMode);

        function setupSlider(sliderId, valueId, settingKey, unit, cssVar, valueTransform) {
            const slider = document.getElementById(sliderId);
            const valueDisplay = document.getElementById(valueId);

            slider.addEventListener("input", function () {
                const value = parseFloat(this.value);
                const transformedValue = valueTransform(value);

                valueDisplay.textContent = transformedValue.toString().replace(unit, '');
                storage.set(settingKey, value);
                document.documentElement.style.setProperty(cssVar, transformedValue);
            });
        }

        setupSlider("fontSizeSlider", "fontSizeValue", "fontSize", "rem", "--font_size", (val) => `${val}rem`);
        setupSlider("lineHeightSlider", "lineHeightValue", "lineHeight", "", "--line_height", (val) => val);
        setupSlider("heightBigSlider", "heightBigValue", "heightBig", "px", "--height_big", (val) => `${val}px`);
        setupSlider("sentenceHeightSlider", "sentenceHeightValue", "sentenceHeight", "px", "--sentence_height", (val) => `${val}px`);

        const autoFinishingCheckbox = document.getElementById("autoFinishingCheckbox");
        autoFinishingCheckbox.addEventListener('change', (event) => {
            const checked = event.target.checked;
            storage.set("autoFinishing", checked);
            settings.autoFinishing = checked;
        });

        document.getElementById("closeSettingsBtn").addEventListener("click", () => {
            settingsPopup.style.display = "none";
        });

        function resetSettings() {
            if (!confirm("Reset all settings to default?")) return;

            const currentColorMode = document.getElementById("colorModeSelector").value;
            const defaultColorSettings = currentColorMode === "dark" ? defaults.darkColors : defaults.whiteColors;

            document.getElementById("styleTypeSelector").value = defaults.styleType;
            document.getElementById("fontSizeSlider").value = defaults.fontSize;
            document.getElementById("fontSizeValue").textContent = defaults.fontSize;
            document.getElementById("lineHeightSlider").value = defaults.lineHeight;
            document.getElementById("lineHeightValue").textContent = defaults.lineHeight;
            document.getElementById("heightBigSlider").value = defaults.heightBig;
            document.getElementById("heightBigValue").textContent = defaults.heightBig;
            document.getElementById("sentenceHeightSlider").value = defaults.sentenceHeight;
            document.getElementById("sentenceHeightValue").textContent = defaults.sentenceHeight;

            updateColorInputs(defaultColorSettings);
            updateColorPickerBackgrounds(defaultColorSettings);

            for (const [key, value] of Object.entries(defaults)) {
                storage.set(key, value);
            }

            const prefix = currentColorMode === "dark" ? "dark_" : "white_";
            for (const [key, value] of Object.entries(defaultColorSettings)) {
                storage.set(prefix + key, value);
            }

            applyStyles(defaults.styleType, currentColorMode);

            document.getElementById("videoSettings").style.display = defaults.styleType === "video" ? "block" : "none";
            document.getElementById("sentenceVideoSettings").style.display = defaults.styleType === "off" ? "block" : "none";

            document.documentElement.style.setProperty("--font_size", `${defaults.fontSize}rem`);
            document.documentElement.style.setProperty("--line_height", defaults.lineHeight);
            document.documentElement.style.setProperty("--height_big", `${defaults.heightBig}px`);
            document.documentElement.style.setProperty("--sentence_height", `${defaults.sentenceHeight}px`);
            updateCssColorVariables(defaultColorSettings);

            document.getElementById("autoFinishingCheckbox").checked = defaults.autoFinishing;
        }

        document.getElementById("resetSettingsBtn").addEventListener("click", resetSettings);
    }

    async function setupDownloadWordsEventListeners(downloadWordsButton, downloadWordsPopup) {
        async function getAllWords(baseUrl, pageSize, apiType, additionalParams="", progressCallback = () => {}) {
            let allResults = [];
            let nextUrl = `${baseUrl}?page_size=${pageSize}&page=1${additionalParams}`;
            let currentPage = 0;
            let totalPages = 0;
            let isFirstCall = true;

            while (nextUrl) {
                try {
                    const response = await fetch(nextUrl);

                    if (!response.ok) {
                        throw new Error(`HTTP error! Status: ${response.status}`);
                    }

                    const data = await response.json();
                    currentPage++;

                    if (isFirstCall) {
                        isFirstCall = false;
                        totalPages = Math.ceil(data.count / pageSize);
                        console.log(`total pages: ${totalPages}`);
                    }

                    progressCallback(currentPage, totalPages, false, null, data.count);

                    if (apiType === 'lingq') {
                        const filteredResults = data.results.map(item => ({
                            pk: item.pk,
                            term: item.term,
                            fragment: item.fragment,
                            status: item.status,
                            hint: item.hints && item.hints[0] ? item.hints[0].text : null
                        }));
                        allResults = allResults.concat(filteredResults);
                    } else if (apiType === 'known') {
                        allResults = allResults.concat(data.results);
                    }

                    nextUrl = data.next;

                    if (nextUrl) {
                        console.log("Fetched page. Next URL:", nextUrl);
                    } else {
                        console.log("Finished fetching all pages");
                        progressCallback(currentPage, totalPages, true, null, data.count);
                    }
                } catch (error) {
                    console.error('Error fetching data:', error);
                    progressCallback(currentPage, totalPages, true, error, 0);
                    break;
                }
            }

            return allResults;
        }

        async function downloadWords(baseUrl, pageSize, fileName, apiType, additionalParams="") {
            const progressContainer = document.getElementById("downloadProgressContainer");
            const progressBar = document.getElementById("downloadProgressBar");
            const progressText = document.getElementById("downloadProgressText");

            if (progressContainer && progressBar && progressText) {
                progressBar.value = 0;
                progressBar.max = 100;
                progressText.textContent = "Initializing download...";
                progressContainer.style.display = "block";
            }

            const progressCallback = (currentPage, totalPages,_isDone, error_isErrorEncountered, totalCount) => {
                if (progressBar && progressText) {
                    if (error_isErrorEncountered) {
                        progressText.textContent = `Error fetching page ${currentPage}: ${error_isErrorEncountered.message}`;
                        progressBar.style.backgroundColor = 'red';
                        return;
                    }

                    progressBar.max = totalPages;
                    progressBar.value = currentPage;
                    progressText.textContent = `Fetching data... Page ${currentPage} of ${totalPages} (Total items: ${totalCount || 'N/A'})`;

                    if (_isDone) {
                        progressText.textContent = error_isErrorEncountered ? `Export failed: ${error_isErrorEncountered.message}` : `${totalCount} items exported`;
                    }
                }
            };

            try {
                const allWords = await getAllWords(baseUrl, pageSize, apiType, additionalParams, progressCallback);

                if (!allWords || allWords.length === 0) {
                    console.warn("No words found or an error occurred.");
                    return;
                }

                let blob;
                const fileType = fileName.split(".")[1];

                if (fileType === 'json') {
                    const dataString = JSON.stringify(allWords, null, 2);
                    blob = new Blob([dataString], { type: 'application/json' });
                } else if (fileType === 'csv') {
                    const headers = Object.keys(allWords[0]).join(',');
                    const rows = allWords.map(item => {
                        return Object.values(item).map(value => {
                            if (typeof value === 'string') {
                                return `"${value.replace(/"/g, '""')}"`;
                            }
                            return value;
                        }).join(',');
                    }).join('\n');

                    const dataString = headers + '\n' + rows;
                    blob = new Blob([dataString], { type: 'text/csv' });
                }

                downloadBlob(blob, fileName);
                console.log("Export completed.");
            } catch (error) {
                console.error('Error:', error);
            }
        }

        function downloadBlob(blob, fileName) {
            const url = URL.createObjectURL(blob);
            const a = createElement("a", {href: url, download: fileName});
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }

        downloadWordsButton.addEventListener("click", () => {
            downloadWordsPopup.style.display = "block";

            const progressContainer = document.getElementById("downloadProgressContainer");
            if (progressContainer) progressContainer.style.display = "none";

            const dragHandle = document.getElementById("lingqDownloadWordsDragHandle");
            if (dragHandle) {
                makeDraggable(downloadWordsPopup, dragHandle);
            }
        });

        const languageCode = await getLanguageCode();
        const pageSize = 1000;

        const setButtonsDisabled = (disabled) => {
            const buttons = downloadWordsPopup.querySelectorAll('.popup-button');
            buttons.forEach(button => {
                button.disabled = disabled;
            });
        };

        const handleDownloadButtonClick = async (url, filename, type, params = '') => {
            setButtonsDisabled(true);
            try {
                await downloadWords(url, pageSize, filename, type, params);
            } finally {
                setButtonsDisabled(false);
            }
        };

        // Download Unknown LingQs button
        document.getElementById("downloadUnknownLingqsBtn").addEventListener("click", async () => {
            await handleDownloadButtonClick(`https://www.lingq.com/api/v3/${languageCode}/cards/`, "unknown_lingqs.csv", 'lingq', '&status=0&status=1&status=2&status=3');
        });

        // Download Unknown LingQ Words button
        document.getElementById("downloadUnknownLingqWordsBtn").addEventListener("click", async () => {
            await handleDownloadButtonClick(`https://www.lingq.com/api/v3/${languageCode}/cards/`, "unknown_lingq_words.csv", 'lingq', '&status=0&status=1&status=2&status=3&phrases=false');
        });

        // Download Unknown LingQ phrases button
        document.getElementById("downloadUnknownLingqPhrasesBtn").addEventListener("click", async () => {
            await handleDownloadButtonClick(`https://www.lingq.com/api/v3/${languageCode}/cards/`, "unknown_lingq_phrases.csv", 'lingq', '&status=0&status=1&status=2&status=3&phrases=True');
        });

        // Download Known LingQs button
        document.getElementById("downloadKnownLingqsBtn").addEventListener("click", async () => {
            await handleDownloadButtonClick(`https://www.lingq.com/api/v3/${languageCode}/cards/`, "known_lingqs.csv", 'lingq', '&status=4');
        });

        // Download known words button
        document.getElementById("downloadKnownWordsBtn").addEventListener("click", async () => {
            await handleDownloadButtonClick(`https://www.lingq.com/api/v2/${languageCode}/known-words/`, "known_words.csv", "known");
        });

        // Close button
        document.getElementById("closeDownloadWordsBtn").addEventListener("click", () => {
            downloadWordsPopup.style.display = "none";
        });
    }

    function setupEventListeners() {
        document.getElementById("lingqLessonComplete").addEventListener("click", finishLesson);
    }

    function applyStyles(styleType, colorMode) {
        const colorSettings = getColorSettings(colorMode);

        let css = generateBaseCSS(colorSettings, colorMode);
        let uiCSS = generateUICSS();
        let specificCSS = "";

        switch (colorMode) {
            case "dark":
                console.log('dark clicked');
                clickElement(".reader-themes-component > button:nth-child(5)");
                break;
            case "white":
                console.log('white clicked');
                clickElement(".reader-themes-component > button:nth-child(1)");
                break;
        }

        switch (styleType) {
            case "video":
                specificCSS = generateVideoCSS();
                break;
            case "video2":
                specificCSS = generateVideo2CSS();
                break;
            case "audio":
                specificCSS = generateAudioCSS();
                break;
            case "off":
                css = generateOffModeCSS(colorSettings);
                break;
        }

        css += specificCSS;
        css += uiCSS;

        if (styleElement) {
            styleElement.remove();
            styleElement = null;
        }

        styleElement = createElement("style", {textContent: css});
        document.querySelector("head").appendChild(styleElement);
    }

    function generateUICSS() {
        return`
        /*Color picker*/

        .color-picker {
            width: 30px;
            height: 15px;
            border-radius: 4px;
            cursor: pointer;
        }

        .pcr-app {
            z-index: 10001 !important;
        }

        .pcr-app .pcr-interaction .pcr-result {
            color: var(--font_color) !important;
        }

        /*Popup settings*/

        #lingqAddonSettings {
            color: var(--font_color);
        }

        #lingqAddonSettingsPopup, #lingqDownloadWordsPopup {
            position: fixed;
            top: 40%;
            left: 40%;
            transform: translate(-40%, -40%);
            background-color: var(--background-color, #2a2c2e);
            color: var(--font_color, #e0e0e0);
            border: 1px solid grey;
            border-radius: 8px;
            box-shadow: 8px 8px 8px rgba(0, 0, 0, 0.2);
            z-index: 10000;
            display: none;
            width: 400px;
            max-height: 90vh;
            overflow-y: auto;
        }

        #lingqAddonSettingsDragHandle, #lingqDownloadWordsDragHandle {
            cursor: move;
            background-color: rgba(128, 128, 128, 0.2);
            padding: 8px;
            border-radius: 8px 8px 0 0;
            text-align: center;
            user-select: none;
        }

        .popup-row {
            margin: 5px 0;
        }

        .nav-button {
            background: none;
            border: none;
            cursor: pointer;
            font-size: 1.2rem;
            margin-left: 10px;
            padding: 5px;
        }

        .popup-button {
            padding: 5px 10px;
            border: 1px solid;
            border-radius: 5px;
            margin: 5px 0;
        }

        #downloadProgressContainer {
            display: none;
        }

        #downloadProgressText {
            text-align: center;
            margin-bottom: 5px;
            font-size: 0.9em;
        }

        #downloadProgressBar {
            width: 100%;
            height: 20px;
        }

        progress[value]::-webkit-progress-bar {
            border-radius: 5px;
        }

        progress[value]::-webkit-progress-value {
            border-radius: 5px;
        }
        `;
    }

    function generateBaseCSS(colorSettings, colorMode) {
        return `
        :root {
            --font_size: ${settings.fontSize}rem;
            --line_height: ${settings.lineHeight};

            --article_height: calc(var(--app-height) - var(--height_big) - 10px);
            --grid-layout: var(--article_height) calc(var(--height_big) - 80px) 90px;

            --font_color: ${colorSettings.fontColor};
            --lingq_background: ${colorSettings.lingqBackground};
            --lingq_border: ${colorSettings.lingqBorder};
            --lingq_border_learned: ${colorSettings.lingqBorderLearned};
            --blue_border: ${colorSettings.blueBorder};
            --is_playing_underline: ${colorSettings.playingUnderline};

            --background-color: ${colorMode === "dark" ? "#2a2c2e" : "#ffffff"}
        }

        /*header settings*/

        .main-wrapper {
            padding: 0 !important;
        }

        #main-nav {
            z-index: 1;
        }

        #main-nav > nav {
            height: 50px;
        }

        #main-nav > nav > div:nth-child(1) {
            height: 50px;
        }

        .main-header {
            pointer-events: none;
        }

        .main-header > div {
            grid-template-columns: 1fr 150px !important;
            padding-left: 400px !important;
        }

        .main-header section:nth-child(1) {
            display: none;
        }

        .main-header section {
            pointer-events: auto;
            z-index: 1;
        }

        .main-header svg {
            width: 20px !important;
            height: 20px !important;
        }

        .lesson-progress-section {
            grid-template-rows: unset !important;
            grid-template-columns: unset !important;
            grid-column: 1 !important;
            pointer-events: auto;
        }

        .lesson-progress-section .rc-slider{
            grid-row: unset !important;
            grid-column: unset !important;
            width: 50% !important;
        }

        /*layout*/

        #lesson-reader {
            grid-template-rows: var(--grid-layout);
            overflow-y: hidden;
            height: auto !important;
        }

        .sentence-text {
            height: calc(var(--article_height) - 70px) !important;
        }

        .reader-container-wrapper {
            height: 100% !important;
        }

        .widget-area {
            padding-top: 50px !important;
            height: 100% !important;
        }

        .main-footer {
            grid-area: 3 / 1 / 3 / 1 !important;
            align-self: end;
            margin: 10px 0;
        }

        .main-content {
            grid-template-rows: 45px 1fr !important;
            overflow: hidden;
            align-items: anchor-center;
        }

        /*make prev/next page buttons compact*/

        .reader-component {
            grid-template-columns: 0rem 1fr 0rem !important;
            align-items: baseline;
            margin-top: 10px;
        }

        .reader-component > div > a.button > span {
            width: 0.5rem !important;
        }

        .reader-component > div > a.button > span > svg {
            width: 15px !important;
            height: 15px !important;
        }

        .loadedContent {
            padding: 0 0 5px 15px !important;;
        }

        /*video viewer*/

        .video-player {
            display: flex !important;
            justify-content: flex-end !important;
            align-items: flex-start !important;
            pointer-events: none;
        }

        .video-player > .modal-background {
            background-color: rgb(26 28 30 / 0%) !important;
        }

        .video-player > .modal-content {
            max-width: var(--width_big) !important;
            margin: var(--video_margin) !important;
            border-radius: 0.75rem !important;
        }

        .video-player .modal-section {
            display: none !important;
        }

        .video-wrapper {
            height: var(--height_big) !important;
            overflow: hidden;
            pointer-events: auto;
        }

        /*video controller*/

        .rc-slider-rail {
            background-color: dimgrey !important;
        }

        .lingq-audio-player {
            margin-left: 10px;
        }

        .section--player.is-expanded {
            padding: 5px 0px !important;
            width: 390px !important;
            margin-left: 10px !important;
        }

        .sentence-mode-button {
            margin: 0 0 10px 0;
        }

        .player-wrapper {
            grid-template-columns: 1fr 40px !important;
            padding: 0 !important;
        }

        .audio-player {
            padding: 0 0.5rem !important;
        }

        /*font settings*/

        .reader-container {
            margin: 0 !important;
            float: left !important;
            line-height: var(--line_height) !important;
            font-size: var(--font_size) !important;
            columns: unset !important;
            overflow-y: scroll !important;
            max-width: unset !important;
        }

        .sentence-text-head {
            min-height: 4.5rem !important;
        }

        .reader-container p {
            margin-top: 0 !important;
        }

        .reader-container p span.sentence-item,
        .reader-container p .sentence {
            color: var(--font_color) !important;
        }

        .sentence.is-playing,
        .sentence.is-playing span {
            text-underline-offset: .2em !important;
            text-decoration-color: var(--is_playing_underline) !important;
        }

        /*highlightings*/

        .phrase-item {
            padding: 0 !important;
        }

        .phrase-item:not(.phrase-item-status--4, .phrase-item-status--4x2)) {
            background-color: var(--lingq_background) !important;
        }

        .phrase-item.phrase-item-status--4,
        .phrase-item.phrase-item-status--4x2 {
            background-color: rgba(0, 0, 0, 0) !important;
        }

        .phrase-cluster:not(:has(.phrase-item-status--4, .phrase-item-status--4x2)) {
            border: 1px solid var(--lingq_border) !important;
            border-radius: .25rem;
        }

        .phrase-cluster:has(.phrase-item-status--4, .phrase-item-status--4x2) {
            border: 1px solid var(--lingq_border_learned) !important;
            border-radius: .25rem;
        }

        .reader-container .sentence .lingq-word:not(.is-learned) {
            border: 1px solid var(--lingq_border) !important;
            background-color: var(--lingq_background) !important;
        }

        .reader-container .sentence .lingq-word.is-learned {
            border: 1px solid var(--lingq_border_learned) !important;
        }

        .reader-container .sentence .blue-word {
            border: 1px solid var(--blue_border) !important;
        }

        .phrase-cluster:hover,
        .phrase-created:hover {
            padding: 0 !important;
        }

        .phrase-cluster:hover .phrase-item,
        .phrase-created .phrase-item {
            padding: 0 !important;
        }

        .reader-container .sentence .selected-text {
            padding: 0 !important;
        }
        `;
    }

    function generateVideoCSS() {
        return `
        :root {
            --width_big: calc(100vw - 424px - 10px);
            --height_big: ${settings.heightBig}px;
            --video_margin: 0 0 10px 10px !important;
        }

        .main-content {
            grid-area: 1 / 1 / 2 / 2 !important;
        }

        .widget-area {
            grid-area: 1 / 2 / 3 / 2 !important;
        }

        .main-footer {
            grid-area: 3 / 2 / 4 / 3 !important;
            align-self: end;
        }
        `;
    }

    function generateVideo2CSS() {
        return `
        :root {
            --width_big: calc(50vw - 217px);
            --height_big: calc(100vh - 80px);

            --grid-layout: var(--article_height) 90px;
            --video_margin: 0 10px 10px 10px !important;
            --article_height: calc(var(--app-height) - 85px);
        }

        .page.reader-page.has-widget-fixed:not(.is-edit-mode):not(.workspace-sentence-reviewer) {
            grid-template-columns: 1fr 424px 1fr;
        }

        .main-content {
            grid-area: 1 / 1 / 2 / 2 !important;
        }

        .widget-area {
            grid-area: 1 / 2 / 2 / 3 !important;
        }

        .main-footer {
            grid-area: 2 / 2 / 3 / 3 !important;
            align-self: end;
        }

        .modal-container .modls {
            align-items: end;
        }
        `;
    }

    function generateAudioCSS() {
        return `
        :root {
            --height_big: 60px;
        }

        .main-content {
            grid-area: 1 / 1 / 2 / 2 !important;
        }

        .widget-area {
            grid-area: 1 / 2 / 2 / 2 !important;
        }
        `;
    }

    function generateOffModeCSS(colorSettings) {
        return `
        :root {
            --width_small: 440px;
            --height_small: 260px;
            --sentence_height: ${settings.sentenceHeight}px;
            --right_pos: 0.5%;
            --bottom_pos: 5.5%;
        }

        /*video player*/

        .video-player.is-minimized .video-wrapper,
        .sent-video-player.is-minimized .video-wrapper {
            height: var(--height_small);
            width: var(--width_small);
            overflow: auto;
            resize: both;
        }

        .video-player.is-minimized .modal-content,
        .sent-video-player.is-minimized .modal-content {
            max-width: calc(var(--width_small)* 3);
            margin-bottom: 0;
        }

        .video-player.is-minimized,
        .sent-video-player.is-minimized {
            left: auto;
            top: auto;
            right: var(--right_pos);
            bottom: var(--bottom_pos);
            z-index: 99999999;
            overflow: visible
        }

        /*sentence mode video player*/
        .loadedContent:has(#sentence-video-player-portal) {
            grid-template-rows: var(--sentence_height) auto auto 1fr !important;
        }

        #sentence-video-player-portal .video-section {
            width: 100% !important;
            max-width: none !important;
        }

        #sentence-video-player-portal .video-wrapper {
            height: 100% !important;
            max-height: none !important;
        }

        #sentence-video-player-portal div:has(> iframe) {
            height: 100% !important;
        }
        `;
    }

    function clickElement(selector) {
        const element = document.querySelector(selector);
        if (element) element.click();
    }

    function focusElement(selector) {
        const element = document.querySelector(selector);
        if (element) {
            element.focus();
            element.setSelectionRange(element.value.length, element.value.length);
        }
    }

    function copySelectedText() {
        const selected_text = document.querySelector(".reference-word");
        if (selected_text) {
            navigator.clipboard.writeText(selected_text.textContent);
        }
    }

    function finishLesson(){
        clickElement(".reader-component > .nav--right > a");
    }

    function setupKeyboardShortcuts() {
        function preventPropagation(event){
            event.preventDefault();
            event.stopPropagation();
        }

        document.addEventListener("keydown", function (event) {
            const targetElement = event.target;
            const isTextInput = targetElement.type === "text" || targetElement.type === "textarea";
            const withoutModifierKeys = !event.ctrlKey && !event.shiftKey && !event.altKey;
            const eventKey = event.key.toLowerCase();
            if (isTextInput) {
                if ((eventKey == 'enter' || eventKey == 'esc') && withoutModifierKeys) {
                    preventPropagation(event);
                    event.target.blur();
                } else {
                    return;
                }
            }

            const shortcuts = {
                'q': () => clickElement(".modal-section > div > button:nth-child(2)"), // video full screen toggle
                'w': () => clickElement(".audio-player--controllers > div:nth-child(1) > a"), // 5 sec Backward
                'e': () => clickElement(".audio-player--controllers > div:nth-child(2) > a"), // 5 sec Forward
                'r': () => document.dispatchEvent(new KeyboardEvent("keydown", { key: "k" })), // Make word Known
                't': () => clickElement(".dictionary-resources > a:nth-last-child(1)"), // Open Translator
                '`': () => focusElement(".reference-input-text"), // Move cursor to reference input
                'd': () => clickElement(".dictionary-resources > a:nth-child(1)"), // Open Dictionary
                'f': () => clickElement(".dictionary-resources > a:nth-child(1)"), // Open Dictionary
                'c': () => copySelectedText() // Copy selected text
            };

            if (shortcuts[eventKey] && withoutModifierKeys) {
                preventPropagation(event);
                shortcuts[eventKey]();
            }
        }, true);
    }

    async function getLanguageCode() {
        const url = `https://www.lingq.com/api/v3/profiles/`;

        const response = await fetch(url);
        const data = await response.json();

        return data.results[0].active_language;
    }

    function getLessonId() {
        const url = document.URL;
        const regex = /https*:\/\/www\.lingq\.com\/\w+\/learn\/\w+\/web\/reader\/(\d+)/;
        const match = url.match(regex);

        return match[1];
    }

    async function getCollectionId() {
        const url = document.URL;
        const regex = /https*:\/\/www\.lingq\.com\/\w+\/learn\/\w+\/web\/library\/course\/(\d+)/;
        const match = url.match(regex);

        return match[1];
    }

    async function getLessonInfo(lessonId) {
        const languageCode = await getLanguageCode();
        const url = `https://www.lingq.com/api/v3/${languageCode}/lessons/counters/?lesson=${lessonId}`;

        const response = await fetch(url);
        const data = await response.json();

        return data[lessonId];
    }

    async function getAllLessons(languageCode, collectionId) {
        let allLessons = [];
        let nextUrl = `https://www.lingq.com/api/v3/${languageCode}/search/?page=1&page_size=1000&collection=${collectionId}`;

        while (nextUrl) {
            try {
                const response = await fetch(nextUrl);
                if (!response.ok) {
                    throw new Error(`HTTP error! Status: ${response.status}`);
                }

                const data = await response.json();
                allLessons = allLessons.concat(data.results);
                nextUrl = data.next;
            } catch (error) {
                console.error('Error fetching lessons:', error);
                break;
            }
        }

        return allLessons;
    }

    async function setLessonProgress(lessonId, wordIndex) {
        const languageCode = await getLanguageCode();
        const url = `https://www.lingq.com/api/v3/${languageCode}/lessons/${lessonId}/bookmark/`;
        const payload = { wordIndex: wordIndex, completedWordIndex: wordIndex, client: 'web' };

        fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });
    }

    function setupYoutubePlayerCustomization() {
        function replaceNoCookie() {
            document.querySelectorAll("iframe").forEach(function (iframe) {
                let src = iframe.getAttribute("src");
                if (src && src.includes("disablekb=1")) {
                    src = src.replace("disablekb=1", "disablekb=0"); // keyboard controls are enabled
                    src = src + "&cc_load_policy=1"; // caption is shown by default
                    src = src + "&controls=0"; // player controls do not display in the player
                    iframe.setAttribute("src", src);
                }
            });
        }

        async function setupSliderObserver() {
            const lessonId = getLessonId();
            const lessonInfo = await getLessonInfo(lessonId);
            let lastCompletedPercentage = lessonInfo["progress"];
            console.log(`last progress: ${lastCompletedPercentage}`);

            const sliderTrack = document.querySelector('.audio-player--progress .rc-slider-track');

            const sliderContainer = createSliderElements();
            const videoContainer = document.querySelector(".modal-content > div");
            videoContainer.appendChild(sliderContainer);
            const videoSliderTrack = sliderContainer.querySelector(".rc-slider-track");

            const syncVideoSliderTrack = (videoSliderTrack, sliderTrack) => {
                videoSliderTrack.style.cssText = sliderTrack.style.cssText;
            };

            const updateLessonProgress = (lessonId, lessonInfo, progressPercentage, lastCompletedPercentage) => {
                const progressUpdatePeriod = 5;
                const flooredProgressPercentage = Math.floor(progressPercentage / progressUpdatePeriod) * progressUpdatePeriod;

                if (flooredProgressPercentage > lastCompletedPercentage) {
                    console.log(`progress percentage: ${flooredProgressPercentage}. Updated`);
                    const wordIndex = Math.floor(lessonInfo["totalWordsCount"] * (flooredProgressPercentage / 100));
                    setLessonProgress(lessonId, wordIndex);
                    return flooredProgressPercentage;
                }
                return lastCompletedPercentage;
            };

            const sliderObserver = new MutationObserver(function (mutationsList) {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
                        syncVideoSliderTrack(videoSliderTrack, sliderTrack);

                        const progressPercentage = parseFloat(sliderTrack.style.width);

                        lastCompletedPercentage = updateLessonProgress(lessonId, lessonInfo, progressPercentage, lastCompletedPercentage);
                        const isLessonFinished = progressPercentage >= 99.5;
                        if (isLessonFinished && settings.autoFinishing) {
                            finishLesson();
                        }
                    }
                }
            });

            sliderObserver.observe(sliderTrack, {attributes: true, attributeFilter: ['style']});
            console.log('Observer started for rc-slider-track');
        }

        function createSliderElements() {
            const sliderContainer = createElement("div", {className: "rc-slider rc-slider-horizontal"});
            const sliderRail = createElement("div", {className: "rc-slider-rail"});
            const sliderTrack = createElement("div", {className: "rc-slider-track"});
            sliderContainer.appendChild(sliderRail);
            sliderContainer.appendChild(sliderTrack);
            return sliderContainer;
        }

        const iframeObserver = new MutationObserver(function (mutationsList) {
            for (const mutation of mutationsList) {
                if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeName === "IFRAME") {
                            replaceNoCookie();
                            clickElement('.modal-section.modal-section--head button[title="Expand"]');
                            setupSliderObserver();
                        }
                    });
                }
            }
        });

        iframeObserver.observe(document.body, {childList: true, subtree: true, attributes: true, attributeFilter: ["src"]});
    }

    function setupScrollCustomization() {
        setTimeout(() => {
            const readerContainer = document.querySelector(".reader-container");
            if (readerContainer) {
                readerContainer.addEventListener("wheel", (event) => {
                    event.preventDefault();
                    const delta = event.deltaY;
                    const scrollAmount = 0.3;
                    readerContainer.scrollTop += delta * scrollAmount;
                });
            }
        }, 3000);
    }

    function setupSentenceFocus() {
        function focusPlayingSentence() {
            const playingSentence = document.querySelector(".sentence.is-playing");
            if (playingSentence) {
                /*
                playingSentence.scrollIntoView({
                    behavior: "smooth",
                    block: "center"
                });
                */
                const scrolling_div = document.querySelector(".reader-container")
                scrolling_div.scrollTop = playingSentence.offsetTop + Math.floor(playingSentence.offsetHeight / 2) - Math.floor(scrolling_div.offsetHeight / 2);

            }
        }

        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (
                    mutation.type === "attributes" &&
                    mutation.attributeName === "class" &&
                    mutation.target.classList.contains("sentence")
                ) {
                    focusPlayingSentence();
                }
            });
        });

        const container = document.querySelector(".sentence-text");
        if (container) {
            observer.observe(container, {
                attributes: true,
                subtree: true
            });
        }
    }

    async function waitForElement(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }

            const observer = new MutationObserver(() => {
                if (document.querySelector(selector)) {
                    resolve(document.querySelector(selector));
                    observer.disconnect();
                }
            });

            observer.observe(document.documentElement, {
                childList: true,
                subtree: true
            });
        });
    }

    async function setupCourse() {
        function createCourseUI(){
            const resetButton = createElement("button", {
                id: "resetLessonPositions",
                textContent: "⏮️",
                title: "Reset all lessons to the first page",
                className: "nav-button"
            });

            let nav = document.querySelector(".library-section > .list-header > .list-header-index");
            nav.appendChild(resetButton);
        }

        function setupCourseStyles() {
            const css = `
            .nav-button {
                background: none;
                border: none;
                cursor: pointer;
                font-size: 1.5rem;
            }

            .library-section > .list-header > .list-header-index {
                grid-template-columns: auto 1fr auto !important;
            }

            .dynamic--word-progress {
                grid-template-columns: repeat(3, auto) !important;
            }

            .word-indicator--box-green {
                background-color: var(--green-450);
                border-color: var(--green-default);
            }
            `;
            const styleElement = createElement("style", { textContent: css });
            document.querySelector("head").appendChild(styleElement);
        }

        function enrichLessonDetails() {
            function addKnownWordsIndicator(lessonElement, lessonInfo) {
                const dynamicWordProgress = lessonElement.querySelector('.dynamic--word-progress');

                const knownWordPercentage = Math.round((lessonInfo.knownWordsCount / lessonInfo.uniqueWordsCount) * 100);

                const knownWordsItem = createElement('div', {className: 'word-indicator--item grid-layout grid-align--center grid-item is-fluid--left', title: 'Known Words'});

                const knownWordsBox = createElement('div', {className: 'word-indicator--box word-indicator--box-green'});
                knownWordsItem.appendChild(knownWordsBox);

                const textWrapper = createElement('span', {className: 'text-wrapper is-size-8'});
                textWrapper.appendChild(createElement('span', {textContent: `${lessonInfo.knownWordsCount} (${knownWordPercentage}%)`}));

                knownWordsItem.appendChild(textWrapper);
                dynamicWordProgress.appendChild(knownWordsItem);
            }

            async function updateWordIndicatorPercentages(lessonElement, lessonId) {
                console.log(`lessonId: ${lessonId}`);
                const lessonInfo = await getLessonInfo(lessonId);

                const wordIndicatorItems = lessonElement.querySelector(".word-indicator--item");
                if (!wordIndicatorItems) { return; }

                const lingqsPercentage = Math.round((lessonInfo.cardsCount / lessonInfo.uniqueWordsCount) * 100);
                const lingqsElement = lessonElement.querySelector('.word-indicator--item[title="LingQs"] > span > span');
                lingqsElement.textContent = `${lessonInfo.cardsCount} (${lingqsPercentage}%)`;

                addKnownWordsIndicator(lessonElement, lessonInfo);
            }

            const observer = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        mutation.addedNodes.forEach((node) => {
                            if (node.classList && node.classList.contains('library-item-wrap')) {
                                const lessonId = node.id.split("--")[1].split("-")[0];
                                updateWordIndicatorPercentages(node, lessonId);
                            }
                        });
                    }
                });
            });

            const targetNode = document.querySelector('.library-section .library-list');
            console.log(targetNode);
            const config = { childList: true, subtree: true };
            observer.observe(targetNode, config);
        }

        function enableCourseSorting() {
            const dropdownItems = document.querySelectorAll('.library-section > .list-header .tw-dropdown--item');
            if (dropdownItems.length) {
                // Setup library sort event listener
                dropdownItems.forEach((item, index) => {
                    item.addEventListener('click', () => {
                        console.log(`Clicked sort option: ${index}`);
                        storage.set('librarySortOption', index);
                        settings.librarySortOption = index;
                    });
                });

                // Change sort by the setting
                dropdownItems[settings.librarySortOption].click();
                return true;
            } else {
                console.warn("Dropdown items not found for library sort.");
                return false;
            }
        }

        function setupLessonResetButton() {
            const resetButton = document.getElementById("resetLessonPositions");
            resetButton.addEventListener("click", async () => {
                const languageCode = await getLanguageCode();
                const collectionId = await getCollectionId();

                const allLessons = await getAllLessons(languageCode, collectionId);
                const confirmed = confirm(`Reset all ${allLessons.length} lessons to their starting positions?`);
                if (!confirmed) { return; }

                for (const lesson of allLessons) {
                    await setLessonProgress(lesson.id, 0);
                    console.log(`Reset lesson ID: ${lesson.id} to the first page`);
                }

                alert(`Successfully reset ${allLessons.length} lessons to their starting positions.`);
            });
        }

        const libraryHeader = await waitForElement('.library-section > .list-header');
        createCourseUI();
        setupCourseStyles();

        enrichLessonDetails();
        enableCourseSorting();
        setupLessonResetButton();
    }

    function fixBugs() {
        const resizeToast = () => {
            const css = `
            .toasts {
                height: fit-content;
            }
            `;
            const cssElement = createElement("style", {textContent: css});
            document.querySelector("head").appendChild(cssElement);
        }

        resizeToast();
    }

    function init() {
        fixBugs();

        if (document.URL.includes("reader")) {
            createUI();
            applyStyles(settings.styleType, settings.colorMode);
            setupKeyboardShortcuts();
            setupYoutubePlayerCustomization();
            setupScrollCustomization();
            setupSentenceFocus();
        }
        if (document.URL.includes("library")) {
            setupCourse();
        }
    }

    init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址