This commit is contained in:
+170
@@ -0,0 +1,170 @@
|
||||
'use strict';
|
||||
function uniqStrings(arr){
|
||||
const seen = new Set(); const out = [];
|
||||
for(const v of arr || []){
|
||||
const name = String(v ?? '').trim();
|
||||
if(!name || seen.has(name)) continue;
|
||||
seen.add(name); out.push(name);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function normalizeTags(tags){
|
||||
if(!tags) return [];
|
||||
if(Array.isArray(tags)) return uniqStrings(tags.map(t => typeof t === 'string' ? t : (t?.name || t?.label || t?.tag || '')));
|
||||
if(typeof tags === 'string') return uniqStrings(tags.split(/[,,\s]+/));
|
||||
return [];
|
||||
}
|
||||
function normalizePublicTags(tags){
|
||||
if(!tags) return [];
|
||||
const list = Array.isArray(tags) ? tags : (typeof tags === 'string' ? tags.split(/[,,\s]+/) : []);
|
||||
const out = []; const seen = new Set();
|
||||
for(const t of list){
|
||||
const name = String(typeof t === 'string' ? t : (t?.name || t?.label || t?.tag || '')).trim();
|
||||
if(!name || seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
const count = typeof t === 'object' && t ? Number(t.count ?? t.total_cont ?? t.total ?? t.num ?? 0) || 0 : 0;
|
||||
out.push(count ? {name, count} : {name});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function myTagsOf(c){ return normalizeTags(c?.myTags || c?.my_tags || c?.tags || []); }
|
||||
function publicTagsOf(c){ return normalizePublicTags(c?.publicTags || c?.public_tags || c?.subjectTags || c?.raw?.subject?.tags || []); }
|
||||
function publicTagNames(c){ return publicTagsOf(c).map(t=>t.name).filter(Boolean); }
|
||||
function allTagNames(c){ return uniqStrings([...myTagsOf(c), ...publicTagNames(c)]); }
|
||||
function tagKey(scope, name){ return scope + '::' + String(name || ''); }
|
||||
function parseTagKey(key){
|
||||
if(key === '全部' || key === '无标签') return {scope:'special', name:key};
|
||||
const idx = String(key || '').indexOf('::');
|
||||
if(idx > 0) return {scope:String(key).slice(0, idx), name:String(key).slice(idx + 2)};
|
||||
return {scope:'all', name:String(key || '')};
|
||||
}
|
||||
function tagLabelFromKey(key){
|
||||
const t = parseTagKey(key);
|
||||
if(t.scope === 'my') return '我的:' + t.name;
|
||||
if(t.scope === 'public') return '公共:' + t.name;
|
||||
return t.name || '全部';
|
||||
}
|
||||
function bgmTagUrl(subjectType, name){
|
||||
const path = BGM_TYPE_PATH[Number(subjectType)] || 'subject_search';
|
||||
const q = encodeURIComponent(name || '');
|
||||
const base = bgmSiteBase();
|
||||
return path === 'subject_search' ? `${base}/subject_search/${q}?cat=all` : `${base}/${path}/tag/${q}`;
|
||||
}
|
||||
function publicTagsForDisplay(c){ return publicTagsOf(c); }
|
||||
function shortValue(v, max=80){
|
||||
const t = Array.isArray(v) ? v.join(' / ') : String(v ?? '');
|
||||
return t.length > max ? t.slice(0, max) + '…' : t;
|
||||
}
|
||||
function valuesSame(a,b){ return stableStringify(a ?? '') === stableStringify(b ?? ''); }
|
||||
function collectionFieldDiff(oldC, newC){
|
||||
const fields = [
|
||||
['collection_type','收藏状态', v => STATUS[v] || v || '-'],
|
||||
['rate','我的评分', v => v || '未评分'],
|
||||
['ep_status','章节进度', v => v || '-'],
|
||||
['vol_status','卷进度', v => v || '-'],
|
||||
['comment','吐槽/观后感', v => shortValue(v || '无吐槽', 120)],
|
||||
['private','私密状态', v => v ? '私密' : '公开'],
|
||||
['tags','我的标签', v => shortValue(normalizeTags(v), 120)],
|
||||
['publicTags','公共标签', v => shortValue(normalizePublicTags(v).map(x=>x.count ? `${x.name}(${x.count})` : x.name), 120)],
|
||||
['bangumi_updated_at','API 更新时间', v => formatDate(v) || '-'],
|
||||
['title','标题', v => v || '-'],
|
||||
['date','上映日期', v => v || '-'],
|
||||
['bgm_score','Bangumi 均分', v => formatScoreValue(v) || '-'],
|
||||
['rank','排名', v => v || '-']
|
||||
];
|
||||
const changes = [];
|
||||
for(const [key, label, fmt] of fields){
|
||||
const oldVal = key === 'tags' ? myTagsOf(oldC) : (key === 'publicTags' ? publicTagsOf(oldC) : oldC?.[key]);
|
||||
const newVal = key === 'tags' ? myTagsOf(newC) : (key === 'publicTags' ? publicTagsOf(newC) : newC?.[key]);
|
||||
if(!valuesSame(oldVal, newVal)) changes.push({field:key, label, from:fmt(oldVal), to:fmt(newVal), old:oldVal ?? '', new:newVal ?? ''});
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
function historyChangeHtml(h){
|
||||
const changes = Array.isArray(h.changes) ? h.changes : [];
|
||||
if(!changes.length) return esc(h.note || '');
|
||||
return changes.slice(0,6).map(x => `<div class="hist-change"><span>${esc(x.label || x.field)}</span>:${esc(x.from)} → ${esc(x.to)}</div>`).join('') + (changes.length > 6 ? `<div class="hist-change">还有 ${changes.length - 6} 项变化…</div>` : '');
|
||||
}
|
||||
function normalizeCollection(item, fallbackType){
|
||||
const s = item.subject || {};
|
||||
const imgs = s.images || {};
|
||||
const st = item.subject_type || s.type || fallbackType?.subject_type || 0;
|
||||
const ct = item.type || fallbackType?.collection_type || 0;
|
||||
const title = s.name_cn || s.name || `Subject ${item.subject_id || s.id}`;
|
||||
const raw = item;
|
||||
return {
|
||||
subject_id: Number(item.subject_id || s.id), subject_type:Number(st), collection_type:Number(ct),
|
||||
title, name:s.name||'', name_cn:s.name_cn||'', date:s.date||'', eps:s.eps ?? '', volumes:s.volumes ?? '',
|
||||
bgm_score: s.score ?? s.rating?.score ?? '', rank: s.rank ?? '',
|
||||
summary:s.summary||s.short_summary||'', image: imgs.large || imgs.common || imgs.medium || imgs.grid || imgs.small || '', images:imgs,
|
||||
rate:item.rate ?? 0, comment:item.comment || '', tags:normalizeTags(item.tags), myTags:normalizeTags(item.tags), publicTags:normalizePublicTags(s.tags || item.publicTags || item.public_tags), ep_status:item.ep_status ?? '', vol_status:item.vol_status ?? '',
|
||||
private:!!item.private,
|
||||
collection_created_at:item.created_at || item.createdAt || item.collect_created_at || '',
|
||||
bangumi_updated_at:item.updated_at || item.updatedAt || '', raw
|
||||
};
|
||||
}
|
||||
function stableStringify(obj){
|
||||
if(obj === null || typeof obj !== 'object') return JSON.stringify(obj);
|
||||
if(Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
||||
return '{' + Object.keys(obj).sort().map(k => JSON.stringify(k)+':'+stableStringify(obj[k])).join(',') + '}';
|
||||
}
|
||||
function hashString(str){ let h=2166136261; for(let i=0;i<str.length;i++){ h ^= str.charCodeAt(i); h = Math.imul(h,16777619); } return (h>>>0).toString(16); }
|
||||
function comparable(n){ return {subject_id:n.subject_id, subject_type:n.subject_type, collection_type:n.collection_type, title:n.title, name:n.name, name_cn:n.name_cn, date:n.date, eps:n.eps, volumes:n.volumes, bgm_score:n.bgm_score, rank:n.rank, summary:n.summary, image:n.image, rate:n.rate, comment:n.comment, tags:myTagsOf(n), publicTags:publicTagsOf(n), ep_status:n.ep_status, vol_status:n.vol_status, private:n.private, collection_created_at:n.collection_created_at, bangumi_updated_at:n.bangumi_updated_at}; }
|
||||
function makeSnapshot(n){ return {...n, content_hash:hashString(stableStringify(comparable(n)))}; }
|
||||
|
||||
function getAllCollections(){ return Object.values(state.collections || {}); }
|
||||
function visibleCollections(){
|
||||
const q = filters.q.trim().toLowerCase();
|
||||
let arr = getAllCollections().filter(c => c.status !== 'confirmed_deleted');
|
||||
// Guests (read-only public mode) never see the safety box, even if the filter is somehow set to it.
|
||||
const sf = (!authReady && filters.status === 99) ? 0 : filters.status;
|
||||
if(sf === 99) arr = getAllCollections().filter(c => c.status === 'missing_candidate' || c.status === 'protected_local' || c.status === 'confirmed_deleted');
|
||||
else if(sf) arr = arr.filter(c => c.collection_type === sf && c.status !== 'missing_candidate');
|
||||
else arr = arr.filter(c => c.status !== 'missing_candidate');
|
||||
if(filters.type) arr = arr.filter(c => c.subject_type === filters.type);
|
||||
if(isPublicMode && !authReady && !publicShowNsfw) arr = arr.filter(c => c.nsfw !== true);
|
||||
else if(filters.nsfw === 'only') arr = arr.filter(c => c.nsfw === true);
|
||||
else if(filters.nsfw === 'hide') arr = arr.filter(c => c.nsfw !== true);
|
||||
if(filters.tag !== '全部') arr = arr.filter(c => {
|
||||
const tf = parseTagKey(filters.tag);
|
||||
if(tf.name === '无标签') return !myTagsOf(c).length && !publicTagNames(c).length;
|
||||
if(tf.scope === 'my') return myTagsOf(c).includes(tf.name);
|
||||
if(tf.scope === 'public') return publicTagNames(c).includes(tf.name);
|
||||
return allTagNames(c).includes(tf.name);
|
||||
});
|
||||
if(filters.hasComment) arr = arr.filter(c => !!(c.comment && String(c.comment).trim()));
|
||||
if(q){ arr = arr.filter(c => [c.title,c.name,c.name_cn,c.comment,c.summary,c.date].join(' ').toLowerCase().includes(q)); }
|
||||
const cmpDate = (x) => Date.parse(x || '') || 0;
|
||||
arr.sort((a,b)=>{
|
||||
if(filters.sort==='updated_asc') return cmpDate(a.bangumi_updated_at)-cmpDate(b.bangumi_updated_at);
|
||||
if(filters.sort==='rate_desc') return (Number(b.rate)||0)-(Number(a.rate)||0) || cmpDate(b.bangumi_updated_at)-cmpDate(a.bangumi_updated_at);
|
||||
if(filters.sort==='name_asc') return (a.title||'').localeCompare(b.title||'', 'zh-Hans-CN');
|
||||
if(filters.sort==='first_seen_desc') return cmpDate(b.first_seen_at)-cmpDate(a.first_seen_at);
|
||||
return cmpDate(b.bangumi_updated_at)-cmpDate(a.bangumi_updated_at);
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
|
||||
async function getCachedImage(id){
|
||||
const c=state.collections?.[String(id)];
|
||||
// v0.20: Data URL 只在离线导出页优先使用。在线/便携模式优先走真实文件或原始在线图片,
|
||||
// 避免超大的 base64 封面残留到运行库后影响在线海报显示。
|
||||
if(window.BANGUMI_VAULT_OFFLINE_STATE && c?.cover_data_url) return {dataUrl:c.cover_data_url};
|
||||
if(c?.cover_local_url) return {url:c.cover_local_url, file:c.cover_local_file||''};
|
||||
try{ const cached = await idbGet('img:'+id); if(cached?.dataUrl || cached?.url) return cached; }catch{}
|
||||
if(c?.cover_data_url && !onlineCoverUrl(c)) return {dataUrl:c.cover_data_url};
|
||||
return null;
|
||||
}
|
||||
function onlineCoverUrl(c){ return c.image || c.images?.large || c.images?.common || c.images?.medium || c.images?.grid || c.images?.small || ''; }
|
||||
// Rewrite an online image URL through the configured proxy/mirror (empty = no change).
|
||||
function applyImageProxy(url, proxy){
|
||||
proxy = (proxy || '').trim();
|
||||
if(!proxy || !url || !/^https?:\/\//i.test(url)) return url;
|
||||
return proxy.includes('{url}') ? proxy.replace('{url}', encodeURIComponent(url)) : proxy + url;
|
||||
}
|
||||
function coverUrl(c){
|
||||
if(window.BANGUMI_VAULT_OFFLINE_STATE && c.cover_data_url) return c.cover_data_url;
|
||||
// Local cached covers (/images/...) are served by us — never proxy those; only online bgm images.
|
||||
return c.cover_local_url || applyImageProxy(onlineCoverUrl(c), state.settings?.imageProxy) || c.cover_data_url || '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user