window.$ = (id) => document.getElementById(id); window.escapeHtml = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); // 结果会被插进 style="...",cover 来自第三方接口,属不可信输入。 // 既要防 CSS 串逃逸(引号、反斜杠、括号),也要防 HTML 属性逃逸(交给 escapeHtml)。 window.coverStyle = (cover) => { if (!cover) return ''; const raw = String(cover); const url = /^(https?:|data:)/i.test(raw) ? raw : 'file:///' + raw.replace(/\\/g, '/'); if (/[\r\n]/.test(url)) return ''; const css = url.replace(/[\\'"()]/g, (c) => '\\' + c); return window.escapeHtml(`background-image:url('${css}')`); }; window.copyText = async (btn, text) => { await window.api.copy(text); const orig = btn.textContent; btn.textContent = '已复制 ✓'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = orig; btn.classList.remove('copied'); }, 1500); }; window.formatDate = (ts) => { if (!ts) return ''; const d = new Date(ts); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; window.getEnabledSources = () => { let ids = null; try { const raw = localStorage.getItem('enabledSources'); if (raw) ids = JSON.parse(raw); } catch (e) { /* ignore */ } return ids; }; window.setEnabledSources = (ids) => { localStorage.setItem('enabledSources', JSON.stringify(ids)); }; // 通用弹窗: 返回 Promise<{ok, values}|null> window.openModal = (title, bodyHtml, onOk) => { const modal = $('modal'); $('modalTitle').textContent = title; $('modalBody').innerHTML = bodyHtml; modal.classList.remove('hidden'); return new Promise((resolve) => { const close = (result) => { modal.classList.add('hidden'); $('modalOk').onclick = null; $('modalCancel').onclick = null; resolve(result); }; $('modalCancel').onclick = () => close(null); $('modalOk').onclick = async () => { const r = onOk ? await onOk() : true; if (r !== false) close(r); }; }); }; window.confirmModal = (title, text) => window.openModal(title, `

${escapeHtml(text)}

`);