'use strict';
  async function cacheCoversAuto(){
    const portable = await localApiAvailable();
    const todo = getAllCollections().filter(c => c.status !== 'confirmed_deleted' && !c.cover_local_url);
    if(!todo.length){ log('syncLog','封面自动缓存：全部已缓存，跳过。'); return; }
    log('syncLog', `封面自动缓存：开始缓存 ${todo.length} 个未缓存封面${portable ? '到 Docker 文件卷' : '到浏览器 IndexedDB'}...`);
    let ok=0, fail=0;
    for(let i=0; i<todo.length; i++){
      const c=todo[i]; const id=String(c.subject_id);
      const url=coverUrl({...c, cover_local_url:'', cover_data_url:''}); if(!url){ fail++; continue; }
      try{
        if(portable){
          const js=await localPostJSON('/api/cache-cover', {subject_id:id, url});
          if(!js?.ok || !js.url) throw new Error(js?.error||'no url');
          const row=state.collections[id];
          if(row){ row.cover_local_url=js.url; row.cover_local_file=js.file||''; row.cover_cached_at=nowISO(); }
        } else {
          const res=await fetch(url,{mode:'cors',referrerPolicy:'no-referrer'}); if(!res.ok) throw new Error(res.statusText);
          const blob=await res.blob(); const dataUrl=await blobToDataURL(blob); await idbSet('img:'+id,{dataUrl,updated_at:nowISO(),source:url});
        }
        ok++; if((i+1)%20===0){ log('syncLog',`封面自动缓存 ${i+1}/${todo.length}`); await saveState(); }
        await sleep(portable ? 35 : 80);
      }catch(e){ fail++; if(fail<=5) log('syncLog',`封面失败 ${id}：${e.message||e}`); }
    }
    await saveState();
    log('syncLog', `封面自动缓存完成：成功 ${ok}，失败 ${fail}`);
  }
  async function cacheCoversCurrent(){
    const arr = visibleCollections(); if(!arr.length){ toast('没有可缓存的封面'); return; }
    const portable = await localApiAvailable();
    openModal('syncModal'); $('syncLog').textContent='';
    log('syncLog', portable ? `开始缓存当前筛选下 ${arr.length} 个封面到 Docker 文件卷...` : `未检测到 Web 服务，临时缓存到浏览器 IndexedDB：${arr.length} 个封面...`);
    setProgress(0);
    let ok=0, fail=0, i=0;
    for(const c of arr){
      i++; setProgress(i/arr.length*100); const id=String(c.subject_id);
      if(c.cover_local_url || (!portable && await getCachedImage(id))) { ok++; continue; }
      const url=coverUrl({...c, cover_local_url:'', cover_data_url:''}); if(!url){ fail++; continue; }
      try{
        if(portable){
          const js = await localPostJSON('/api/cache-cover', {subject_id:id, url, title:c.title||''});
          if(!js?.ok || !js.url) throw new Error(js?.error || '本地服务没有返回图片路径');
          const row = state.collections[id];
          if(row){ row.cover_local_url = js.url; row.cover_local_file = js.file || ''; row.cover_cached_at = nowISO(); }
        } else {
          const res=await fetch(url, {mode:'cors', referrerPolicy:'no-referrer'}); if(!res.ok) throw new Error(res.statusText); const blob=await res.blob(); const dataUrl=await blobToDataURL(blob); await idbSet('img:'+id, {dataUrl, updated_at:nowISO(), source:url});
        }
        ok++; if(i%10===0){ log('syncLog',`已处理 ${i}/${arr.length}`); await saveState(); }
        await sleep(portable ? 35 : 80);
      }catch(e){ fail++; if(fail <= 5) log('syncLog',`封面失败 ${id}：${e.message || e}`); }
    }
    await saveState();
    log('syncLog',`封面缓存完成：成功 ${ok}，失败 ${fail}`); toast(`封面缓存完成：成功 ${ok}，失败 ${fail}`); render();
  }
  function blobToDataURL(blob){ return new Promise((resolve,reject)=>{ const r=new FileReader(); r.onload=()=>resolve(r.result); r.onerror=()=>reject(r.error); r.readAsDataURL(blob); }); }

  async function imageBlobToDataURLFromCache(c){
    const offlineData = c.cover_data_url || '';
    const localUrl = c.cover_local_url || '';
    const remoteUrl = onlineCoverUrl(c);
    // v0.20: 导出用的 base64 只在内存中的离线副本里生成，不写回 收藏数据.json。
    // Web 运行界面优先使用服务端缓存封面或在线原图。
    if(localUrl){
      try{ const res = await fetch(localUrl, {cache:'no-store'}); if(res.ok) return await blobToDataURL(await res.blob()); }catch{}
    }
    try{ const cached = await idbGet('img:'+c.subject_id); if(cached?.dataUrl) return cached.dataUrl; }catch{}
    if(offlineData) return offlineData;
    // 没有本地缓存时，不主动在线拉远端图片进 HTML，避免导出完整 ZIP 时大量跨域下载卡住。
    // 需要离线封面的条目，请先点“缓存当前封面到目录”。
    return '';
  }
  async function collectCachedImageFiles(arr,onProgress){
    const files = {};
    let i = 0;
    for(const c of arr){
      i++;
      if(onProgress) onProgress(i, arr.length, c);
      if(!c.cover_local_url) { if(i % 40 === 0) await sleep(0); continue; }
      const file = (c.cover_local_file || '').split(/[\\/]/).pop() || (`${c.subject_id}.jpg`);
      try{ const res = await fetch(c.cover_local_url, {cache:'no-store'}); if(!res.ok) continue; files['images/' + file] = new Uint8Array(await res.arrayBuffer()); }
      catch{}
      if(i % 30 === 0) await sleep(0);
    }
    return files;
  }
