LingQ Addon

Provides custom LingQ layouts

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         LingQ Addon
// @description  Provides custom LingQ layouts
// @match        https://www.lingq.com/*/learn/*/web/reader/*
// @version      3.6.1
// @grant       GM_setValue
// @grant       GM_getValue
// @namespace https://greasyfork.org/users/1458847
// ==/UserScript==

(function () {
    "use strict";

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

    // Default values
    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"
        }
    };

    // Load stored settings
    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)
    };

    // Helper function to get color settings based on mode
    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)
        };
    }

    // Get current color settings
    const colorSettings = getColorSettings(settings.colorMode);

    // UI Creation
    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"
        });

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

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

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

        // Add event listeners
        setupEventListeners(settingsButton, completeLessonButton, settingsPopup);
    }

    // Create settings popup with all controls
    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;
    }

    // Generate HTML content for the popup
    function generatePopupContent() {
        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);

        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 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 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;
    }

    // Initialize Pickr color pickers
    function initializePickrs() {
        // Load Pickr library dynamically
        return new Promise((resolve) => {
            // Add Pickr CSS
            const pickrCss = createElement('link', {
                rel: 'stylesheet',
                href: 'https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/nano.min.css'
            });
            document.head.appendChild(pickrCss);

            // Add Pickr JS
            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(() => {
            // Setup each color picker
            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');
        });
    }

    // Setup RGBA color picker with Pickr
    function setupRGBAPickr(pickerId, textId, settingKey, cssVar) {
        const pickerElement = document.getElementById(pickerId);
        const textElement = document.getElementById(textId);

        if (!pickerElement || !textElement) return;

        // Set initial color for the picker element
        pickerElement.style.backgroundColor = textElement.value;

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

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

            // Round the RGBA values
            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; // Update picker background
            document.documentElement.style.setProperty(cssVar, roundedRGBA);

            saveColorSetting(settingKey, roundedRGBA);
        });

        // Update picker when text input changes
        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;
        });

        // Update picker background color when color changes
        pickr.on('hide', () => {
            const rgbaColor = pickr.getColor().toRGBA().toString();
            pickerElement.style.backgroundColor = rgbaColor;
        });
    }

    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;
        }
    }

    // Set up all event listeners for the settings UI
    function setupEventListeners(settingsButton, completeLessonButton, settingsPopup) {
        // Initialize Pickr after popup is displayed
        settingsButton.addEventListener("click", () => {
            setTimeout(() => {
                initializePickrs();
            }, 100);
        });

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

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

        // Toggle popup visibility
        settingsButton.addEventListener("click", () => {
            settingsPopup.style.display = "block";
        });

        // Complete a lesson
        completeLessonButton.addEventListener("click", () => {
            document.querySelector(".reader-component > .nav--right > a").click();
        });

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

        // Reset button
        document.getElementById("resetSettingsBtn").addEventListener("click", resetSettings);

        // Style type selector
        const styleTypeSelector = document.getElementById("styleTypeSelector");
        styleTypeSelector.addEventListener("change", function () {
            const selectedStyleType = this.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);
        });

        // Color mode selector
        document.getElementById("colorModeSelector").addEventListener("change", updateColorMode);

        // Setup sliders
        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`);
    }

    // Helper function to set up slider controls
    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);
        });
    }

    // Function to save color setting with appropriate prefix
    function saveColorSetting(key, value) {
        const currentColorMode = document.getElementById("colorModeSelector").value;
        const prefix = currentColorMode === "dark" ? "dark_" : "white_";
        storage.set(prefix + key, value);
    }

    // Update color mode and related settings
    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);

        // Load color settings for the selected mode
        const colorSettings = getColorSettings(selectedColorMode);

        // Update all color inputs
        updateColorInputs(colorSettings);

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

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

        // Update color picker backgrounds
        updateColorPickerBackgrounds(colorSettings);
    }

    // Update all color input fields with new settings
    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;

        //Update Pickr color pickers with new values
        const fontColorPicker = document.getElementById("fontColorPicker");
        if (fontColorPicker) fontColorPicker.style.backgroundColor = colorSettings.fontColor;

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

    // Update color picker backgrounds
    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;
            }
        });
    }

    // Update CSS color variables
    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);
    }

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

        const currentColorMode = document.getElementById("colorModeSelector").value;

        // Default values
        const defaultSettings = {
            styleType: "video",
            colorMode: currentColorMode,
            fontSize: 1.1,
            lineHeight: 1.7,
            heightBig: 400,
            sentenceHeight: 400
        };

        // Default color settings for current mode
        const defaultColorSettings = currentColorMode === "dark"
            ? defaults.darkColors
            : defaults.whiteColors;

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

        // Update color inputs
        updateColorInputs(defaultColorSettings);

        // Update color picker backgrounds
        updateColorPickerBackgrounds(defaultColorSettings);

        // Save general settings
        for (const [key, value] of Object.entries(defaultSettings)) {
            storage.set(key, value);
        }

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

        // Apply styles
        applyStyles(defaultSettings.styleType, currentColorMode);

        // Show/hide video settings
        document.getElementById("videoSettings").style.display = defaultSettings.styleType === "video" ? "block" : "none";
        document.getElementById("sentenceVideoSettings").style.display = defaultSettings.styleType === "off" ? "block" : "none";

        // Update CSS variables directly
        document.documentElement.style.setProperty("--font_size", `${defaultSettings.fontSize}rem`);
        document.documentElement.style.setProperty("--line_height", defaultSettings.lineHeight);
        document.documentElement.style.setProperty("--height_big", `${defaultSettings.heightBig}px`);
        document.documentElement.style.setProperty("--sentence_height", `${defaultSettings.sentenceHeight}px`);
        updateCssColorVariables(defaultColorSettings);
    }

    // CSS Management
    let styleElement = null;

    // Apply styles based on current settings
    function applyStyles(styleType, colorMode) {
        // Load color settings for the current mode
        const colorSettings = getColorSettings(colorMode);

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

        // Apply color mode CSS
        switch (colorMode) {
            case "dark":
                theme_btn = document.querySelector(".reader-themes-component > button:nth-child(5)");
                break;
            case "white":
                theme_btn = document.querySelector(".reader-themes-component > button:nth-child(1)");
                break;
        }

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

        // Append the style-specific CSS
        css += specificCSS;
        css += uiCSS;

        // Remove the old style tag
        if (styleElement) {
            styleElement.remove();
            styleElement = null;
        }

        // Create & append the new style tag
        if (css) {
            styleElement = createElement("style", {textContent: css});
            document.querySelector("head").appendChild(styleElement);
        }

        if (theme_btn) {
            theme_btn.click();
        }
    }

    // Generate UI-related CSS
    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*/

        #lingqAddonSettingsPopup {
            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{
            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;
        }
        `;
    }

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

            --article_height: calc(var(--app-height) - var(--height_big) - 45px);
            --grid-layout: calc(var(--article_height) - 10px) 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"}
        }

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

        #lingqAddonSettingsPopup {
            background-color: var(--background-color);
            color: var(--font_color);
        }

        .main-wrapper {
            padding-top: calc(var(--spacing) * 12) !important;
        }

        #main-nav .navbar,
        #main-nav .navbar-brand {
            min-height: 2.75rem !important;
        }

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

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

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

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

        /*video viewer*/

        .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;
        }

        .main-content > .main-header {
            margin: 5px 0;
        }

        .main-content > .main-header > section:nth-child(1) {
            margin-top: 30px;
        }

        .modal-container .modls {
            pointer-events: none;
            justify-content: end !important;
            align-items: flex-start;
        }

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

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

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

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

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

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

        /*make prev/next page buttons compact*/

        .reader-component {
            grid-template-columns: 0rem 1fr 0rem !important;
        }

        .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;;
        }

        /*font settings*/

        .reader-container {
            margin: 0 !important;
            float: left !important;
            line-height: var(--line_height) !important;
            padding: 0 0 100px 0 !important;
            font-size: var(--font_size) !important;
            columns: unset !important;
            overflow-y: scroll !important;
            max-width: unset !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;
        }

        /*LingQ 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;
        }
        `;
    }

    // Generate Video mode CSS
    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;
            height: 100% !important;
        }

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

        .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;
        }
        `;
    }

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

            --grid-layout: 1fr 80px;
            --video_margin: 0 10px 20px 10px !important;
            --article_height: calc(var(--app-height) - 265px);
        }

        .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 / -1 / 1 !important;
        }

        .widget-area {
            grid-area: 1 / 2 / -1 / 2 !important;
        }
        .main-footer {
            grid-area: 2 / 1 / 2 / 1 !important;
            margin: 10px 0;
        }

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

    // Generate Audio mode CSS
    function generateAudioCSS() {
        return `
        :root {
            --height_big: 80px;
        }

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

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

    // Generate Off mode CSS
    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.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*/
        .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;
        }
        `;
    }

    // Keyboard shortcuts and other functionality
    function setupKeyboardShortcuts() {
        document.addEventListener("keydown", function (event) {
            const targetElement = event.target;
            const isTextInput = targetElement.type === "text" || targetElement.type === "textarea";
            if (isTextInput) return;

            const shortcuts = {
                'q': () => clickElement(".modal-section > div > button:nth-child(2)"), // video full screen toggle
                '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
                '`': () => 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
                't': () => clickElement(".dictionary-resources > a:nth-last-child(1)"), // Open Translator
                'c': () => copySelectedText() // Copy selected text
            };

            if (shortcuts[event.key]) {
                event.preventDefault();
                event.stopPropagation();
                shortcuts[event.key]();
            }
        }, true);
    }

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

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

    // Helper function to copy selected text
    function copySelectedText() {
        const selected_text = document.querySelector(".reference-word");
        if (selected_text) {
            navigator.clipboard.writeText(selected_text.textContent);
        }
    }

    // Custom embedded player
    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);
                }
            });
        }

        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"]');
                        }
                    });
                }
            }
        });

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

    // Scroll customization
    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);
    }

    // Focus on playing sentence
    function setupSentenceFocus() {
        function focusPlayingSentence() {
            const playingSentence = document.querySelector(".sentence.is-playing");
            if (playingSentence) {
                playingSentence.scrollIntoView({
                    behavior: "smooth",
                    block: "center"
                });
            }
        }

        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
            });
        }
    }

    // Initialize everything
    function init() {
        createUI();
        applyStyles(settings.styleType, settings.colorMode);
        setupKeyboardShortcuts();
        setupYoutubePlayerCustomization();
        setupScrollCustomization();
        setupSentenceFocus();
    }

    // Start the script
    init();
})();