FiiO Web EQ Import/Export Tool (SquigLink)

A tool to import/export EQ settings between SquigLink (txt) or Hangout Audio URLs and FiiO Web EQ.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         FiiO Web EQ Import/Export Tool (SquigLink)
// @namespace    http://tampermonkey.net/
// @version      2.95
// @description  A tool to import/export EQ settings between SquigLink (txt) or Hangout Audio URLs and FiiO Web EQ.
// @author       NateAFish
// @match        https://fiiocontrol.fiio.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const CACHE_KEY = 'tm_squig_sites_data';
    const CACHE_DURATION = 24 * 60 * 60 * 1000;

    const style = document.createElement('style');
    style.innerHTML = `
        .tm-loading-fade { transition: opacity 0.3s ease !important; }
        .tm-fade-in { opacity: 1 !important; }
        .tm-fade-out { opacity: 0 !important; }

        .tm-squig-container {
            position: relative;
            display: flex;
            align-items: center;
            margin-right: 15px;
            cursor: pointer;
            height: 100%;
            user-select: none;
        }
        .tm-squig-trigger {
            font-size: 14px;
            color: #ffffff !important;
            padding: 0 5px;
        }
        .tm-squig-trigger:hover {
            opacity: 0.8;
        }

        .tm-squig-dropdown {
            position: absolute;
            top: 100%;
            left: 50%;
            transform: translateX(-50%) translateY(-10px);
            width: 260px;
            background-color: var(--el-bg-color-overlay, #ffffff);
            border: 1px solid var(--el-border-color-light, #e4e7ed);
            border-radius: 4px;
            box-shadow: var(--el-box-shadow-light, 0 2px 12px 0 rgba(0, 0, 0, 0.1));
            overflow: visible;
            padding: 0;
            z-index: 2050;
            text-align: left;
            margin-top: 16px;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
        }

        .tm-squig-scroll-pane {
            max-height: 60vh;
            overflow-y: auto;
            border-radius: 4px;
            padding-bottom: 5px;
            background-color: var(--el-bg-color-overlay, #ffffff);
        }

        .tm-squig-scroll-pane::-webkit-scrollbar { width: 6px; }
        .tm-squig-scroll-pane::-webkit-scrollbar-track { background: transparent; }
        .tm-squig-scroll-pane::-webkit-scrollbar-thumb {
            background: var(--el-border-color-dark, #dcdfe6);
            border-radius: 3px;
        }
        .tm-squig-scroll-pane::-webkit-scrollbar-thumb:hover {
            background: var(--el-text-color-secondary, #909399);
        }

        .tm-squig-dropdown.is-active {
            opacity: 1;
            visibility: visible;
            transform: translateX(-50%) translateY(0);
        }

        .tm-squig-dropdown::before {
            content: "";
            position: absolute;
            top: -5px;
            left: 50%;
            width: 8px;
            height: 8px;
            background: var(--el-bg-color-overlay, #ffffff);
            border: 1px solid var(--el-border-color-light, #e4e7ed);
            border-right: none;
            border-bottom: none;
            transform: translateX(-50%) rotate(45deg);
            z-index: 2051;
            pointer-events: none;
        }

        .tm-menu-category {
            padding: 10px 15px 6px;
            font-size: 12px;
            color: #c8102e !important;
            font-weight: bold;
            position: sticky;
            top: 0;
            z-index: 10;
            background: var(--el-bg-color-overlay, rgba(255, 255, 255, 0.98));
            @supports (backdrop-filter: blur(8px)) {
                 background: var(--el-bg-color-overlay, rgba(255, 255, 255, 0.8));
                 backdrop-filter: blur(8px);
            }
            border-bottom: 1px solid var(--el-border-color-lighter, #ebeef5);
            box-shadow: 0 1px 2px rgba(0,0,0,0.02);
        }

        .tm-menu-item {
            display: block;
            padding: 8px 20px;
            font-size: 13px;
            color: var(--el-text-color-regular, #606266);
            text-decoration: none;
            transition: background-color 0.2s, color 0.2s;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .tm-menu-item:hover {
            background-color: var(--el-fill-color-light, #ecf5ff);
            color: var(--el-color-primary, #409eff);
        }
    `;
    document.head.appendChild(style);

    const isChinese = navigator.language.startsWith('zh');

    const TEXT = {
        exportBtn: isChinese ? "导出 EQ" : "Export EQ",
        importBtn: isChinese ? "导入参数 EQ" : "Import Parametric EQ",
        urlBtn: isChinese ? "导入 Hangout 链接" : "Import Hangout URL",
        fileName: "FiiO_EQ_Export.txt",
        dialogTitle: isChinese ? "导入 Hangout Audio 数据" : "Import Hangout Audio Data",
        dialogPlaceholder: isChinese ? "在此粘贴分享链接 (https://...)" : "Paste share link here (https://...)",
        cancel: isChinese ? "取消" : "Cancel",
        confirm: isChinese ? "确认" : "Confirm",
        statusInit: isChinese ? "初始化..." : "Initializing...",
        statusImporting: isChinese ? "写入中..." : "Writing...",
        detectSlots: (n) => isChinese ? `识别到 ${n} 个频段` : `Detected ${n} bands`,
        setPreamp: (v) => isChinese ? `Preamp: ${v} dB` : `Preamp: ${v} dB`,
        setBandType: (i, max, t) => isChinese ? `频段 ${i}/${max}: ${t}` : `Band ${i}/${max}: ${t}`,
        resetBand: (i, max) => isChinese ? `重置频段 ${i}/${max}` : `Reset Band ${i}/${max}`,
        finish: isChinese ? "导入完成,请点击保存" : "Done. Please Save.",
        errNoSlots: isChinese ? "未找到EQ频段,请刷新页面" : "No EQ bands found",
        errParse: isChinese ? "解析失败: " : "Parse error: ",
        errNoData: isChinese ? "无效数据" : "No data",
        errInvalidURL: isChinese ? "链接无效" : "Invalid URL",
        errExportNoData: isChinese ? "无法导出,页面未加载" : "Export failed, page not loaded",
        errExportFail: isChinese ? "导出错误: " : "Export error: "
    };

    const ACTION_DELAY = 120;

    function init() {
        const checkExist = setInterval(function() {
            if (window.location.href.includes('/equalizer/custom')) {
                const btnContainer = document.querySelector('.el-row.is-justify-end');
                if (btnContainer && !document.getElementById('tampermonkey-export-btn')) {
                    addButtons(btnContainer);
                }
            }
        }, 1000);

        const checkNavbar = setInterval(function() {
            const rightPanel = document.querySelector('.navbar .content-right');
            if (rightPanel && !document.getElementById('tm-squig-menu')) {
                addSquigLinksMenu(rightPanel);
            }
        }, 1000);
    }

    async function getSquigData() {
        const cached = localStorage.getItem(CACHE_KEY);
        if (cached) {
            try {
                const parsed = JSON.parse(cached);
                const now = new Date().getTime();
                if (now - parsed.timestamp < CACHE_DURATION) {
                    return parsed.data;
                }
            } catch (e) {
                console.warn(e);
            }
        }

        try {
            const response = await fetch('https://squig.link/squigsites.json');
            if (!response.ok) throw new Error('Network response was not ok');
            const data = await response.json();
            localStorage.setItem(CACHE_KEY, JSON.stringify({
                timestamp: new Date().getTime(),
                data: data
            }));
            return data;
        } catch (error) {
            return null;
        }
    }

    function processSquigData(squigSites) {
        const categories = { '5128': [], 'IEMs': [], 'Headphones': [], 'Earbuds': [] };
        squigSites.forEach(site => {
            const username = site.username;
            const name = site.name;
            const rootDomain = site.urlType === "root";
            const subDomain = site.urlType === "subdomain";
            const altDomain = site.urlType === "altDomain";
            const baseUrl = rootDomain ? 'https://squig.link' :
                            altDomain ? site.altDomain :
                            subDomain ? 'https://' + username + '.squig.link' :
                            'https://squig.link/lab/' + username;
            site.dbs.forEach(db => {
                if (categories[db.type]) {
                    categories[db.type].push({ name: name, url: baseUrl + db.folder });
                }
            });
        });
        const result = [];
        ['5128', 'IEMs', 'Headphones', 'Earbuds'].forEach(type => {
            if (categories[type].length > 0) result.push({ category: type, links: categories[type] });
        });
        return result;
    }

    async function addSquigLinksMenu(container) {
        const menuContainer = document.createElement('div');
        menuContainer.id = 'tm-squig-menu';
        menuContainer.className = 'tm-squig-container';
        menuContainer.innerHTML = `<span class="tm-squig-trigger">Squiglinks...</span>`;

        const dropdown = document.createElement('div');
        dropdown.className = 'tm-squig-dropdown';
        const scrollPane = document.createElement('div');
        scrollPane.className = 'tm-squig-scroll-pane';
        scrollPane.innerHTML = `<div style="padding:15px;text-align:center;color:#909399;font-size:12px;">Loading...</div>`;

        dropdown.appendChild(scrollPane);
        menuContainer.appendChild(dropdown);

        menuContainer.addEventListener('click', (e) => {
            e.stopPropagation();
            dropdown.classList.toggle('is-active');
        });

        document.addEventListener('click', (e) => {
            if (!menuContainer.contains(e.target)) dropdown.classList.remove('is-active');
        });

        if (container.firstChild) container.insertBefore(menuContainer, container.firstChild);
        else container.appendChild(menuContainer);

        const rawData = await getSquigData();
        if (rawData) {
            const sortedData = processSquigData(rawData);
            renderMenu(scrollPane, sortedData);
        } else {
            scrollPane.innerHTML = `<div style="padding:15px;text-align:center;color:#c8102e;font-size:12px;">Load Failed</div>`;
        }
    }

    function renderMenu(container, data) {
        container.innerHTML = '';
        data.forEach(group => {
            const catHeader = document.createElement('div');
            catHeader.className = 'tm-menu-category';
            catHeader.textContent = group.category;
            container.appendChild(catHeader);
            group.links.forEach(link => {
                const item = document.createElement('a');
                item.className = 'tm-menu-item';
                item.href = link.url;
                item.textContent = link.name;
                item.target = '_blank';
                container.appendChild(item);
            });
        });
    }

    function addButtons(container) {
        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = '.txt';
        fileInput.style.display = 'none';
        fileInput.id = 'import-file-input';
        fileInput.addEventListener('change', handleFileSelect);
        document.body.appendChild(fileInput);

        const createBtn = (text, onClick, id = null) => {
            const btn = document.createElement('button');
            if (id) btn.id = id;
            btn.className = 'el-button lighter-shadow';
            btn.type = 'button';
            btn.style.marginLeft = '10px';
            btn.innerHTML = `<span>${text}</span>`;
            btn.addEventListener('click', onClick);
            return btn;
        };

        container.appendChild(createBtn(TEXT.exportBtn, exportData, 'tampermonkey-export-btn'));
        container.appendChild(createBtn(TEXT.importBtn, () => fileInput.click()));
        container.appendChild(createBtn(TEXT.urlBtn, showURLDialog));
    }

    function showOverlay(text) {
        let overlay = document.getElementById('tm-loading-overlay');
        if (!overlay) {
            overlay = document.createElement('div');
            overlay.id = 'tm-loading-overlay';
            overlay.className = 'el-loading-mask is-fullscreen tm-loading-fade';
            overlay.style.zIndex = '9999';
            overlay.style.display = 'none';
            overlay.style.opacity = '0';
            document.body.appendChild(overlay);
        }
        overlay.innerHTML = `
            <div class="el-loading-spinner">
                <svg class="circular" viewBox="0 0 50 50">
                    <circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
                </svg>
                <p class="el-loading-text">${text || TEXT.statusInit}</p>
            </div>
        `;
        overlay.style.display = 'block';
        overlay.classList.remove('tm-fade-out');
        void overlay.offsetWidth;
        overlay.classList.add('tm-fade-in');
    }

    function updateOverlayText(text) {
        const overlay = document.getElementById('tm-loading-overlay');
        if (overlay) {
            const textEl = overlay.querySelector('.el-loading-text');
            if(textEl) textEl.innerText = text;
        }
    }

    function hideOverlay() {
        const overlay = document.getElementById('tm-loading-overlay');
        if (overlay) {
            overlay.classList.remove('tm-fade-in');
            overlay.classList.add('tm-fade-out');
            setTimeout(() => {
                if (overlay.classList.contains('tm-fade-out')) {
                    overlay.style.display = 'none';
                }
            }, 300);
        }
    }

    function showURLDialog() {
        if (document.getElementById('tm-url-overlay')) return;
        const overlay = document.createElement('div');
        overlay.id = 'tm-url-overlay';
        overlay.className = 'el-overlay el-modal-dialog';
        overlay.style.zIndex = '2030';
        overlay.style.backgroundColor = 'rgba(0,0,0,0)';
        overlay.style.transition = 'background-color 0.3s';
        overlay.innerHTML = `
            <div role="dialog" aria-modal="true" aria-label="${TEXT.dialogTitle}" class="el-overlay-dialog">
                <div class="el-dialog" tabindex="-1" style="--el-dialog-width: 400px; margin-top: 15vh; opacity: 0; transform: translateY(-20px); transition: opacity 0.3s, transform 0.3s;">
                    <header class="el-dialog__header show-close">
                        <span role="heading" aria-level="2" class="el-dialog__title">${TEXT.dialogTitle}</span>
                        <button class="el-dialog__headerbtn" type="button" id="tm-close-btn"><i class="el-icon el-dialog__close"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"></path></svg></i></button>
                    </header>
                    <div class="el-dialog__body">
                        <div class="el-input"><div class="el-input__wrapper"><input class="el-input__inner" type="text" autocomplete="off" placeholder="${TEXT.dialogPlaceholder}" id="tm-url-input"></div></div>
                    </div>
                    <footer class="el-dialog__footer">
                        <button type="button" class="el-button" id="tm-cancel-btn"><span>${TEXT.cancel}</span></button>
                        <button type="button" class="el-button el-button--primary" id="tm-confirm-btn"><span>${TEXT.confirm}</span></button>
                    </footer>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);
        requestAnimationFrame(() => {
            overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
            const dialog = overlay.querySelector('.el-dialog');
            if (dialog) { dialog.style.opacity = '1'; dialog.style.transform = 'translateY(0)'; }
        });
        setTimeout(() => { const input = document.getElementById('tm-url-input'); if (input) input.focus(); }, 100);
        const closeDialog = () => {
            overlay.style.backgroundColor = 'rgba(0,0,0,0)';
            const dialog = overlay.querySelector('.el-dialog');
            if (dialog) { dialog.style.opacity = '0'; dialog.style.transform = 'translateY(-20px)'; }
            setTimeout(() => { if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); }, 300);
        };
        document.getElementById('tm-close-btn').addEventListener('click', closeDialog);
        document.getElementById('tm-cancel-btn').addEventListener('click', closeDialog);
        document.getElementById('tm-url-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') document.getElementById('tm-confirm-btn').click(); });
        document.getElementById('tm-confirm-btn').addEventListener('click', () => {
            const urlVal = document.getElementById('tm-url-input').value.trim();
            if (!urlVal) return;
            try { const data = parseHangoutURL(urlVal); closeDialog(); applySettings(data); } catch (e) { alert(TEXT.errParse + e.message); }
        });
        overlay.addEventListener('click', (e) => { if (e.target.classList.contains('el-overlay-dialog')) closeDialog(); });
    }

    function parseHangoutURL(urlStr) {
        try {
            const url = new URL(urlStr);
            const params = url.searchParams;
            const data = { preamp: 0, filters: [] };
            if (params.has('P')) data.preamp = parseFloat(params.get('P'));
            for (let i = 1; i <= 20; i++) {
                if (params.has(`T${i}`) && params.has(`F${i}`) && params.has(`G${i}`)) {
                    let typeCode = params.get(`T${i}`).toUpperCase();
                    if (typeCode === 'PK') typeCode = 'P';
                    if (typeCode === 'HSQ') typeCode = 'HS';
                    if (typeCode === 'LSQ') typeCode = 'LS';

                    data.filters.push({
                        index: i, on: true, type: typeCode,
                        freq: parseFloat(params.get(`F${i}`)),
                        gain: parseFloat(params.get(`G${i}`)),
                        q: parseFloat(params.get(`Q${i}`) || 0)
                    });
                }
            }
            if (data.filters.length === 0 && data.preamp === 0) throw new Error(TEXT.errNoData);
            data.filters.forEach((f, idx) => { f.index = idx + 1; });
            return data;
        } catch (e) {
            throw new Error(TEXT.errInvalidURL);
        }
    }

    function handleFileSelect(event) {
        const file = event.target.files[0];
        if (!file) return;
        const reader = new FileReader();
        reader.onload = function(e) {
            try { const data = parseSquigLink(e.target.result); applySettings(data); } catch (err) { alert(TEXT.errParse + err.message); }
        };
        reader.readAsText(file);
        event.target.value = '';
    }

    function parseSquigLink(text) {
        const lines = text.split('\n');
        const data = { preamp: 0, filters: [] };
        lines.forEach(line => {
            line = line.trim();
            if (!line) return;
            if (line.toLowerCase().startsWith('preamp:')) {
                const match = line.match(/Preamp:\s*([\d\.-]+)\s*dB/i);
                if (match) data.preamp = parseFloat(match[1]);
            } else if (line.toLowerCase().startsWith('filter')) {
                const regex = /Filter\s+(\d+):\s+(ON|OFF)\s+([A-Z]+)\s+Fc\s+([\d\.]+)\s+Hz\s+Gain\s+([\d\.\-]+)\s+dB\s+Q\s+([\d\.]+)/i;
                const match = line.match(regex);
                if (match) {
                    let type = match[3].toUpperCase();
                    if (type === 'LSC') type = 'LS';
                    if (type === 'HSC') type = 'HS';
                    if (type === 'LSQ') type = 'LS';
                    if (type === 'HSQ') type = 'HS';
                    if (type === 'PK') type = 'P';

                    data.filters.push({
                        index: parseInt(match[1]), on: match[2].toUpperCase() === 'ON', type: type,
                        freq: parseFloat(match[4]), gain: parseFloat(match[5]), q: parseFloat(match[6])
                    });
                }
            }
        });
        if (data.filters.length === 0 && data.preamp === 0) throw new Error(TEXT.errNoData);
        return data;
    }

    function simulateClick(element) {
        if (!element) return;
        const eventOpts = { bubbles: true, cancelable: true, view: window };
        element.dispatchEvent(new MouseEvent('mousedown', eventOpts));
        element.dispatchEvent(new MouseEvent('mouseup', eventOpts));
        element.dispatchEvent(new MouseEvent('click', eventOpts));
    }

    async function applySettings(data) {
        showOverlay(TEXT.statusInit);
        try {
            const bands = document.querySelectorAll('.band-item');
            const maxSlots = bands.length;
            if (maxSlots === 0) throw new Error(TEXT.errNoSlots);
            updateOverlayText(TEXT.detectSlots(maxSlots));
            await delay(400);
            const tasks = [];

            tasks.push(async () => {
                updateOverlayText(TEXT.setPreamp(data.preamp));
                const globalGainLabel = document.querySelector('.global-gain');
                if (globalGainLabel) {
                    const inputWrapper = globalGainLabel.nextElementSibling;
                    if (inputWrapper) {
                        const input = inputWrapper.querySelector('input');
                        if (input) {
                            input.scrollIntoView({behavior: "auto", block: "center"});
                            safeSetValue(input, data.preamp);
                        }
                    }
                }
            });

            for (let i = 1; i <= maxSlots; i++) {
                const importFilter = data.filters.find(f => f.index === i);
                const band = bands[i - 1];
                if (!band) continue;

                const targetValues = importFilter ? {
                    type: importFilter.type, freq: importFilter.freq, gain: importFilter.gain, q: importFilter.q, isReset: false
                } : {
                    type: 'P', freq: 20000, gain: 0, q: 0.71, isReset: true
                };

                if (targetValues.type === 'HSQ') targetValues.type = 'HS';
                if (targetValues.type === 'LSQ') targetValues.type = 'LS';

                tasks.push(async () => {
                    const msg = targetValues.isReset ? TEXT.resetBand(i, maxSlots) : TEXT.setBandType(i, maxSlots, targetValues.type);
                    updateOverlayText(msg);
                    band.scrollIntoView({behavior: "auto", block: "center"});

                    const btns = band.querySelectorAll('.btn-group button');
                    let targetBtn = null;

                    for (const btn of btns) {
                        const labelEl = btn.querySelector('.label');
                        const btnText = labelEl ? labelEl.textContent.trim() : btn.textContent.trim();
                        if (btnText === targetValues.type) {
                            targetBtn = btn;
                            break;
                        }
                    }

                    if (targetBtn) {
                        simulateClick(targetBtn);
                        const innerLabel = targetBtn.querySelector('.label');
                        if (innerLabel) {
                            simulateClick(innerLabel);
                        }
                    } else {
                         console.warn("Could not find button for type:", targetValues.type);
                    }
                });

                const inputs = band.querySelectorAll('.band-item-row-2 input.el-input__inner');
                if (inputs.length >= 3) {
                    tasks.push(async () => { safeSetValue(inputs[0], targetValues.gain); });
                    tasks.push(async () => { safeSetValue(inputs[1], targetValues.freq); });
                    tasks.push(async () => { safeSetValue(inputs[2], targetValues.q); });
                }
            }

            for (const task of tasks) {
                await task();
                await delay(ACTION_DELAY);
            }
            updateOverlayText(TEXT.finish);
            await delay(1500);
        } catch (error) {
            console.error(error);
            alert("Error: " + error.message);
        } finally {
            hideOverlay();
        }
    }

    function safeSetValue(element, value) {
        if (!element) return;
        const descriptor = Object.getOwnPropertyDescriptor(element, 'value');
        let setter = descriptor ? descriptor.set : null;
        if (!setter) {
             const prototype = Object.getPrototypeOf(element);
             if (prototype) {
                 const protoDescriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
                 setter = protoDescriptor ? protoDescriptor.set : null;
             }
        }
        if (setter) setter.call(element, value);
        else element.value = value;
        element.dispatchEvent(new Event('input', { bubbles: true }));
        element.dispatchEvent(new Event('change', { bubbles: true }));
        element.dispatchEvent(new Event('blur', { bubbles: true }));
    }

    function exportData() {
        try {
            let content = "";
            const globalGainLabel = document.querySelector('.global-gain');
            let preampValue = 0;
            if (globalGainLabel) {
                const inputWrapper = globalGainLabel.nextElementSibling;
                if (inputWrapper) {
                    const input = inputWrapper.querySelector('input');
                    if (input) preampValue = input.value;
                }
            }
            content += `Preamp: ${preampValue} dB\n`;

            const bands = document.querySelectorAll('.band-item');
            if (!bands || bands.length === 0) {
                alert(TEXT.errExportNoData);
                return;
            }

            let filterLines = [];
            bands.forEach((band, index) => {
                const bandIndex = index + 1;
                const selectedTypeBtn = band.querySelector('.filter-button-selected .label');
                if (!selectedTypeBtn) return;
                let typeCode = selectedTypeBtn.textContent.trim();
                if (typeCode === 'P') typeCode = 'PK';
                else if (typeCode === 'LS') typeCode = 'LSC';
                else if (typeCode === 'HS') typeCode = 'HSC';
                const inputs = band.querySelectorAll('.band-item-row-2 input.el-input__inner');
                if (inputs.length < 3) return;
                const gain = inputs[0].value;
                const freq = inputs[1].value;
                const qVal = inputs[2].value;
                filterLines.push(`Filter ${bandIndex}: ON ${typeCode} Fc ${freq} Hz Gain ${gain} dB Q ${qVal}`);
            });
            content += filterLines.join('\n');
            const element = document.createElement('a');
            element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content));
            element.setAttribute('download', TEXT.fileName);
            element.style.display = 'none';
            document.body.appendChild(element);
            element.click();
            document.body.removeChild(element);
        } catch (e) {
            alert(TEXT.errExportFail + e.message);
        }
    }

    function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }

    init();

})();