diff --git a/package-lock.json b/package-lock.json index d980ead..dc5e2c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "peoplelib", - "version": "2.1.3", + "version": "2.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "peoplelib", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "dependencies": { "foliate-js": "1.0.1", diff --git a/package.json b/package.json index a488a5d..3397a38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "peoplelib", - "version": "2.1.3", + "version": "2.1.4", "description": "多源开放文献、电子书与本地书库客户端", "main": "main.js", "author": "peoplelib", diff --git a/src/_test/sources.test.js b/src/_test/sources.test.js index 08733a8..e8905b3 100644 --- a/src/_test/sources.test.js +++ b/src/_test/sources.test.js @@ -16,6 +16,11 @@ test('注册表:每个源都实现完整接口', () => { } assert.ok(s.name, `${s.id} 缺 name`); } + assert.strictEqual(list.find((source) => source.id === 'zlib').downloadOnDemand, true); + assert.ok( + list.filter((source) => source.id !== 'zlib').every((source) => source.downloadOnDemand === false), + '只有会消耗下载额度的 Z-Library 应按点击解析' + ); }); test('注册表:开放教材与中英文维基文库已启用', () => { @@ -527,7 +532,9 @@ test('zlib: postId 缺 hash 时详情仍可用', async () => { }); const d = await zlib.detail('123/'); assert.strictEqual(d.title, 'B'); + assert.strictEqual(urls.length, 1, '打开详情不应顺带请求下载接口'); assert.ok(urls[0].includes('/eapi/book/123'), urls[0]); + assert.ok(!urls[0].includes('/file'), '打开详情消耗了下载额度: ' + urls[0]); assert.ok(!urls[0].includes('/eapi/book/123/?'), '缺 hash 时不该留下尾斜杠: ' + urls[0]); } finally { auth.getSession = origSession; diff --git a/src/_test/ui.test.js b/src/_test/ui.test.js index f2f4a54..5c4ddf5 100644 --- a/src/_test/ui.test.js +++ b/src/_test/ui.test.js @@ -218,6 +218,28 @@ test('下载区接入全局任务中心且完成按钮使用高对比绿色底 assert.match(rule[1], /color:\s*#07130b/); }); +test('Z-Library 进入详情不消耗下载额度,点击下载后才解析并开始任务', () => { + const browse = fs.readFileSync(path.join(__dirname, '..', 'ui', 'views', 'browse.js'), 'utf8'); + const openDetail = browse.slice( + browse.indexOf('async function openDetail'), + browse.indexOf('function sourceNameOf') + ); + assert.match(openDetail, /if \(sourceDownloadsOnDemand\(activeSourceId\)\)/); + assert.match(openDetail, /renderOnDemandDownload\(postId, activeSourceId, token\)/); + assert.match(openDetail, /else\s*\{\s*loadDownload\(postId, activeSourceId, token\)/); + + const onDemand = browse.slice( + browse.indexOf('function renderOnDemandDownload'), + browse.indexOf('// 当前条目的元数据') + ); + const clickAt = onDemand.indexOf('btn.onclick = async'); + const resolveAt = onDemand.indexOf('window.api.sources.download', clickAt); + const startAt = onDemand.indexOf('download.click()', resolveAt); + assert.ok(clickAt >= 0 && resolveAt > clickAt, '下载地址在用户点击前被获取'); + assert.ok(startAt > resolveAt, '解析下载地址后没有立即开始真实下载'); + assert.match(onDemand, /每日免费下载额度有限,仅在点击后获取下载地址/); +}); + test('主窗口在设置旁提供持久化明暗主题切换', () => { const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8'); const app = fs.readFileSync(path.join(__dirname, '..', 'ui', 'app.js'), 'utf8'); diff --git a/src/sources/index.js b/src/sources/index.js index e1aa8b0..12f19a7 100644 --- a/src/sources/index.js +++ b/src/sources/index.js @@ -36,7 +36,12 @@ const sources = [ const byId = new Map(sources.map((s) => [s.id, s])); function listSources() { - return sources.map((s) => ({ id: s.id, name: s.name, supportsSearch: s.supportsSearch !== false })); + return sources.map((s) => ({ + id: s.id, + name: s.name, + supportsSearch: s.supportsSearch !== false, + downloadOnDemand: s.downloadOnDemand === true + })); } function getSource(id) { diff --git a/src/sources/zlib.js b/src/sources/zlib.js index a508c2a..8c6e60f 100644 --- a/src/sources/zlib.js +++ b/src/sources/zlib.js @@ -270,6 +270,7 @@ module.exports = { id: 'zlib', name: 'Z-Library', supportsSearch: true, + downloadOnDemand: true, // 无关键词时展示热门书目 async list(page) { diff --git a/src/ui/views/browse.js b/src/ui/views/browse.js index bb57871..cba19ae 100644 --- a/src/ui/views/browse.js +++ b/src/ui/views/browse.js @@ -314,7 +314,11 @@ const Browse = (() => { } state.currentDetail = res.data; renderDetail(res.data); - loadDownload(postId, activeSourceId, token); + if (sourceDownloadsOnDemand(activeSourceId)) { + renderOnDemandDownload(postId, activeSourceId, token); + } else { + loadDownload(postId, activeSourceId, token); + } refreshAddButton(); } @@ -323,6 +327,11 @@ const Browse = (() => { return s ? s.name : id; } + function sourceDownloadsOnDemand(id) { + const source = state.sourceList.find((item) => item.id === id); + return !!(source && source.downloadOnDemand); + } + function renderDetail(d) { const tagsHtml = (d.tags || []).map((t) => { const idx = t.indexOf(':'); @@ -391,7 +400,10 @@ const Browse = (() => { $('dlRetry').onclick = () => loadDownload(postId, sourceId, token); return; } - const d = res.data; + renderDownloadResult(box, res.data); + } + + function renderDownloadResult(box, d) { let html = ''; const files = d.files || []; if (files.length) { @@ -412,6 +424,34 @@ const Browse = (() => { box.querySelectorAll('[data-open]').forEach((a) => { a.onclick = (e) => { e.preventDefault(); window.api.openExternal(a.dataset.open); }; }); } + function renderOnDemandDownload(postId, sourceId, token) { + const box = $('downloadBox'); + if (!box) return; + box.innerHTML = `