feat: 内置阅读器、批注笔记与 AI 助手,发布 1.3.0

新增 PDF/EPUB/MOBI/AZW 内置阅读器,PDF 分段读取支持超大文件,
批注、读书与画布笔记、封面生成与本地导入。AI 助手支持三种协议、
图像上下文与安全 Markdown 渲染,上下文范围改为 选中/当前页/全文,
页面与全文无需选中文本即可发送,全文会提示可能超出模型限制。

便携版输出目录固定为 PeopleLib-windows-x64,不再随版本号变化,
避免升级后 data/ 被遗留在旧目录。

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
lofyer
2026-08-03 12:13:02 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent b8c8d24107
commit 3ccd044527
307 changed files with 98477 additions and 1148 deletions
+83 -6
View File
@@ -427,41 +427,118 @@ const Browse = (() => {
};
}
function formatBytes(value) {
const bytes = Math.max(0, Number(value) || 0);
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
function createDownloadProgress(row) {
const box = document.createElement('div');
box.className = 'dl-progress';
const track = document.createElement('div');
track.className = 'dl-progress-track';
const fill = document.createElement('div');
fill.className = 'dl-progress-fill';
const label = document.createElement('span');
label.className = 'dl-progress-label';
label.textContent = '准备下载…';
track.appendChild(fill);
box.append(track, label);
row.appendChild(box);
return { box, fill, label };
}
function updateDownloadProgress(progress, data) {
const received = Number(data && data.receivedBytes) || 0;
const total = Number(data && data.totalBytes) || 0;
const ratio = Number(data && data.percent);
if (data && data.percent != null && Number.isFinite(ratio)) {
const percent = Math.max(0, Math.min(1, ratio));
progress.box.classList.remove('indeterminate');
progress.fill.style.width = `${Math.round(percent * 100)}%`;
progress.label.textContent = total
? `${Math.round(percent * 100)}% · ${formatBytes(received)} / ${formatBytes(total)}`
: `${Math.round(percent * 100)}% · ${formatBytes(received)}`;
} else {
progress.box.classList.add('indeterminate');
progress.label.textContent = `${formatBytes(received)} 已下载`;
}
}
async function downloadFile(btn, url) {
const orig = btn.textContent;
const sourceId = state.activeSourceId;
const sourcePostId = state.currentPostId;
const meta = entryMeta();
const suggestedName = btn.dataset.name || '';
const row = btn.closest('.dl-file-row');
const oldProgress = row && row.querySelector('.dl-progress');
if (oldProgress) oldProgress.remove();
const progress = row ? createDownloadProgress(row) : null;
btn.disabled = true;
btn.textContent = '下载中...';
const lib = await window.api.library.findBySource(state.activeSourceId, state.currentPostId);
const lib = await window.api.library.findBySource(sourceId, sourcePostId);
const entryId = (lib.ok && lib.data) ? lib.data.id : undefined;
// 传 meta:条目还不在书库时由主进程自动建,避免下载完却找不到文件
const res = await window.api.downloadFile(url, btn.dataset.name || '', entryId, undefined, entryMeta());
let res;
try {
res = await window.api.downloadFile(
url,
suggestedName,
entryId,
undefined,
meta,
(data) => { if (progress) updateDownloadProgress(progress, data); }
);
} catch (error) {
res = { ok: false, error: (error && error.message) || String(error) };
}
if (res.ok && res.data && res.data.canceled) {
if (progress) progress.box.remove();
btn.textContent = orig; btn.disabled = false;
return;
}
if (!res.ok) {
if (progress) {
progress.box.classList.add('failed');
progress.label.textContent = res.error || '下载失败';
}
btn.textContent = '失败';
btn.title = res.error || '';
setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 2000);
setTimeout(() => {
if (progress) progress.box.remove();
btn.textContent = orig;
btn.disabled = false;
}, 2000);
return;
}
// 下载即入库,刷新"加入书库"按钮并就地提供打开入口
if (window.Library) window.Library.markDirty();
refreshAddButton();
if (state.activeSourceId === sourceId && state.currentPostId === sourcePostId) {
refreshAddButton();
}
const saved = res.data.path;
if (progress) {
updateDownloadProgress(progress, { ...res.data, percent: 1, complete: true });
progress.label.textContent = '下载完成';
setTimeout(() => progress.box.remove(), 900);
}
btn.textContent = '打开';
btn.title = '打开已下载文件';
btn.disabled = false;
btn.classList.add('copied');
btn.classList.add('downloaded');
btn.onclick = async () => {
const r = await window.api.openPath(saved);
if (!r.ok) await confirmModal('打开失败', r.error || '无法打开该文件');
};
const row = btn.closest('.dl-file-row');
if (row && !row.querySelector('.reveal-btn')) {
const reveal = document.createElement('button');
reveal.className = 'copy-btn reveal-btn';