// ==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 // @downloadURL https://update.greasyfork.icu/scripts/564536/FiiO%20Web%20EQ%20ImportExport%20Tool%20%28SquigLink%29.user.js // @updateURL https://update.greasyfork.icu/scripts/564536/FiiO%20Web%20EQ%20ImportExport%20Tool%20%28SquigLink%29.meta.js // ==/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 = `Squiglinks...`; const dropdown = document.createElement('div'); dropdown.className = 'tm-squig-dropdown'; const scrollPane = document.createElement('div'); scrollPane.className = 'tm-squig-scroll-pane'; scrollPane.innerHTML = `
Loading...
`; 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 = `
Load Failed
`; } } 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 = `${text}`; 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 = `

${text || TEXT.statusInit}

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