feat: 完善本地书库与发布更新流程
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
de3e1d8a44
commit
b8c8d24107
+122
-2
@@ -84,19 +84,139 @@ $('zlibLogoutBtn').onclick = async () => {
|
||||
|
||||
refreshZlibStatus();
|
||||
|
||||
async function refreshLibraryDir() {
|
||||
const r = await window.api.library.getDir();
|
||||
if (r.ok && r.data) $('libDirPath').textContent = r.data.dir + (r.data.isDefault ? '(默认)' : '');
|
||||
}
|
||||
|
||||
$('libDirPickBtn').onclick = async () => {
|
||||
const pick = await window.api.library.pickDir();
|
||||
if (!pick.ok || !pick.data) return;
|
||||
const dest = pick.data;
|
||||
|
||||
const cur = await window.api.library.getDir();
|
||||
if (cur.ok && cur.data && cur.data.dir === dest) return;
|
||||
|
||||
// 让用户决定旧目录里已有的书怎么处理
|
||||
let migrate = false;
|
||||
const choice = await openModal('切换书库目录', `
|
||||
<p>新目录:</p>
|
||||
<div class="settings-path" style="margin:6px 0 12px;">${escapeHtml(dest)}</div>
|
||||
<label style="display:block;margin-bottom:6px;">
|
||||
<input type="radio" name="migMode" value="migrate" checked /> 迁移:把现有书库内容移动到新目录
|
||||
</label>
|
||||
<label style="display:block;">
|
||||
<input type="radio" name="migMode" value="switch" /> 直接切换:旧目录原样保留,新目录重新扫描
|
||||
</label>
|
||||
`, () => {
|
||||
const sel = document.querySelector('input[name="migMode"]:checked');
|
||||
return { migrate: sel && sel.value === 'migrate' };
|
||||
});
|
||||
if (!choice) return;
|
||||
migrate = choice.migrate;
|
||||
|
||||
const res = await window.api.library.setDir(dest, migrate);
|
||||
if (!res.ok) { await confirmModal('切换失败', res.error || '未知错误'); return; }
|
||||
await refreshLibraryDir();
|
||||
if (window.Library) window.Library.markDirty();
|
||||
};
|
||||
|
||||
$('libDirOpenBtn').onclick = async () => {
|
||||
const r = await window.api.library.getDir();
|
||||
if (r.ok && r.data) window.api.openPath(r.data.dir);
|
||||
};
|
||||
|
||||
async function refreshAskSave() {
|
||||
const r = await window.api.settings.get('askSavePath', false);
|
||||
$('askSaveChk').checked = !!(r.ok && r.data);
|
||||
}
|
||||
$('askSaveChk').onchange = () => window.api.settings.set('askSavePath', $('askSaveChk').checked);
|
||||
|
||||
refreshLibraryDir();
|
||||
refreshAskSave();
|
||||
|
||||
async function refreshProxy() {
|
||||
const r = await window.api.proxy.get();
|
||||
if (r.ok) $('proxyInput').value = r.data || '';
|
||||
}
|
||||
$('proxySaveBtn').onclick = async () => {
|
||||
await window.api.proxy.set($('proxyInput').value.trim());
|
||||
$('proxySaveBtn').textContent = '已保存 ✓';
|
||||
const r = await window.api.proxy.set($('proxyInput').value.trim());
|
||||
$('proxySaveBtn').textContent = r.ok ? '已保存 ✓' : '保存失败';
|
||||
$('proxySaveBtn').title = r.ok ? '' : (r.error || '代理地址无效');
|
||||
setTimeout(() => { $('proxySaveBtn').textContent = '保存'; }, 1500);
|
||||
};
|
||||
refreshProxy();
|
||||
|
||||
async function refreshSemanticKeyStatus() {
|
||||
const r = await window.api.semanticScholar.keyStatus();
|
||||
const status = r.ok && r.data ? r.data : { configured: false, persistent: false };
|
||||
$('semanticKeyStatus').textContent = status.configured
|
||||
? (status.persistent ? '已配置(由系统安全存储加密)' : '已配置(仅本次运行)')
|
||||
: '未配置(匿名请求容易被限流)';
|
||||
$('semanticKeyClearBtn').classList.toggle('hidden', !status.configured);
|
||||
}
|
||||
|
||||
$('semanticKeySaveBtn').onclick = async () => {
|
||||
const key = $('semanticKeyInput').value.trim();
|
||||
if (!key) {
|
||||
$('semanticKeySaveBtn').textContent = '请输入 Key';
|
||||
setTimeout(() => { $('semanticKeySaveBtn').textContent = '保存'; }, 1500);
|
||||
return;
|
||||
}
|
||||
const r = await window.api.semanticScholar.setKey(key);
|
||||
$('semanticKeyInput').value = '';
|
||||
$('semanticKeySaveBtn').textContent = r.ok ? '已保存 ✓' : '保存失败';
|
||||
$('semanticKeySaveBtn').title = r.ok ? '' : (r.error || '保存失败');
|
||||
await refreshSemanticKeyStatus();
|
||||
setTimeout(() => { $('semanticKeySaveBtn').textContent = '保存'; }, 1500);
|
||||
};
|
||||
|
||||
$('semanticKeyClearBtn').onclick = async () => {
|
||||
const ok = await confirmModal('清除 API Key', '确定清除 Semantic Scholar API Key 吗?');
|
||||
if (!ok) return;
|
||||
await window.api.semanticScholar.clearKey();
|
||||
await refreshSemanticKeyStatus();
|
||||
};
|
||||
|
||||
refreshSemanticKeyStatus();
|
||||
|
||||
async function runUpdateCheck(silent) {
|
||||
const statusEl = $('updateStatus');
|
||||
const btn = $('checkUpdateBtn');
|
||||
if (!silent) {
|
||||
btn.disabled = true;
|
||||
statusEl.textContent = '正在检查...';
|
||||
}
|
||||
const res = await window.api.checkUpdate();
|
||||
if (!silent) btn.disabled = false;
|
||||
if (!res.ok) {
|
||||
if (!silent) statusEl.textContent = '检查失败:' + res.error;
|
||||
return;
|
||||
}
|
||||
const { latest, hasUpdate, url } = res.data;
|
||||
if (hasUpdate) {
|
||||
statusEl.textContent = `发现新版本 ${latest}`;
|
||||
const ok = await openModal(
|
||||
'发现新版本',
|
||||
`<p>检测到新版本 <b>${escapeHtml(latest)}</b>,是否前往 GitHub Releases 下载?</p>`
|
||||
);
|
||||
if (ok) window.api.openExternal(url);
|
||||
} else if (!silent) {
|
||||
statusEl.textContent = '已是最新版本';
|
||||
}
|
||||
}
|
||||
|
||||
window.api.getVersion().then((r) => {
|
||||
if (r && r.ok) $('appVersion').textContent = 'v' + r.data;
|
||||
});
|
||||
|
||||
const autoCheckEl = $('autoCheckUpdate');
|
||||
window.api.settings.get('autoCheckUpdate', false).then((r) => {
|
||||
autoCheckEl.checked = !!(r.ok && r.data);
|
||||
if (autoCheckEl.checked) runUpdateCheck(true);
|
||||
});
|
||||
autoCheckEl.onchange = () => window.api.settings.set('autoCheckUpdate', autoCheckEl.checked);
|
||||
|
||||
$('checkUpdateBtn').onclick = () => runUpdateCheck(false);
|
||||
|
||||
switchTab('library');
|
||||
|
||||
+42
-2
@@ -30,6 +30,7 @@
|
||||
<div class="toolbar">
|
||||
<span id="libStatus" class="status-bar"></span>
|
||||
<div class="spacer"></div>
|
||||
<button id="rescanBtn" class="tb-btn ghost">重新扫描</button>
|
||||
<button id="addLocalBtn" class="tb-btn">+ 添加本地文件</button>
|
||||
</div>
|
||||
<div id="libGrid" class="grid"></div>
|
||||
@@ -80,6 +81,24 @@
|
||||
<div id="sourceList" class="source-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-info">
|
||||
<div class="settings-item-label">书库目录</div>
|
||||
<div class="settings-item-desc">元数据、下载文件与封面都存放在此目录;启动时自动扫描 files 子目录导入新书</div>
|
||||
<div class="settings-path" id="libDirPath">-</div>
|
||||
</div>
|
||||
<button id="libDirPickBtn" class="tb-btn">更改</button>
|
||||
<button id="libDirOpenBtn" class="tb-btn ghost">打开</button>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-info">
|
||||
<div class="settings-item-label">下载前询问保存位置</div>
|
||||
<div class="settings-item-desc">关闭时直接存入书库目录并自动入库;开启则每次弹出另存为对话框</div>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="askSaveChk" /></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-info">
|
||||
@@ -103,6 +122,17 @@
|
||||
<button id="proxySaveBtn" class="tb-btn">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-info">
|
||||
<div class="settings-item-label">Semantic Scholar API Key</div>
|
||||
<div class="settings-item-desc" id="semanticKeyStatus">未配置(匿名请求容易被限流)</div>
|
||||
</div>
|
||||
<input id="semanticKeyInput" class="settings-input" type="password" placeholder="可选 API Key" autocomplete="off" />
|
||||
<button id="semanticKeySaveBtn" class="tb-btn">保存</button>
|
||||
<button id="semanticKeyClearBtn" class="tb-btn ghost hidden">清除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-info">
|
||||
@@ -116,9 +146,19 @@
|
||||
<div class="settings-group">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-info">
|
||||
<div class="settings-item-label">关于</div>
|
||||
<div class="settings-item-desc">PeopleLib <span id="appVersion">-</span> · 开放获取文献与公版图书客户端</div>
|
||||
<div class="settings-item-label">检查更新</div>
|
||||
<div class="settings-item-desc">当前版本 <span id="appVersion">-</span> · <span id="updateStatus">从 GitHub 获取最新版本</span></div>
|
||||
</div>
|
||||
<button id="checkUpdateBtn" class="tb-btn">检查更新</button>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-info">
|
||||
<div class="settings-item-label">启动时自动检查更新</div>
|
||||
<div class="settings-item-desc">发现新版本后可前往 GitHub Releases 下载二进制文件</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="autoCheckUpdate" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+47
-12
@@ -93,6 +93,11 @@ body {
|
||||
.browse-head .toolbar { margin-bottom: 0; }
|
||||
.browse-head .status-bar { margin: 0; min-height: 0; }
|
||||
.browse-head .status-bar:not(:empty) { margin-top: 8px; }
|
||||
#browseGridView {
|
||||
min-height: calc(100vh - 84px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.source-select {
|
||||
height: 30px; padding: 0 12px;
|
||||
@@ -157,25 +162,43 @@ body {
|
||||
|
||||
/* 分页 */
|
||||
.pager {
|
||||
position: sticky; bottom: -20px; z-index: 6;
|
||||
margin: 16px -20px -20px; padding: 12px 20px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
position: sticky;
|
||||
bottom: -20px;
|
||||
margin: auto -20px -20px;
|
||||
padding: 14px 20px 20px;
|
||||
background: linear-gradient(to top, var(--bg) 62%, transparent);
|
||||
display: flex; align-items: center; justify-content: center; gap: 12px;
|
||||
z-index: 5;
|
||||
}
|
||||
.page-btn {
|
||||
height: 28px; padding: 0 14px;
|
||||
background: var(--bg-soft); color: var(--text);
|
||||
border: 1px solid var(--line); border-radius: 8px; font-size: 12px; cursor: pointer;
|
||||
padding: 8px 20px;
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--text);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
.page-btn:hover:not(:disabled) { border-color: var(--accent); }
|
||||
.page-btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
||||
.page-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent-bright); }
|
||||
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.page-info { color: var(--text-dim); font-size: 13px; }
|
||||
.page-jump { display: flex; align-items: center; gap: 6px; color: var(--text-dim); font-size: 12px; }
|
||||
.page-jump { display: flex; align-items: center; gap: 8px; color: var(--text-dim); font-size: 13px; }
|
||||
.jump-input {
|
||||
width: 60px; height: 28px; text-align: center;
|
||||
background: rgba(255,255,255,0.06); border: 1px solid var(--line); border-radius: 8px;
|
||||
color: var(--text); font-size: 12px; outline: none;
|
||||
width: 64px;
|
||||
padding: 7px 8px;
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--text);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
.jump-input:focus { outline: none; border-color: var(--accent); }
|
||||
.jump-input::-webkit-outer-spin-button,
|
||||
.jump-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
||||
|
||||
/* 详情 */
|
||||
.back-btn {
|
||||
@@ -258,9 +281,21 @@ body {
|
||||
.settings-item-block { flex-direction: column; align-items: stretch; }
|
||||
.settings-item-label { font-size: 14px; font-weight: 600; }
|
||||
.settings-item-desc { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
|
||||
.settings-input {
|
||||
width: 220px; padding: 6px 10px;
|
||||
background: #222; border: 1px solid #444; color: #eee; border-radius: 4px;
|
||||
}
|
||||
.settings-input:focus { outline: none; border-color: var(--accent); }
|
||||
.source-list { display: flex; flex-direction: column; gap: 2px; padding: 10px 0 14px; }
|
||||
.source-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; cursor: pointer; }
|
||||
.source-row input { accent-color: var(--accent); }
|
||||
.settings-path {
|
||||
margin-top: 6px; padding: 6px 10px;
|
||||
background: rgba(255,255,255,0.05); border: 1px solid var(--line); border-radius: 6px;
|
||||
font-family: Consolas, monospace; font-size: 11px; color: var(--text-dim);
|
||||
word-break: break-all;
|
||||
}
|
||||
.switch input { width: 38px; height: 20px; accent-color: var(--accent); cursor: pointer; }
|
||||
|
||||
/* 弹窗 */
|
||||
.modal {
|
||||
|
||||
+76
-35
@@ -12,6 +12,8 @@ const Browse = (() => {
|
||||
scrollY: 0,
|
||||
sourceList: [],
|
||||
aggToken: 0,
|
||||
gridToken: 0,
|
||||
detailToken: 0,
|
||||
activeSourceId: null,
|
||||
currentPostId: null,
|
||||
currentDetail: null
|
||||
@@ -127,6 +129,7 @@ const Browse = (() => {
|
||||
}
|
||||
|
||||
async function loadGrid() {
|
||||
const token = ++state.gridToken;
|
||||
state.aggToken++;
|
||||
showGrid();
|
||||
statusBar.textContent = '加载中...';
|
||||
@@ -136,14 +139,19 @@ const Browse = (() => {
|
||||
|
||||
if (isAgg()) return loadAggregate();
|
||||
|
||||
const res = state.mode === 'search'
|
||||
? await window.api.sources.search(state.sourceId, state.keyword, state.page)
|
||||
: await window.api.sources.browse(state.sourceId, state.page);
|
||||
const sourceId = state.sourceId;
|
||||
const mode = state.mode;
|
||||
const keyword = state.keyword;
|
||||
const page = state.page;
|
||||
const res = mode === 'search'
|
||||
? await window.api.sources.search(sourceId, keyword, page)
|
||||
: await window.api.sources.browse(sourceId, page);
|
||||
if (token !== state.gridToken) return;
|
||||
|
||||
grid.className = 'grid';
|
||||
|
||||
if (!res.ok) {
|
||||
const isAuth = state.sourceId === 'zlib' && /登录|登录|AUTH/i.test(res.error || '');
|
||||
const isAuth = sourceId === 'zlib' && /登录|AUTH/i.test(res.error || '');
|
||||
statusBar.innerHTML = `加载失败:${escapeHtml(res.error)} <button class="retry-btn" id="gridRetry">重试</button>`;
|
||||
if (isAuth) {
|
||||
grid.innerHTML = '<div class="empty">Z-Library 需要登录,请到"设置"页配置账号</div>';
|
||||
@@ -158,23 +166,23 @@ const Browse = (() => {
|
||||
state.maxPage = maxPage || 1;
|
||||
|
||||
if (!items.length) {
|
||||
grid.innerHTML = '<div class="empty">未找到相关结果</div>';
|
||||
statusBar.textContent = state.mode === 'search' ? `搜索:${escapeHtml(state.keyword)}` : '';
|
||||
grid.innerHTML = `<div class="empty">${escapeHtml(res.data.note || '未找到相关结果')}</div>`;
|
||||
statusBar.textContent = mode === 'search' ? `搜索:${keyword}` : '';
|
||||
return;
|
||||
}
|
||||
|
||||
statusBar.textContent = state.mode === 'search' ? `搜索:${escapeHtml(state.keyword)}(第 ${state.page} 页)` : '';
|
||||
statusBar.textContent = mode === 'search' ? `搜索:${keyword}(第 ${page} 页)` : '';
|
||||
|
||||
grid.innerHTML = items.map(cardHtml).join('');
|
||||
bindCards(grid, state.sourceId);
|
||||
bindCards(grid, sourceId);
|
||||
|
||||
$('pageInfo').textContent = `第 ${state.page} / ${state.maxPage} 页`;
|
||||
$('prevBtn').disabled = state.page <= 1;
|
||||
$('nextBtn').disabled = state.page >= state.maxPage;
|
||||
$('pageInfo').textContent = `第 ${page} / ${state.maxPage} 页`;
|
||||
$('prevBtn').disabled = page <= 1;
|
||||
$('nextBtn').disabled = page >= state.maxPage;
|
||||
const jump = $('jumpInput');
|
||||
jump.max = state.maxPage;
|
||||
jump.value = '';
|
||||
jump.placeholder = state.page;
|
||||
jump.placeholder = page;
|
||||
pager.classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -280,21 +288,25 @@ const Browse = (() => {
|
||||
}
|
||||
|
||||
function showGrid() {
|
||||
state.detailToken++;
|
||||
detailView.classList.add('hidden');
|
||||
gridView.classList.remove('hidden');
|
||||
if (state.scrollY) mainEl.scrollTop = state.scrollY;
|
||||
}
|
||||
|
||||
async function openDetail(postId, sourceId) {
|
||||
const token = ++state.detailToken;
|
||||
state.scrollY = mainEl.scrollTop;
|
||||
state.currentPostId = postId;
|
||||
state.activeSourceId = sourceId || state.sourceId;
|
||||
const activeSourceId = sourceId || state.sourceId;
|
||||
state.activeSourceId = activeSourceId;
|
||||
gridView.classList.add('hidden');
|
||||
detailView.classList.remove('hidden');
|
||||
mainEl.scrollTop = 0;
|
||||
detailContent.innerHTML = '<div class="dl-loading">加载中...</div>';
|
||||
|
||||
const res = await window.api.sources.detail(state.activeSourceId, postId);
|
||||
const res = await window.api.sources.detail(activeSourceId, postId);
|
||||
if (token !== state.detailToken) return;
|
||||
if (!res.ok) {
|
||||
detailContent.innerHTML = `<div class="dl-error">加载失败:${escapeHtml(res.error)}<button class="retry-btn" id="detailRetry">重试</button></div>`;
|
||||
$('detailRetry').onclick = () => openDetail(postId, state.activeSourceId);
|
||||
@@ -302,7 +314,7 @@ const Browse = (() => {
|
||||
}
|
||||
state.currentDetail = res.data;
|
||||
renderDetail(res.data);
|
||||
loadDownload(postId);
|
||||
loadDownload(postId, activeSourceId, token);
|
||||
refreshAddButton();
|
||||
}
|
||||
|
||||
@@ -358,16 +370,7 @@ const Browse = (() => {
|
||||
async function addToLibrary() {
|
||||
const d = state.currentDetail;
|
||||
if (!d) return;
|
||||
const res = await window.api.library.add({
|
||||
title: d.title,
|
||||
authors: d.authors || [],
|
||||
cover: d.cover,
|
||||
date: d.date || '',
|
||||
brief: d.brief || '',
|
||||
url: d.url || '',
|
||||
sourceId: state.activeSourceId,
|
||||
sourcePostId: state.currentPostId
|
||||
});
|
||||
const res = await window.api.library.add(entryMeta());
|
||||
if (res.ok) {
|
||||
const btn = $('addLibBtn');
|
||||
btn.textContent = '已加入书库 ✓';
|
||||
@@ -377,15 +380,15 @@ const Browse = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDownload(postId) {
|
||||
async function loadDownload(postId, sourceId = state.activeSourceId, token = state.detailToken) {
|
||||
const box = $('downloadBox');
|
||||
if (!box) return;
|
||||
box.innerHTML = '<div class="dl-loading">下载信息获取中...</div>';
|
||||
const res = await window.api.sources.download(state.activeSourceId, postId);
|
||||
if (!box.isConnected) return;
|
||||
const res = await window.api.sources.download(sourceId, postId);
|
||||
if (!box.isConnected || token !== state.detailToken) return;
|
||||
if (!res.ok) {
|
||||
box.innerHTML = `<div class="dl-error">获取失败:${escapeHtml(res.error)}<button class="retry-btn" id="dlRetry">重试</button></div>`;
|
||||
$('dlRetry').onclick = () => loadDownload(postId);
|
||||
$('dlRetry').onclick = () => loadDownload(postId, sourceId, token);
|
||||
return;
|
||||
}
|
||||
const d = res.data;
|
||||
@@ -409,24 +412,62 @@ const Browse = (() => {
|
||||
box.querySelectorAll('[data-open]').forEach((a) => { a.onclick = (e) => { e.preventDefault(); window.api.openExternal(a.dataset.open); }; });
|
||||
}
|
||||
|
||||
// 当前条目的元数据,供下载时自动建库用
|
||||
function entryMeta() {
|
||||
const d = state.currentDetail || {};
|
||||
return {
|
||||
title: d.title || '未命名',
|
||||
authors: d.authors || [],
|
||||
cover: d.cover || '',
|
||||
date: d.date || '',
|
||||
brief: d.brief || '',
|
||||
url: d.url || '',
|
||||
sourceId: state.activeSourceId,
|
||||
sourcePostId: state.currentPostId
|
||||
};
|
||||
}
|
||||
|
||||
async function downloadFile(btn, url) {
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '下载中...';
|
||||
|
||||
const lib = await window.api.library.findBySource(state.activeSourceId, state.currentPostId);
|
||||
const entryId = (lib.ok && lib.data) ? lib.data.id : undefined;
|
||||
const res = await window.api.downloadFile(url, btn.dataset.name || '', entryId);
|
||||
// 传 meta:条目还不在书库时由主进程自动建,避免下载完却找不到文件
|
||||
const res = await window.api.downloadFile(url, btn.dataset.name || '', entryId, undefined, entryMeta());
|
||||
|
||||
if (res.ok && res.data && res.data.canceled) {
|
||||
btn.textContent = orig; btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (res.ok) {
|
||||
btn.textContent = '已保存 ✓';
|
||||
btn.classList.add('copied');
|
||||
setTimeout(() => { btn.textContent = orig; btn.classList.remove('copied'); btn.disabled = false; }, 2000);
|
||||
} else {
|
||||
if (!res.ok) {
|
||||
btn.textContent = '失败';
|
||||
btn.title = res.error || '';
|
||||
setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 下载即入库,刷新"加入书库"按钮并就地提供打开入口
|
||||
if (window.Library) window.Library.markDirty();
|
||||
refreshAddButton();
|
||||
|
||||
const saved = res.data.path;
|
||||
btn.textContent = '打开';
|
||||
btn.disabled = false;
|
||||
btn.classList.add('copied');
|
||||
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';
|
||||
reveal.textContent = '定位';
|
||||
reveal.onclick = () => window.api.showItem(saved);
|
||||
row.appendChild(reveal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-7
@@ -13,9 +13,21 @@ const Library = (() => {
|
||||
grid = $('libGrid');
|
||||
statusEl = $('libStatus');
|
||||
$('addLocalBtn').onclick = addLocal;
|
||||
$('rescanBtn').onclick = rescan;
|
||||
window.api.library.onChanged(() => { dirty = true; refresh(true); });
|
||||
}
|
||||
|
||||
async function rescan() {
|
||||
const btn = $('rescanBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '扫描中...';
|
||||
const r = await window.api.library.scan();
|
||||
btn.textContent = r.ok && r.data && r.data.added ? `新增 ${r.data.added} 本 ✓` : '已是最新 ✓';
|
||||
dirty = true;
|
||||
await refresh(true);
|
||||
setTimeout(() => { btn.textContent = '重新扫描'; btn.disabled = false; }, 2000);
|
||||
}
|
||||
|
||||
function getSortMode() { return sortMode; }
|
||||
function setSortMode(m) {
|
||||
sortMode = m;
|
||||
@@ -30,16 +42,20 @@ const Library = (() => {
|
||||
dirty = false;
|
||||
if (!res.ok) { statusEl.textContent = '加载失败:' + res.error; return; }
|
||||
const items = res.data.slice().sort(SORTERS[sortMode] || SORTERS.added);
|
||||
statusEl.textContent = `共 ${items.length} 条`;
|
||||
const missingCount = items.filter((it) => it.missing).length;
|
||||
statusEl.textContent = `共 ${items.length} 条` + (missingCount ? `,${missingCount} 条文件缺失` : '');
|
||||
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
|
||||
// exists 由主进程按实际磁盘状态给出:文件被手动删掉时要如实反映
|
||||
const openable = (it.files || []).some((f) => f.exists);
|
||||
const badge = openable
|
||||
? '<span class="card-badge">已下载</span>'
|
||||
: '<span class="card-badge miss">未下载</span>';
|
||||
: ((it.files || []).length
|
||||
? '<span class="card-badge miss">文件缺失</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>
|
||||
@@ -47,7 +63,8 @@ const Library = (() => {
|
||||
${(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>
|
||||
<button class="open-btn" data-act="open" ${openable ? '' : 'disabled'}>打开</button>
|
||||
${openable ? '<button data-act="reveal">定位</button>' : ''}
|
||||
${it.url ? '<button data-act="page">页面</button>' : ''}
|
||||
<button data-act="remove">移除</button>
|
||||
</div>
|
||||
@@ -67,8 +84,13 @@ const Library = (() => {
|
||||
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);
|
||||
const f = (it.files || []).find((x) => x.exists) || (it.files || [])[0];
|
||||
if (!f) return;
|
||||
const r = await window.api.openPath(f.path);
|
||||
if (!r.ok) await confirmModal('打开失败', r.error || '无法打开该文件');
|
||||
} else if (act === 'reveal') {
|
||||
const f = (it.files || []).find((x) => x.exists);
|
||||
if (f) window.api.showItem(f.path);
|
||||
} else if (act === 'page') {
|
||||
if (it.url) window.api.openExternal(it.url);
|
||||
} else if (act === 'remove') {
|
||||
|
||||
Reference in New Issue
Block a user