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:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
b8c8d24107
commit
3ccd044527
+83
-6
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user