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.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();

})();