'use strict'; function safeExportState(){ const s=structuredClone(state); delete s.settings.token; // v0.20: 常规 JSON 备份不携带 base64 图片,避免 state 变巨大;离线 HTML 会临时内嵌。 for(const c of Object.values(s.collections || {})) delete c.cover_data_url; return s; } function manifest(){ const arr=getAllCollections(); return {app:'Bangumi 保管库 PosterWall', schema:9, exported_at:nowISO(), username:state.settings.username||'', collections_count:arr.length, active_count:arr.filter(c=>c.status!=='confirmed_deleted'&&c.status!=='missing_candidate').length, missing_count:arr.filter(c=>c.status==='missing_candidate').length, history_count:state.history.length}; } function buildJson(){ return JSON.stringify({manifest:manifest(), state:safeExportState()}, null, 2); } function csvCell(s){ return '"' + String(s ?? '').replace(/"/g,'""') + '"'; } function buildCsv(){ const head=['subject_id','分类','状态','标题','原名','中文名','日期','我的评分','Bangumi均分','进度章节','进度卷','我的标签','公共标签','全部标签','吐槽/观后感','是否私密','条目最后更新时间','本地同步时间','本地状态','首次备份','最后确认存在']; const lines=[head.map(csvCell).join(',')]; for(const c of getAllCollections()) lines.push([c.subject_id,SUBJECT_LABEL[c.subject_type]||c.subject_type,STATUS[c.collection_type]||c.collection_type,c.title,c.name,c.name_cn,c.date,c.rate,(formatScoreValue(c.bgm_score || c.raw?.subject?.score || c.raw?.subject?.rating?.score) || ''),c.ep_status,c.vol_status,myTagsOf(c).join(' / '),publicTagNames(c).join(' / '),allTagNames(c).join(' / '),c.comment,c.private?'是':'否',c.bangumi_updated_at,c.last_synced_at || c.last_seen_at || '',c.status,c.first_seen_at,c.last_seen_at].map(csvCell).join(',')); return '\ufeff' + lines.join('\r\n'); } async function enrichExportCollections(mode='inline', onProgress){ const arr = getAllCollections().map(c=>structuredClone(c)); let i = 0; for(const c of arr){ i++; if(onProgress) onProgress(i, arr.length, c); if(mode === 'relative' && c.cover_local_file){ const file = String(c.cover_local_file).split(/[\\/]/).pop(); c.cover_local_url = 'images/' + file; delete c.cover_data_url; } else { const dataUrl = await imageBlobToDataURLFromCache(c); if(dataUrl){ c.cover_data_url = dataUrl; // 离线 HTML 优先使用内嵌图片,避免 /images/... 在 file:// 或压缩包预览中失效。 delete c.cover_local_url; delete c.cover_local_file; } else { // 没缓存到本地的条目,离线 HTML 可以在有网时继续显示远程图;离线则显示占位。 delete c.cover_data_url; } } if(i % 25 === 0) await sleep(0); } return arr; } async function buildOfflineHtml(mode='inline', onProgress){ const offlineState = safeExportState(); offlineState.collections = {}; for(const c of await enrichExportCollections(mode, onProgress)) offlineState.collections[String(c.subject_id)] = c; const payload = JSON.stringify(offlineState).replace(/ 一起删掉,导致离线 HTML 点击海报后没有详情容器。 // 这里克隆整页,只移除打开态/关闭态,并清空详情容器,保留设置、导出、drawer、toast 等必要节点。 const cloned = document.documentElement.cloneNode(true); cloned.querySelectorAll('.show,.closing').forEach(el => el.classList.remove('show','closing')); const drawer = cloned.querySelector('#drawer'); if(drawer) drawer.innerHTML = ''; cloned.querySelectorAll('.sync-log').forEach(el => { el.textContent = ''; }); const toastEl = cloned.querySelector('#toast'); if(toastEl) toastEl.textContent = ''; // Inline the stylesheet so the exported file is self-contained. try{ const cssText = await fetch('css/styles.css').then(r=>r.text()); const link = cloned.querySelector('link[rel="stylesheet"]'); if(link){ const styleEl = document.createElement('style'); styleEl.textContent = cssText; link.replaceWith(styleEl); } }catch{} // Bundle every JS module into one inline script. The modules behave as ordered classic // scripts (shared scope), so concatenating them inside one IIFE reproduces the live app. const moduleNames = ['core','model','view','cron','sync','covers','exports','main']; let bundleBody = ''; for(const n of moduleNames){ try{ bundleBody += (await fetch('js/'+n+'.js').then(r=>r.text())) + '\n'; }catch{} } // Escape any literal closing-script token so the inline