feat: 分类显示书籍数量并发布 v2.1.2
构建与发布 / 单测与集成测试 (push) Waiting to run
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
构建与发布 / 发布 GitHub Release (push) Blocked by required conditions
构建与发布 / 单测与集成测试 (push) Waiting to run
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
构建与发布 / 发布 GitHub Release (push) Blocked by required conditions
This commit is contained in:
@@ -74,7 +74,7 @@ DRM 保护的 MOBI/AZW/AZW3、KFX、Topaz 以及损坏或不兼容的文件不
|
|||||||
2. 双击目录中的 `PeopleLib.exe`。
|
2. 双击目录中的 `PeopleLib.exe`。
|
||||||
3. 保留整个程序目录,不要只移动 exe。用户数据默认保存在程序同级的 `data/`。
|
3. 保留整个程序目录,不要只移动 exe。用户数据默认保存在程序同级的 `data/`。
|
||||||
|
|
||||||
当前版本为 **2.1.1**。可在「设置」中手动检查更新,也可启用启动时自动检查。检测到新版本后,应用会打开对应的 GitHub Release 下载页,更新前请退出旧版本并覆盖程序文件,`data/` 目录无需替换。
|
当前版本为 **2.1.2**。可在「设置」中手动检查更新,也可启用启动时自动检查。检测到新版本后,应用会打开对应的 GitHub Release 下载页,更新前请退出旧版本并覆盖程序文件,`data/` 目录无需替换。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "peoplelib",
|
"name": "peoplelib",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "peoplelib",
|
"name": "peoplelib",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"foliate-js": "1.0.1",
|
"foliate-js": "1.0.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "peoplelib",
|
"name": "peoplelib",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"description": "多源开放文献、电子书与本地书库客户端",
|
"description": "多源开放文献、电子书与本地书库客户端",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"author": "peoplelib",
|
"author": "peoplelib",
|
||||||
|
|||||||
@@ -569,6 +569,22 @@ app.whenReady().then(async () => {
|
|||||||
);
|
);
|
||||||
readerWindow.destroy();
|
readerWindow.destroy();
|
||||||
await wait(200);
|
await wait(200);
|
||||||
|
check(
|
||||||
|
'分类侧栏显示全部、未分类和各书架的真实书籍数量',
|
||||||
|
await js(`(() => {
|
||||||
|
const all = document.querySelector('#libraryTab .library-filter[data-shelf=""]');
|
||||||
|
const uncategorized = document.querySelector(
|
||||||
|
'#libraryTab .library-filter[data-shelf="__uncategorized__"]'
|
||||||
|
);
|
||||||
|
const shelves = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'));
|
||||||
|
const research = shelves.find((button) => button.textContent.includes('研究书架'));
|
||||||
|
const review = shelves.find((button) => button.textContent.includes('待复核书架'));
|
||||||
|
return all.querySelector('.library-filter-count').textContent === '3'
|
||||||
|
&& uncategorized.querySelector('.library-filter-count').textContent === '1'
|
||||||
|
&& research.querySelector('.library-filter-count').textContent === '1'
|
||||||
|
&& review.querySelector('.library-filter-count').textContent === '1';
|
||||||
|
})()`)
|
||||||
|
);
|
||||||
check(
|
check(
|
||||||
'标签侧栏显示真实聚合计数',
|
'标签侧栏显示真实聚合计数',
|
||||||
await js(`(() => {
|
await js(`(() => {
|
||||||
@@ -632,7 +648,7 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
await js(`(() => {
|
await js(`(() => {
|
||||||
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||||||
.find((candidate) => candidate.textContent.trim() === '研究书架');
|
.find((candidate) => candidate.firstElementChild.textContent.trim() === '研究书架');
|
||||||
button.click();
|
button.click();
|
||||||
})()`);
|
})()`);
|
||||||
await pollJs(
|
await pollJs(
|
||||||
@@ -681,7 +697,7 @@ app.whenReady().then(async () => {
|
|||||||
!!uiShelf
|
!!uiShelf
|
||||||
&& await js(`(() => {
|
&& await js(`(() => {
|
||||||
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||||||
.find((candidate) => candidate.textContent.trim() === '界面书架');
|
.find((candidate) => candidate.firstElementChild.textContent.trim() === '界面书架');
|
||||||
return !!button && button.classList.contains('active')
|
return !!button && button.classList.contains('active')
|
||||||
&& document.getElementById('libStatus').textContent === '显示 0 条,共 3 条';
|
&& document.getElementById('libStatus').textContent === '显示 0 条,共 3 条';
|
||||||
})()`)
|
})()`)
|
||||||
@@ -783,7 +799,7 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
await js(`(() => {
|
await js(`(() => {
|
||||||
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||||||
.find((candidate) => candidate.textContent.trim() === '界面书架已重命名');
|
.find((candidate) => candidate.firstElementChild.textContent.trim() === '界面书架已重命名');
|
||||||
button.click();
|
button.click();
|
||||||
})()`);
|
})()`);
|
||||||
await pollJs(
|
await pollJs(
|
||||||
@@ -859,7 +875,7 @@ app.whenReady().then(async () => {
|
|||||||
// 否则批量操作会误伤用户看不到的书
|
// 否则批量操作会误伤用户看不到的书
|
||||||
await js(`(() => {
|
await js(`(() => {
|
||||||
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||||||
.find((candidate) => candidate.textContent.trim() === '研究书架');
|
.find((candidate) => candidate.firstElementChild.textContent.trim() === '研究书架');
|
||||||
button.click();
|
button.click();
|
||||||
})()`);
|
})()`);
|
||||||
await pollJs(
|
await pollJs(
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ test('下载校验协议,拒绝 file:// 等非 http(s)', () => {
|
|||||||
test('下载请求使用可识别且带项目地址的 User-Agent', () => {
|
test('下载请求使用可识别且带项目地址的 User-Agent', () => {
|
||||||
assert.match(mainSrc, /UA:\s*DL_UA[\s\S]+require\('\.\/src\/sources\/http'\)/);
|
assert.match(mainSrc, /UA:\s*DL_UA[\s\S]+require\('\.\/src\/sources\/http'\)/);
|
||||||
const httpSrc = fs.readFileSync(path.join(__dirname, '..', 'sources', 'http.js'), 'utf8');
|
const httpSrc = fs.readFileSync(path.join(__dirname, '..', 'sources', 'http.js'), 'utf8');
|
||||||
assert.match(httpSrc, /PeopleLib\/2\.1\.1 \(\+https:\/\/github\.com\/lofyer\/peoplelib\)/);
|
assert.match(httpSrc, /PeopleLib\/2\.1\.2 \(\+https:\/\/github\.com\/lofyer\/peoplelib\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('移除书籍默认保留阅读资料,仅显式勾选时清理', () => {
|
test('移除书籍默认保留阅读资料,仅显式勾选时清理', () => {
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ test('wikisource: 搜索使用整数偏移并携带可识别 User-Agent', async
|
|||||||
assert.strictEqual(result.items[0].subtitle, '中文');
|
assert.strictEqual(result.items[0].subtitle, '中文');
|
||||||
assert.strictEqual(result.maxPage, 500, 'MediaWiki 搜索最多允许偏移到 10000 条');
|
assert.strictEqual(result.maxPage, 500, 'MediaWiki 搜索最多允许偏移到 10000 条');
|
||||||
assert.ok(request.url.includes('sroffset=20'), request.url);
|
assert.ok(request.url.includes('sroffset=20'), request.url);
|
||||||
assert.match(request.options.headers['User-Agent'], /PeopleLib\/2\.1\.1/);
|
assert.match(request.options.headers['User-Agent'], /PeopleLib\/2\.1\.2/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('wikisource: 浏览按 continuation 令牌翻页', async () => {
|
test('wikisource: 浏览按 continuation 令牌翻页', async () => {
|
||||||
|
|||||||
@@ -613,6 +613,25 @@ test('书架 CRUD 强制唯一非空名称并返回深拷贝', () => {
|
|||||||
assert.throws(() => store.updateShelf(other.id, { name: ' 文学 ' }), /已存在/);
|
assert.throws(() => store.updateShelf(other.id, { name: ' 文学 ' }), /已存在/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('书架列表返回每个分类当前包含的书籍数量', () => {
|
||||||
|
freshRoot('shelf-counts');
|
||||||
|
const research = store.addShelf('研究');
|
||||||
|
const archive = store.addShelf('归档');
|
||||||
|
const first = store.add({ title: 'A', shelfId: research.id });
|
||||||
|
store.add({ title: 'B', shelfId: research.id });
|
||||||
|
store.add({ title: '未分类' });
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
store.listShelves().map((shelf) => [shelf.name, shelf.count]),
|
||||||
|
[['研究', 2], ['归档', 0]]
|
||||||
|
);
|
||||||
|
store.update(first.id, { shelfId: archive.id });
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
store.listShelves().map((shelf) => [shelf.name, shelf.count]),
|
||||||
|
[['研究', 1], ['归档', 1]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('删除书架只清空条目 shelfId 且组织变更触发通知', () => {
|
test('删除书架只清空条目 shelfId 且组织变更触发通知', () => {
|
||||||
freshRoot('shelf-remove');
|
freshRoot('shelf-remove');
|
||||||
let changes = 0;
|
let changes = 0;
|
||||||
|
|||||||
@@ -165,6 +165,22 @@ test('书库页提供可管理标签目录和整理多选下拉', () => {
|
|||||||
assert.doesNotMatch(library, /id="libraryBookTags" type="text"/);
|
assert.doesNotMatch(library, /id="libraryBookTags" type="text"/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('全部书籍、未分类和每个书架都显示实时书籍数量', () => {
|
||||||
|
const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8');
|
||||||
|
const library = fs.readFileSync(libFile, 'utf8');
|
||||||
|
assert.match(
|
||||||
|
html,
|
||||||
|
/data-shelf="">[\s\S]*全部书籍[\s\S]*class="library-filter-count">0<\/span>/
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
html,
|
||||||
|
/data-shelf="__uncategorized__">[\s\S]*未分类[\s\S]*class="library-filter-count">0<\/span>/
|
||||||
|
);
|
||||||
|
assert.match(library, /items\.filter\(\(item\) => !item\.shelfId\)\.length/);
|
||||||
|
assert.match(library, /count\.textContent = String\(shelf\.count \|\| 0\)/);
|
||||||
|
assert.match(library, /filter\.append\(name, count\)/);
|
||||||
|
});
|
||||||
|
|
||||||
test('下载区接入全局任务中心且完成按钮使用高对比绿色底色', () => {
|
test('下载区接入全局任务中心且完成按钮使用高对比绿色底色', () => {
|
||||||
const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8');
|
const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8');
|
||||||
const browse = fs.readFileSync(path.join(__dirname, '..', 'ui', 'views', 'browse.js'), 'utf8');
|
const browse = fs.readFileSync(path.join(__dirname, '..', 'ui', 'views', 'browse.js'), 'utf8');
|
||||||
|
|||||||
@@ -494,7 +494,12 @@ function findBySource(sourceId, sourcePostId) {
|
|||||||
|
|
||||||
function listShelves() {
|
function listShelves() {
|
||||||
load();
|
load();
|
||||||
return shelves.map((shelf) => ({ ...shelf }));
|
const counts = new Map();
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item.shelfId) continue;
|
||||||
|
counts.set(item.shelfId, (counts.get(item.shelfId) || 0) + 1);
|
||||||
|
}
|
||||||
|
return shelves.map((shelf) => ({ ...shelf, count: counts.get(shelf.id) || 0 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function listTags() {
|
function listTags() {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 PeopleLib/2.1.1 (+https://github.com/lofyer/peoplelib)';
|
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 PeopleLib/2.1.2 (+https://github.com/lofyer/peoplelib)';
|
||||||
const { fetch: undiciFetch, ProxyAgent } = require('undici');
|
const { fetch: undiciFetch, ProxyAgent } = require('undici');
|
||||||
|
|
||||||
// 代理配置:默认直连(空字符串)。一旦设置,所有 HTTP 请求统一走该代理。
|
// 代理配置:默认直连(空字符串)。一旦设置,所有 HTTP 请求统一走该代理。
|
||||||
|
|||||||
+6
-2
@@ -69,8 +69,12 @@
|
|||||||
<button id="addShelfBtn" class="notes-icon-btn" title="新建书架" aria-label="新建书架">+</button>
|
<button id="addShelfBtn" class="notes-icon-btn" title="新建书架" aria-label="新建书架">+</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="library-filter-list">
|
<div class="library-filter-list">
|
||||||
<button class="library-filter active" data-shelf="">全部书籍</button>
|
<button class="library-filter active" data-shelf="">
|
||||||
<button class="library-filter" data-shelf="__uncategorized__">未分类</button>
|
<span>全部书籍</span><span class="library-filter-count">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="library-filter" data-shelf="__uncategorized__">
|
||||||
|
<span>未分类</span><span class="library-filter-count">0</span>
|
||||||
|
</button>
|
||||||
<div id="libraryShelfList" class="library-filter-list"></div>
|
<div id="libraryShelfList" class="library-filter-list"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="library-sidebar-section">
|
<div class="library-sidebar-section">
|
||||||
|
|||||||
+17
-5
@@ -372,10 +372,10 @@ const Library = (() => {
|
|||||||
if (selectedShelf && selectedShelf !== '__uncategorized__'
|
if (selectedShelf && selectedShelf !== '__uncategorized__'
|
||||||
&& !shelves.some((shelf) => shelf.id === selectedShelf)) selectedShelf = '';
|
&& !shelves.some((shelf) => shelf.id === selectedShelf)) selectedShelf = '';
|
||||||
if (selectedTag && !libraryTags.some((tag) => tag.name === selectedTag)) selectedTag = '';
|
if (selectedTag && !libraryTags.some((tag) => tag.name === selectedTag)) selectedTag = '';
|
||||||
renderOrganizationSidebar();
|
const allItems = res.data.slice();
|
||||||
|
renderOrganizationSidebar(allItems);
|
||||||
const noteCounts = noteCountsOf(noteCountResult);
|
const noteCounts = noteCountsOf(noteCountResult);
|
||||||
const annotationCounts = noteCountsOf(annotationCountResult);
|
const annotationCounts = noteCountsOf(annotationCountResult);
|
||||||
const allItems = res.data.slice();
|
|
||||||
const scopedItems = allItems.filter((item) => {
|
const scopedItems = allItems.filter((item) => {
|
||||||
if (selectedShelf === '__uncategorized__' && item.shelfId) return false;
|
if (selectedShelf === '__uncategorized__' && item.shelfId) return false;
|
||||||
if (selectedShelf && selectedShelf !== '__uncategorized__' && item.shelfId !== selectedShelf) return false;
|
if (selectedShelf && selectedShelf !== '__uncategorized__' && item.shelfId !== selectedShelf) return false;
|
||||||
@@ -424,12 +424,19 @@ const Library = (() => {
|
|||||||
refresh(true);
|
refresh(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderOrganizationSidebar() {
|
function renderOrganizationSidebar(items) {
|
||||||
document.querySelectorAll('#libraryTab .library-filter[data-shelf]').forEach((button) => {
|
document.querySelectorAll('#libraryTab .library-filter[data-shelf]').forEach((button) => {
|
||||||
|
const shelfId = button.dataset.shelf || '';
|
||||||
button.classList.toggle(
|
button.classList.toggle(
|
||||||
'active',
|
'active',
|
||||||
!selectedTag && (button.dataset.shelf || '') === selectedShelf
|
!selectedTag && shelfId === selectedShelf
|
||||||
);
|
);
|
||||||
|
const count = button.querySelector('.library-filter-count');
|
||||||
|
if (count) {
|
||||||
|
count.textContent = String(shelfId === '__uncategorized__'
|
||||||
|
? items.filter((item) => !item.shelfId).length
|
||||||
|
: items.length);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const shelfList = $('libraryShelfList');
|
const shelfList = $('libraryShelfList');
|
||||||
shelfList.textContent = '';
|
shelfList.textContent = '';
|
||||||
@@ -440,8 +447,13 @@ const Library = (() => {
|
|||||||
filter.type = 'button';
|
filter.type = 'button';
|
||||||
filter.className = 'library-filter';
|
filter.className = 'library-filter';
|
||||||
filter.classList.toggle('active', !selectedTag && selectedShelf === shelf.id);
|
filter.classList.toggle('active', !selectedTag && selectedShelf === shelf.id);
|
||||||
filter.textContent = shelf.name;
|
|
||||||
filter.title = shelf.name;
|
filter.title = shelf.name;
|
||||||
|
const name = document.createElement('span');
|
||||||
|
name.textContent = shelf.name;
|
||||||
|
const count = document.createElement('span');
|
||||||
|
count.className = 'library-filter-count';
|
||||||
|
count.textContent = String(shelf.count || 0);
|
||||||
|
filter.append(name, count);
|
||||||
filter.onclick = () => selectShelf(shelf.id);
|
filter.onclick = () => selectShelf(shelf.id);
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
|
|||||||
Reference in New Issue
Block a user