feat: PeopleLib 开放文献客户端,集成 Z-Library 与 LibGen 等多源检索
Electron 桌面客户端,聚合多个开放获取文献源的搜索、详情与下载。 新增数据源: - Z-Library:邮箱登录(凭据本地存储),会话失效自动重登 - LibGen:适配新版 libgen.ac 前端(旧版 search.php 镜像已全部下线) - Memory of the World、Sci-Hub、Anna's Archive 基础设施: - mirror.js:镜像故障转移,支持串行优先与并发竞速两种策略, 失效镜像 5 分钟冷却后自动重试,避免站点恢复后被永久跳过 - http.js:统一 15 秒请求超时,防止单个卡死镜像拖垮整次搜索 - settings.js:全局代理配置持久化,经 Electron net.fetch 生效于所有请求 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit
1a1288ce18
@@ -0,0 +1,114 @@
|
||||
const Library = (() => {
|
||||
let dirty = true;
|
||||
let sortMode = localStorage.getItem('libSortMode') || 'added';
|
||||
let grid, statusEl;
|
||||
|
||||
const SORTERS = {
|
||||
added: (a, b) => (b.addedAt || 0) - (a.addedAt || 0),
|
||||
title: (a, b) => String(a.title).localeCompare(String(b.title), 'zh'),
|
||||
author: (a, b) => String((a.authors || [])[0] || '').localeCompare(String((b.authors || [])[0] || ''), 'zh')
|
||||
};
|
||||
|
||||
function init() {
|
||||
grid = $('libGrid');
|
||||
statusEl = $('libStatus');
|
||||
$('addLocalBtn').onclick = addLocal;
|
||||
window.api.library.onChanged(() => { dirty = true; refresh(true); });
|
||||
}
|
||||
|
||||
function getSortMode() { return sortMode; }
|
||||
function setSortMode(m) {
|
||||
sortMode = m;
|
||||
localStorage.setItem('libSortMode', m);
|
||||
dirty = true;
|
||||
refresh(true);
|
||||
}
|
||||
|
||||
async function refresh(force) {
|
||||
if (!force && !dirty) return;
|
||||
const res = await window.api.library.list();
|
||||
dirty = false;
|
||||
if (!res.ok) { statusEl.textContent = '加载失败:' + res.error; return; }
|
||||
const items = res.data.slice().sort(SORTERS[sortMode] || SORTERS.added);
|
||||
statusEl.textContent = `共 ${items.length} 条`;
|
||||
if (!items.length) {
|
||||
grid.innerHTML = '<div class="empty">书库为空,去「检索」页添加文献 / 图书吧</div>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = items.map((it) => {
|
||||
const hasFile = (it.files || []).some((f) => f.path);
|
||||
const badge = hasFile
|
||||
? '<span class="card-badge">已下载</span>'
|
||||
: '<span class="card-badge miss">未下载</span>';
|
||||
return `
|
||||
<div class="card" data-id="${escapeHtml(it.id)}">
|
||||
<div class="card-cover" style="${coverStyle(it.cover)}">${it.cover ? '' : `<div class="ph">${escapeHtml(it.title)}</div>`}</div>
|
||||
<div class="card-title">${escapeHtml(it.title)}</div>
|
||||
${(it.authors && it.authors.length) ? `<div class="card-sub">${escapeHtml(it.authors.slice(0, 2).join(', '))}</div>` : ''}
|
||||
${badge}
|
||||
<div class="lib-card-actions">
|
||||
<button class="open-btn" data-act="open" ${hasFile ? '' : 'disabled'}>打开</button>
|
||||
${it.url ? '<button data-act="page">页面</button>' : ''}
|
||||
<button data-act="remove">移除</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
grid.querySelectorAll('.card').forEach((el) => {
|
||||
const id = el.dataset.id;
|
||||
el.querySelectorAll('button').forEach((btn) => {
|
||||
btn.onclick = (e) => { e.stopPropagation(); onAction(id, btn.dataset.act); };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function onAction(id, act) {
|
||||
const res = await window.api.library.get(id);
|
||||
if (!res.ok || !res.data) return;
|
||||
const it = res.data;
|
||||
if (act === 'open') {
|
||||
const f = (it.files || []).find((x) => x.path);
|
||||
if (f) window.api.openPath(f.path);
|
||||
} else if (act === 'page') {
|
||||
if (it.url) window.api.openExternal(it.url);
|
||||
} else if (act === 'remove') {
|
||||
const hasFile = (it.files || []).some((f) => f.path);
|
||||
const r = await openModal('移除条目', `
|
||||
<p>确定移除「${escapeHtml(it.title)}」吗?</p>
|
||||
${hasFile ? '<p style="margin-top:8px"><label><input type="checkbox" id="delFiles" /> 同时删除已下载的文件</label></p>' : ''}
|
||||
`, () => ({ del: !!(document.getElementById('delFiles') || {}).checked }));
|
||||
if (!r) return;
|
||||
await window.api.library.remove(id, r.del);
|
||||
dirty = true;
|
||||
refresh(true);
|
||||
}
|
||||
}
|
||||
|
||||
async function addLocal() {
|
||||
const r = await window.api.pickFile();
|
||||
if (!r.ok || !r.data) return;
|
||||
const { path: p, name } = r.data;
|
||||
const res = await openModal('添加本地文件', `
|
||||
<p>文件:${escapeHtml(p)}</p>
|
||||
<input type="text" id="localTitle" placeholder="标题" value="${escapeHtml(name)}" />
|
||||
<input type="text" id="localAuthor" placeholder="作者(可选)" />
|
||||
`, () => ({
|
||||
title: (document.getElementById('localTitle').value || name).trim(),
|
||||
author: (document.getElementById('localAuthor').value || '').trim()
|
||||
}));
|
||||
if (!res) return;
|
||||
await window.api.library.add({
|
||||
title: res.title,
|
||||
authors: res.author ? [res.author] : [],
|
||||
files: [{ path: p, name: p.split(/[\\/]/).pop(), format: (p.split('.').pop() || '').toUpperCase() }]
|
||||
});
|
||||
dirty = true;
|
||||
refresh(true);
|
||||
}
|
||||
|
||||
function markDirty() { dirty = true; }
|
||||
|
||||
return { init, refresh, markDirty, getSortMode, setSortMode };
|
||||
})();
|
||||
|
||||
window.Library = Library;
|
||||
Reference in New Issue
Block a user