cyreader 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/server/dist/config/paths.js +4 -1
- package/bundle/server/dist/index.js +1 -1
- package/bundle/server/dist/library/scan.js +2 -3
- package/bundle/server/dist/library/scan.test.js +8 -8
- package/bundle/server/dist/library/service.js +11 -3
- package/bundle/server/dist/library/service.test.js +44 -10
- package/bundle/server/dist/library/storage.js +76 -0
- package/bundle/server/dist/library/storage.test.js +74 -0
- package/bundle/server/dist/reading/progress/service.test.js +1 -1
- package/bundle/server/dist/reading/recent/service.test.js +1 -1
- package/bundle/server/dist/routes/config.test.js +1 -1
- package/bundle/server/dist/routes/library.test.js +1 -1
- package/bundle/server/dist/routes/reading.test.js +1 -1
- package/bundle/web/dist/assets/{_shell-Bm2pxAQP.js → _shell-CQkt24cc.js} +1 -1
- package/bundle/web/dist/assets/_shell-N_UfyRoX.js +1 -0
- package/bundle/web/dist/assets/{comic._id-Djtr8diE.js → comic._id-CVm6n_JY.js} +1 -1
- package/bundle/web/dist/assets/{index-BHhqsr5H.js → index-BS403FO9.js} +2 -2
- package/bundle/web/dist/assets/{novel._id-C62VYvcl.js → novel._id-Bgt8GkIZ.js} +1 -1
- package/bundle/web/dist/assets/{save-reading-progress-D231ROaQ.js → save-reading-progress-DntE0i4u.js} +1 -1
- package/bundle/web/dist/assets/scroll-area-BUlmpLx6.js +1 -0
- package/bundle/web/dist/assets/settings-DopKc4yx.js +1 -0
- package/bundle/web/dist/assets/{useRouterState-4dM5FKiy.js → useRouterState-DDtlowFh.js} +1 -1
- package/bundle/web/dist/index.html +1 -1
- package/package.json +1 -1
- package/bundle/web/dist/assets/_shell-Cij8gMSv.js +0 -1
- package/bundle/web/dist/assets/scroll-area-CyEO8R_G.js +0 -1
- package/bundle/web/dist/assets/settings-BRi0XyRg.js +0 -1
|
@@ -16,6 +16,9 @@ export function getRecentFilePath() {
|
|
|
16
16
|
export function getProgressFilePath() {
|
|
17
17
|
return path.join(getConfigDir(), 'progress.json');
|
|
18
18
|
}
|
|
19
|
-
export function
|
|
19
|
+
export function getLibraryFilePath() {
|
|
20
|
+
return path.join(getConfigDir(), 'library.json');
|
|
21
|
+
}
|
|
22
|
+
export function getLegacyLibraryCacheFilePath() {
|
|
20
23
|
return path.join(getConfigDir(), 'library-cache.json');
|
|
21
24
|
}
|
|
@@ -7,7 +7,7 @@ import { ProgressService } from './reading/progress/service.js';
|
|
|
7
7
|
import { RecentService } from './reading/recent/service.js';
|
|
8
8
|
async function main() {
|
|
9
9
|
const configService = await ConfigService.create();
|
|
10
|
-
const libraryService =
|
|
10
|
+
const libraryService = await LibraryService.create(() => configService.get());
|
|
11
11
|
const recentService = await RecentService.create(libraryService);
|
|
12
12
|
const progressService = await ProgressService.create(libraryService);
|
|
13
13
|
const app = createApp(configService, libraryService, recentService, progressService);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import sharp from 'sharp';
|
|
4
|
-
import {
|
|
4
|
+
import { readLibrary } from './storage.js';
|
|
5
5
|
import { generateComicCoverIfMissing, resolveComicCoverFilename } from './cover.js';
|
|
6
6
|
import { isComicCoverFile, isHiddenName, isImageFile, isTxtFile, sortByNaturalName, } from './files.js';
|
|
7
7
|
import { makeLocalItemId } from './id.js';
|
|
@@ -165,12 +165,11 @@ async function scanComicsInDirectory(directoryId, root, cacheByPath) {
|
|
|
165
165
|
return comics.filter((comic) => comic !== null);
|
|
166
166
|
}
|
|
167
167
|
export async function scanLibrary(config) {
|
|
168
|
-
const cache = await
|
|
168
|
+
const cache = await readLibrary();
|
|
169
169
|
const cacheByPath = new Map(cache.items.map((item) => [item.path, item]));
|
|
170
170
|
const novelResults = await Promise.all(config.novelDirectories.map((directory) => scanNovelsInDirectory(directory.id, directory.path, config.gbkNovelHandling, cacheByPath)));
|
|
171
171
|
const comicResults = await Promise.all(config.comicDirectories.map((directory) => scanComicsInDirectory(directory.id, directory.path, cacheByPath)));
|
|
172
172
|
const items = [...novelResults.flat(), ...comicResults.flat()];
|
|
173
|
-
await writeLibraryScanCache({ items });
|
|
174
173
|
return items;
|
|
175
174
|
}
|
|
176
175
|
export async function listComicPages(comicDir) {
|
|
@@ -3,9 +3,9 @@ import { tmpdir } from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
-
import { getLibraryCacheFilePath } from '../config/paths.js';
|
|
7
6
|
import { COMIC_COVER_FILENAME } from './files.js';
|
|
8
7
|
import { listComicPages, scanLibrary } from './scan.js';
|
|
8
|
+
import { readLibrary, writeLibrary } from './storage.js';
|
|
9
9
|
import { writeTestImage } from './test-images.js';
|
|
10
10
|
describe('scanLibrary', () => {
|
|
11
11
|
let tempDir;
|
|
@@ -237,7 +237,7 @@ describe('scanLibrary', () => {
|
|
|
237
237
|
pages: [{ filename: 'cover.jpeg', width: 320, height: 480 }],
|
|
238
238
|
});
|
|
239
239
|
});
|
|
240
|
-
it('
|
|
240
|
+
it('reuses cached comic directories from persisted library', async () => {
|
|
241
241
|
const comicRoot = path.join(tempDir, 'comics');
|
|
242
242
|
const mangaDir = path.join(comicRoot, 'manga-cached');
|
|
243
243
|
await mkdir(mangaDir, { recursive: true });
|
|
@@ -253,6 +253,7 @@ describe('scanLibrary', () => {
|
|
|
253
253
|
coverFilename: COMIC_COVER_FILENAME,
|
|
254
254
|
addedAt: expect.any(String),
|
|
255
255
|
});
|
|
256
|
+
await writeLibrary({ items: first });
|
|
256
257
|
await rm(path.join(mangaDir, '1.jpg'));
|
|
257
258
|
await rm(path.join(mangaDir, COMIC_COVER_FILENAME));
|
|
258
259
|
const second = await scanLibrary({
|
|
@@ -262,8 +263,6 @@ describe('scanLibrary', () => {
|
|
|
262
263
|
});
|
|
263
264
|
expect(second).toEqual(first);
|
|
264
265
|
await expect(access(path.join(mangaDir, COMIC_COVER_FILENAME))).rejects.toThrow();
|
|
265
|
-
const cache = JSON.parse(await readFile(getLibraryCacheFilePath(), 'utf-8'));
|
|
266
|
-
expect(cache.items).toHaveLength(1);
|
|
267
266
|
});
|
|
268
267
|
it('reuses cached novels without rechecking encoding', async () => {
|
|
269
268
|
const novelRoot = path.join(tempDir, 'novels');
|
|
@@ -275,6 +274,7 @@ describe('scanLibrary', () => {
|
|
|
275
274
|
comicDirectories: [],
|
|
276
275
|
gbkNovelHandling: 'ignore',
|
|
277
276
|
});
|
|
277
|
+
await writeLibrary({ items: first });
|
|
278
278
|
await writeFile(filePath, Buffer.from([0xd6, 0xd0, 0xce, 0xc4]));
|
|
279
279
|
const second = await scanLibrary({
|
|
280
280
|
novelDirectories: [{ id: 'novel-root', path: novelRoot }],
|
|
@@ -283,25 +283,25 @@ describe('scanLibrary', () => {
|
|
|
283
283
|
});
|
|
284
284
|
expect(second).toEqual(first);
|
|
285
285
|
});
|
|
286
|
-
it('removes deleted entries
|
|
286
|
+
it('removes deleted entries after scanning persisted library', async () => {
|
|
287
287
|
const novelRoot = path.join(tempDir, 'novels');
|
|
288
288
|
const filePath = path.join(novelRoot, 'book.txt');
|
|
289
289
|
await mkdir(novelRoot);
|
|
290
290
|
await writeFile(filePath, 'content');
|
|
291
|
-
await scanLibrary({
|
|
291
|
+
const first = await scanLibrary({
|
|
292
292
|
novelDirectories: [{ id: 'novel-root', path: novelRoot }],
|
|
293
293
|
comicDirectories: [],
|
|
294
294
|
gbkNovelHandling: 'ignore',
|
|
295
295
|
});
|
|
296
|
+
await writeLibrary({ items: first });
|
|
296
297
|
await rm(filePath);
|
|
297
298
|
const items = await scanLibrary({
|
|
298
299
|
novelDirectories: [{ id: 'novel-root', path: novelRoot }],
|
|
299
300
|
comicDirectories: [],
|
|
300
301
|
gbkNovelHandling: 'ignore',
|
|
301
302
|
});
|
|
302
|
-
const cache = JSON.parse(await readFile(getLibraryCacheFilePath(), 'utf-8'));
|
|
303
303
|
expect(items).toHaveLength(0);
|
|
304
|
-
expect(
|
|
304
|
+
expect((await readLibrary()).items).toHaveLength(1);
|
|
305
305
|
});
|
|
306
306
|
});
|
|
307
307
|
describe('listComicPages', () => {
|
|
@@ -2,6 +2,7 @@ import { attachProgressToDetail, attachProgressToListItem } from '../reading/pro
|
|
|
2
2
|
import { buildComicImageUrl } from './comic-url.js';
|
|
3
3
|
import { NotFoundError, ScanInProgressError } from './errors.js';
|
|
4
4
|
import { scanLibrary } from './scan.js';
|
|
5
|
+
import { readLibrary, writeLibrary } from './storage.js';
|
|
5
6
|
export function toListItemBase(item) {
|
|
6
7
|
if (item.type === 'novel') {
|
|
7
8
|
return {
|
|
@@ -43,10 +44,15 @@ function toItemDetailBase(item) {
|
|
|
43
44
|
}
|
|
44
45
|
export class LibraryService {
|
|
45
46
|
getConfig;
|
|
46
|
-
items
|
|
47
|
+
items;
|
|
47
48
|
scanning = false;
|
|
48
|
-
constructor(getConfig) {
|
|
49
|
+
constructor(getConfig, items) {
|
|
49
50
|
this.getConfig = getConfig;
|
|
51
|
+
this.items = items;
|
|
52
|
+
}
|
|
53
|
+
static async create(getConfig) {
|
|
54
|
+
const library = await readLibrary();
|
|
55
|
+
return new LibraryService(getConfig, library.items);
|
|
50
56
|
}
|
|
51
57
|
getState(progressMap = {}) {
|
|
52
58
|
return {
|
|
@@ -67,7 +73,9 @@ export class LibraryService {
|
|
|
67
73
|
}
|
|
68
74
|
this.scanning = true;
|
|
69
75
|
try {
|
|
70
|
-
|
|
76
|
+
const items = await scanLibrary(this.getConfig());
|
|
77
|
+
await writeLibrary({ items });
|
|
78
|
+
this.items = items;
|
|
71
79
|
}
|
|
72
80
|
finally {
|
|
73
81
|
this.scanning = false;
|
|
@@ -2,14 +2,21 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { getLibraryFilePath } from '../config/paths.js';
|
|
5
6
|
import { NotFoundError, ScanInProgressError } from './errors.js';
|
|
6
7
|
import { LibraryService } from './service.js';
|
|
8
|
+
import { readLibrary } from './storage.js';
|
|
7
9
|
import { writeTestImage } from './test-images.js';
|
|
8
10
|
describe('LibraryService', () => {
|
|
9
11
|
let tempDir;
|
|
10
12
|
let previousConfigDir;
|
|
11
13
|
let novelRoot;
|
|
12
14
|
let comicRoot;
|
|
15
|
+
const config = () => ({
|
|
16
|
+
novelDirectories: [{ id: 'novel-root', path: novelRoot }],
|
|
17
|
+
comicDirectories: [{ id: 'comic-root', path: comicRoot }],
|
|
18
|
+
gbkNovelHandling: 'ignore',
|
|
19
|
+
});
|
|
13
20
|
beforeEach(async () => {
|
|
14
21
|
previousConfigDir = process.env.CYREADER_CONFIG_DIR;
|
|
15
22
|
tempDir = await mkdtemp(path.join(tmpdir(), 'cyreader-service-'));
|
|
@@ -30,14 +37,33 @@ describe('LibraryService', () => {
|
|
|
30
37
|
await rm(tempDir, { recursive: true, force: true });
|
|
31
38
|
});
|
|
32
39
|
function createService() {
|
|
33
|
-
return
|
|
34
|
-
novelDirectories: [{ id: 'novel-root', path: novelRoot }],
|
|
35
|
-
comicDirectories: [{ id: 'comic-root', path: comicRoot }],
|
|
36
|
-
gbkNovelHandling: 'ignore',
|
|
37
|
-
}));
|
|
40
|
+
return LibraryService.create(config);
|
|
38
41
|
}
|
|
42
|
+
it('returns persisted items before scan', async () => {
|
|
43
|
+
const scanned = await createService();
|
|
44
|
+
await scanned.scan();
|
|
45
|
+
const preloaded = await createService();
|
|
46
|
+
expect(preloaded.getState()).toEqual({
|
|
47
|
+
scanning: false,
|
|
48
|
+
items: scanned.getState().items,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
it('keeps persisted items visible while scan is in progress', async () => {
|
|
52
|
+
const service = await createService();
|
|
53
|
+
await service.scan();
|
|
54
|
+
const before = service.getState().items;
|
|
55
|
+
await writeFile(path.join(novelRoot, 'newbook.txt'), 'more content');
|
|
56
|
+
const scanPromise = service.scan();
|
|
57
|
+
expect(service.getState()).toEqual({
|
|
58
|
+
scanning: true,
|
|
59
|
+
items: before,
|
|
60
|
+
});
|
|
61
|
+
await scanPromise;
|
|
62
|
+
expect(service.getState().scanning).toBe(false);
|
|
63
|
+
expect(service.getState().items).toHaveLength(2);
|
|
64
|
+
});
|
|
39
65
|
it('populates list items after scan', async () => {
|
|
40
|
-
const service = createService();
|
|
66
|
+
const service = await createService();
|
|
41
67
|
await service.scan();
|
|
42
68
|
expect(service.getState()).toEqual({
|
|
43
69
|
scanning: false,
|
|
@@ -52,14 +78,22 @@ describe('LibraryService', () => {
|
|
|
52
78
|
],
|
|
53
79
|
});
|
|
54
80
|
});
|
|
81
|
+
it('writes library.json after scan', async () => {
|
|
82
|
+
const service = await createService();
|
|
83
|
+
await service.scan();
|
|
84
|
+
const library = await readLibrary();
|
|
85
|
+
expect(library.items).toHaveLength(1);
|
|
86
|
+
expect(library.items[0]).toMatchObject({ type: 'novel', title: 'book' });
|
|
87
|
+
expect(getLibraryFilePath()).toContain('library.json');
|
|
88
|
+
});
|
|
55
89
|
it('rejects concurrent scan', async () => {
|
|
56
|
-
const service = createService();
|
|
90
|
+
const service = await createService();
|
|
57
91
|
const firstScan = service.scan();
|
|
58
92
|
await expect(service.scan()).rejects.toBeInstanceOf(ScanInProgressError);
|
|
59
93
|
await firstScan;
|
|
60
94
|
});
|
|
61
95
|
it('returns novel detail for a known id', async () => {
|
|
62
|
-
const service = createService();
|
|
96
|
+
const service = await createService();
|
|
63
97
|
await service.scan();
|
|
64
98
|
const novel = service.getState().items[0];
|
|
65
99
|
expect(service.getItemDetail(novel.id)).toEqual({
|
|
@@ -77,7 +111,7 @@ describe('LibraryService', () => {
|
|
|
77
111
|
await mkdir(mangaDir);
|
|
78
112
|
await writeTestImage(path.join(mangaDir, '2.jpg'), 300, 400);
|
|
79
113
|
await writeTestImage(path.join(mangaDir, '1.jpg'), 300, 400);
|
|
80
|
-
const service = createService();
|
|
114
|
+
const service = await createService();
|
|
81
115
|
await service.scan();
|
|
82
116
|
const comic = service.getState().items.find((item) => item.type === 'comic');
|
|
83
117
|
expect(comic).toBeDefined();
|
|
@@ -100,7 +134,7 @@ describe('LibraryService', () => {
|
|
|
100
134
|
});
|
|
101
135
|
});
|
|
102
136
|
it('throws when detail is requested for unknown id', async () => {
|
|
103
|
-
const service = createService();
|
|
137
|
+
const service = await createService();
|
|
104
138
|
await service.scan();
|
|
105
139
|
expect(() => service.getItemDetail('missing')).toThrow(NotFoundError);
|
|
106
140
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getConfigDir, getLegacyLibraryCacheFilePath, getLibraryFilePath, } from '../config/paths.js';
|
|
4
|
+
const comicPageSchema = z.object({
|
|
5
|
+
filename: z.string().min(1),
|
|
6
|
+
width: z.number().int().positive(),
|
|
7
|
+
height: z.number().int().positive(),
|
|
8
|
+
});
|
|
9
|
+
const novelItemSchema = z.object({
|
|
10
|
+
id: z.string().min(1),
|
|
11
|
+
type: z.literal('novel'),
|
|
12
|
+
title: z.string(),
|
|
13
|
+
path: z.string().min(1),
|
|
14
|
+
addedAt: z.string().datetime(),
|
|
15
|
+
directoryId: z.string().min(1),
|
|
16
|
+
filename: z.string().min(1),
|
|
17
|
+
});
|
|
18
|
+
const comicItemSchema = z.object({
|
|
19
|
+
id: z.string().min(1),
|
|
20
|
+
type: z.literal('comic'),
|
|
21
|
+
title: z.string(),
|
|
22
|
+
path: z.string().min(1),
|
|
23
|
+
addedAt: z.string().datetime(),
|
|
24
|
+
directoryId: z.string().min(1),
|
|
25
|
+
pages: z.array(comicPageSchema),
|
|
26
|
+
coverFilename: z.string().nullable(),
|
|
27
|
+
});
|
|
28
|
+
const librarySchema = z.object({
|
|
29
|
+
items: z.array(z.discriminatedUnion('type', [novelItemSchema, comicItemSchema])),
|
|
30
|
+
});
|
|
31
|
+
async function fileExists(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
await access(filePath);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function createEmptyLibrary() {
|
|
41
|
+
return { items: [] };
|
|
42
|
+
}
|
|
43
|
+
export function parseLibrary(data) {
|
|
44
|
+
const parsed = librarySchema.safeParse(data);
|
|
45
|
+
if (!parsed.success) {
|
|
46
|
+
return createEmptyLibrary();
|
|
47
|
+
}
|
|
48
|
+
return { items: parsed.data.items };
|
|
49
|
+
}
|
|
50
|
+
async function readLibraryFile(filePath) {
|
|
51
|
+
if (!(await fileExists(filePath))) {
|
|
52
|
+
return createEmptyLibrary();
|
|
53
|
+
}
|
|
54
|
+
let data;
|
|
55
|
+
try {
|
|
56
|
+
data = JSON.parse(await readFile(filePath, 'utf-8'));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return createEmptyLibrary();
|
|
60
|
+
}
|
|
61
|
+
return parseLibrary(data);
|
|
62
|
+
}
|
|
63
|
+
export async function readLibrary() {
|
|
64
|
+
const libraryFile = getLibraryFilePath();
|
|
65
|
+
if (await fileExists(libraryFile)) {
|
|
66
|
+
return readLibraryFile(libraryFile);
|
|
67
|
+
}
|
|
68
|
+
return readLibraryFile(getLegacyLibraryCacheFilePath());
|
|
69
|
+
}
|
|
70
|
+
export async function writeLibrary(library) {
|
|
71
|
+
const configDir = getConfigDir();
|
|
72
|
+
const libraryFile = getLibraryFilePath();
|
|
73
|
+
const normalized = parseLibrary(library);
|
|
74
|
+
await mkdir(configDir, { recursive: true });
|
|
75
|
+
await writeFile(libraryFile, `${JSON.stringify(normalized, null, 2)}\n`, 'utf-8');
|
|
76
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { getLegacyLibraryCacheFilePath, getLibraryFilePath } from '../config/paths.js';
|
|
6
|
+
import { createEmptyLibrary, parseLibrary, readLibrary, writeLibrary } from './storage.js';
|
|
7
|
+
describe('library storage', () => {
|
|
8
|
+
let tempDir;
|
|
9
|
+
let previousConfigDir;
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
previousConfigDir = process.env.CYREADER_CONFIG_DIR;
|
|
12
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'cyreader-storage-'));
|
|
13
|
+
process.env.CYREADER_CONFIG_DIR = tempDir;
|
|
14
|
+
});
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
if (previousConfigDir === undefined) {
|
|
17
|
+
delete process.env.CYREADER_CONFIG_DIR;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
process.env.CYREADER_CONFIG_DIR = previousConfigDir;
|
|
21
|
+
}
|
|
22
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
it('returns empty library when no file exists', async () => {
|
|
25
|
+
expect(await readLibrary()).toEqual(createEmptyLibrary());
|
|
26
|
+
});
|
|
27
|
+
it('reads library.json when present', async () => {
|
|
28
|
+
const library = {
|
|
29
|
+
items: [
|
|
30
|
+
{
|
|
31
|
+
id: 'novel-1',
|
|
32
|
+
type: 'novel',
|
|
33
|
+
title: 'book',
|
|
34
|
+
path: '/tmp/book.txt',
|
|
35
|
+
addedAt: '2024-01-01T00:00:00.000Z',
|
|
36
|
+
directoryId: 'novel-root',
|
|
37
|
+
filename: 'book.txt',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
await writeLibrary(library);
|
|
42
|
+
expect(await readLibrary()).toEqual(library);
|
|
43
|
+
expect(await readFile(getLibraryFilePath(), 'utf-8')).toContain('"novel-1"');
|
|
44
|
+
});
|
|
45
|
+
it('falls back to legacy library-cache.json', async () => {
|
|
46
|
+
const legacy = {
|
|
47
|
+
items: [
|
|
48
|
+
{
|
|
49
|
+
id: 'novel-legacy',
|
|
50
|
+
type: 'novel',
|
|
51
|
+
title: 'legacy',
|
|
52
|
+
path: '/tmp/legacy.txt',
|
|
53
|
+
addedAt: '2024-01-01T00:00:00.000Z',
|
|
54
|
+
directoryId: 'novel-root',
|
|
55
|
+
filename: 'legacy.txt',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
await writeFile(getLegacyLibraryCacheFilePath(), `${JSON.stringify(legacy, null, 2)}\n`, 'utf-8');
|
|
60
|
+
expect(await readLibrary()).toEqual(legacy);
|
|
61
|
+
});
|
|
62
|
+
it('prefers library.json over legacy cache file', async () => {
|
|
63
|
+
await writeFile(getLegacyLibraryCacheFilePath(), `${JSON.stringify({ items: [{ id: 'legacy' }] }, null, 2)}\n`, 'utf-8');
|
|
64
|
+
await writeLibrary(createEmptyLibrary());
|
|
65
|
+
expect(await readLibrary()).toEqual(createEmptyLibrary());
|
|
66
|
+
});
|
|
67
|
+
it('returns empty library for invalid JSON', async () => {
|
|
68
|
+
await writeFile(getLibraryFilePath(), '{ invalid', 'utf-8');
|
|
69
|
+
expect(await readLibrary()).toEqual(createEmptyLibrary());
|
|
70
|
+
});
|
|
71
|
+
it('returns empty library for invalid schema', async () => {
|
|
72
|
+
expect(parseLibrary({ items: [{ id: 'x', type: 'unknown' }] })).toEqual(createEmptyLibrary());
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -30,7 +30,7 @@ describe('ProgressService', () => {
|
|
|
30
30
|
await writeTestImage(path.join(mangaDir, '1.jpg'), 300, 400);
|
|
31
31
|
await writeTestImage(path.join(mangaDir, '2.jpg'), 300, 400);
|
|
32
32
|
await writeTestImage(path.join(mangaDir, '3.jpg'), 300, 400);
|
|
33
|
-
libraryService =
|
|
33
|
+
libraryService = await LibraryService.create(() => ({
|
|
34
34
|
novelDirectories: [{ id: 'novel-root', path: novelRoot }],
|
|
35
35
|
comicDirectories: [{ id: 'comic-root', path: comicRoot }],
|
|
36
36
|
gbkNovelHandling: 'ignore',
|
|
@@ -21,7 +21,7 @@ describe('RecentService', () => {
|
|
|
21
21
|
novelRoot = path.join(tempDir, 'novels');
|
|
22
22
|
await mkdir(novelRoot);
|
|
23
23
|
await Promise.all(Array.from({ length: MAX_RECENT + 2 }, (_, index) => writeFile(path.join(novelRoot, `book-${index}.txt`), 'content')));
|
|
24
|
-
libraryService =
|
|
24
|
+
libraryService = await LibraryService.create(() => ({
|
|
25
25
|
novelDirectories: [{ id: 'novel-root', path: novelRoot }],
|
|
26
26
|
comicDirectories: [],
|
|
27
27
|
gbkNovelHandling: 'ignore',
|
|
@@ -21,7 +21,7 @@ describe('config routes', () => {
|
|
|
21
21
|
tempDir = await mkdtemp(path.join(tmpdir(), 'cyreader-routes-'));
|
|
22
22
|
process.env.CYREADER_CONFIG_DIR = tempDir;
|
|
23
23
|
configService = await ConfigService.create();
|
|
24
|
-
libraryService =
|
|
24
|
+
libraryService = await LibraryService.create(() => configService.get());
|
|
25
25
|
const recentService = await RecentService.create(libraryService);
|
|
26
26
|
const progressService = await ProgressService.create(libraryService);
|
|
27
27
|
app = createApp(configService, libraryService, recentService, progressService);
|
|
@@ -34,7 +34,7 @@ describe('library routes', () => {
|
|
|
34
34
|
novelDirectories: [{ id: 'novel-root', path: novelRoot }],
|
|
35
35
|
comicDirectories: [{ id: 'comic-root', path: comicRoot }],
|
|
36
36
|
});
|
|
37
|
-
libraryService =
|
|
37
|
+
libraryService = await LibraryService.create(() => configService.get());
|
|
38
38
|
const recentService = await RecentService.create(libraryService);
|
|
39
39
|
const progressService = await ProgressService.create(libraryService);
|
|
40
40
|
app = createApp(configService, libraryService, recentService, progressService);
|
|
@@ -30,7 +30,7 @@ describe('reading routes', () => {
|
|
|
30
30
|
novelDirectories: [{ id: 'novel-root', path: novelRoot }],
|
|
31
31
|
comicDirectories: [],
|
|
32
32
|
});
|
|
33
|
-
libraryService =
|
|
33
|
+
libraryService = await LibraryService.create(() => configService.get());
|
|
34
34
|
await libraryService.scan();
|
|
35
35
|
recentService = await RecentService.create(libraryService);
|
|
36
36
|
const progressService = await ProgressService.create(libraryService);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{Y as e,a as t,b as n,c as r,f as i,g as a,h as o,i as s,l as c,p as l,s as u,t as d,u as f,x as p}from"./utils-ChkMmPzE.js";import{a as m,i as h,r as g}from"./query-keys-CbbgiZx4.js";import{i as _,r as ee,t as te}from"./use-library-Bh7Bunn1.js";import{t as ne}from"./useNavigate-B29ssGbr.js";import{t as v}from"./useRouterState-
|
|
1
|
+
import{Y as e,a as t,b as n,c as r,f as i,g as a,h as o,i as s,l as c,p as l,s as u,t as d,u as f,x as p}from"./utils-ChkMmPzE.js";import{a as m,i as h,r as g}from"./query-keys-CbbgiZx4.js";import{i as _,r as ee,t as te}from"./use-library-Bh7Bunn1.js";import{t as ne}from"./useNavigate-B29ssGbr.js";import{t as v}from"./useRouterState-DDtlowFh.js";import{a as re,t as ie}from"./library-utils-BRrejTkM.js";import{a as ae,i as oe,o as se,s as ce,t as le}from"./shell-context-Buw4m44t.js";import{a as ue,f as de,i as fe,m as y,p as pe,r as me,v as he,y as ge}from"./index-BS403FO9.js";var _e=a(`bookmark`,[[`path`,{d:`M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z`,key:`oz39mx`}]]),ve=a(`circle-check-big`,[[`path`,{d:`M21.801 10A10 10 0 1 1 17 3.335`,key:`yps3ct`}],[`path`,{d:`m9 11 3 3L22 4`,key:`1pflzl`}]]),ye=a(`circle-plus`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M8 12h8`,key:`1wcyev`}],[`path`,{d:`M12 8v8`,key:`napkw2`}]]),be=a(`layers`,[[`path`,{d:`M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z`,key:`zw3jo`}],[`path`,{d:`M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12`,key:`1wduqc`}],[`path`,{d:`M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17`,key:`kqbvx6`}]]),xe=a(`library`,[[`path`,{d:`m16 6 4 14`,key:`ji33uf`}],[`path`,{d:`M12 6v14`,key:`1n7gus`}],[`path`,{d:`M8 8v12`,key:`1gg7y9`}],[`path`,{d:`M4 4v16`,key:`6qkkli`}]]),Se=a(`settings`,[[`path`,{d:`M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915`,key:`1i5ecw`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),b=e(p(),1),x=n(),S=`Dialog`,[C,Ce]=c(S),[we,w]=C(S),T=e=>{let{__scopeDialog:n,children:r,open:i,defaultOpen:a,onOpenChange:o,modal:c=!0}=e,l=b.useRef(null),u=b.useRef(null),[d,f]=t({prop:i,defaultProp:a??!1,onChange:o,caller:S});return(0,x.jsx)(we,{scope:n,triggerRef:l,contentRef:u,contentId:s(),titleId:s(),descriptionId:s(),open:d,onOpenChange:f,onOpenToggle:b.useCallback(()=>f(e=>!e),[f]),modal:c,children:r})};T.displayName=S;var E=`DialogTrigger`,Te=b.forwardRef((e,t)=>{let{__scopeDialog:n,...r}=e,i=w(E,n),a=o(t,i.triggerRef);return(0,x.jsx)(f.button,{type:`button`,"aria-haspopup":`dialog`,"aria-expanded":i.open,"aria-controls":i.open?i.contentId:void 0,"data-state":V(i.open),...r,ref:a,onClick:u(e.onClick,i.onOpenToggle)})});Te.displayName=E;var D=`DialogPortal`,[Ee,O]=C(D,{forceMount:void 0}),k=e=>{let{__scopeDialog:t,forceMount:n,children:r,container:i}=e,a=w(D,t);return(0,x.jsx)(Ee,{scope:t,forceMount:n,children:b.Children.map(r,e=>(0,x.jsx)(y,{present:n||a.open,children:(0,x.jsx)(de,{asChild:!0,container:i,children:e})}))})};k.displayName=D;var A=`DialogOverlay`,j=b.forwardRef((e,t)=>{let n=O(A,e.__scopeDialog),{forceMount:r=n.forceMount,...i}=e,a=w(A,e.__scopeDialog);return a.modal?(0,x.jsx)(y,{present:r||a.open,children:(0,x.jsx)(Oe,{...i,ref:t})}):null});j.displayName=A;var De=l(`DialogOverlay.RemoveScroll`),Oe=b.forwardRef((e,t)=>{let{__scopeDialog:n,...r}=e,i=w(A,n);return(0,x.jsx)(ae,{as:De,allowPinchZoom:!0,shards:[i.contentRef],children:(0,x.jsx)(f.div,{"data-state":V(i.open),...r,ref:t,style:{pointerEvents:`auto`,...r.style}})})}),M=`DialogContent`,N=b.forwardRef((e,t)=>{let n=O(M,e.__scopeDialog),{forceMount:r=n.forceMount,...i}=e,a=w(M,e.__scopeDialog);return(0,x.jsx)(y,{present:r||a.open,children:a.modal?(0,x.jsx)(ke,{...i,ref:t}):(0,x.jsx)(Ae,{...i,ref:t})})});N.displayName=M;var ke=b.forwardRef((e,t)=>{let n=w(M,e.__scopeDialog),r=b.useRef(null),i=o(t,n.contentRef,r);return b.useEffect(()=>{let e=r.current;if(e)return oe(e)},[]),(0,x.jsx)(P,{...e,ref:i,trapFocus:n.open,disableOutsidePointerEvents:n.open,onCloseAutoFocus:u(e.onCloseAutoFocus,e=>{e.preventDefault(),n.triggerRef.current?.focus()}),onPointerDownOutside:u(e.onPointerDownOutside,e=>{let t=e.detail.originalEvent,n=t.button===0&&t.ctrlKey===!0;(t.button===2||n)&&e.preventDefault()}),onFocusOutside:u(e.onFocusOutside,e=>e.preventDefault())})}),Ae=b.forwardRef((e,t)=>{let n=w(M,e.__scopeDialog),r=b.useRef(!1),i=b.useRef(!1);return(0,x.jsx)(P,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:t=>{e.onCloseAutoFocus?.(t),t.defaultPrevented||(r.current||n.triggerRef.current?.focus(),t.preventDefault()),r.current=!1,i.current=!1},onInteractOutside:t=>{e.onInteractOutside?.(t),t.defaultPrevented||(r.current=!0,t.detail.originalEvent.type===`pointerdown`&&(i.current=!0));let a=t.target;n.triggerRef.current?.contains(a)&&t.preventDefault(),t.detail.originalEvent.type===`focusin`&&i.current&&t.preventDefault()}})}),P=b.forwardRef((e,t)=>{let{__scopeDialog:n,trapFocus:r,onOpenAutoFocus:i,onCloseAutoFocus:a,...s}=e,c=w(M,n),l=b.useRef(null),u=o(t,l);return se(),(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(ce,{asChild:!0,loop:!0,trapped:r,onMountAutoFocus:i,onUnmountAutoFocus:a,children:(0,x.jsx)(pe,{role:`dialog`,id:c.contentId,"aria-describedby":c.descriptionId,"aria-labelledby":c.titleId,"data-state":V(c.open),...s,ref:u,onDismiss:()=>c.onOpenChange(!1)})}),(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(Me,{titleId:c.titleId}),(0,x.jsx)(Pe,{contentRef:l,descriptionId:c.descriptionId})]})]})}),F=`DialogTitle`,I=b.forwardRef((e,t)=>{let{__scopeDialog:n,...r}=e,i=w(F,n);return(0,x.jsx)(f.h2,{id:i.titleId,...r,ref:t})});I.displayName=F;var L=`DialogDescription`,R=b.forwardRef((e,t)=>{let{__scopeDialog:n,...r}=e,i=w(L,n);return(0,x.jsx)(f.p,{id:i.descriptionId,...r,ref:t})});R.displayName=L;var z=`DialogClose`,B=b.forwardRef((e,t)=>{let{__scopeDialog:n,...r}=e,i=w(z,n);return(0,x.jsx)(f.button,{type:`button`,...r,ref:t,onClick:u(e.onClick,()=>i.onOpenChange(!1))})});B.displayName=z;function V(e){return e?`open`:`closed`}var H=`DialogTitleWarning`,[je,U]=r(H,{contentName:M,titleName:F,docsSlug:`dialog`}),Me=({titleId:e})=>{let t=U(H),n=`\`${t.contentName}\` requires a \`${t.titleName}\` for the component to be accessible for screen reader users.
|
|
2
2
|
|
|
3
3
|
If you want to hide the \`${t.titleName}\`, you can wrap it with our VisuallyHidden component.
|
|
4
4
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Y as e,b as t,f as n,g as r,t as i,x as a}from"./utils-ChkMmPzE.js";import{a as o,i as s,o as c,r as l,t as u}from"./query-keys-CbbgiZx4.js";import{t as d}from"./useNavigate-B29ssGbr.js";import{a as f,c as p,i as m,n as h,o as g,r as _,s as v,t as y}from"./scroll-area-BUlmpLx6.js";import{a as ee,i as te,n as ne,r as re}from"./library-utils-BRrejTkM.js";import{n as b,r as x}from"./shell-context-Buw4m44t.js";import{a as S,i as C,t as w}from"./reading-BozmJNi1.js";import{a as T,i as E}from"./reading-progress-C5bkK-vF.js";var D=r(`clock`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 6v6l4 2`,key:`mmk7yg`}]]),O=r(`layout-grid`,[[`rect`,{width:`7`,height:`7`,x:`3`,y:`3`,rx:`1`,key:`1g98yp`}],[`rect`,{width:`7`,height:`7`,x:`14`,y:`3`,rx:`1`,key:`6d4xhi`}],[`rect`,{width:`7`,height:`7`,x:`14`,y:`14`,rx:`1`,key:`nxv5o0`}],[`rect`,{width:`7`,height:`7`,x:`3`,y:`14`,rx:`1`,key:`1bb6yr`}]]),k=r(`list`,[[`path`,{d:`M3 5h.01`,key:`18ugdj`}],[`path`,{d:`M3 12h.01`,key:`nlz23k`}],[`path`,{d:`M3 19h.01`,key:`noohij`}],[`path`,{d:`M8 5h13`,key:`1pao27`}],[`path`,{d:`M8 12h13`,key:`1za7za`}],[`path`,{d:`M8 19h13`,key:`m83p4d`}]]),A=r(`search-x`,[[`path`,{d:`m13.5 8.5-5 5`,key:`1cs55j`}],[`path`,{d:`m8.5 8.5 5 5`,key:`a8mexj`}],[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),j=r(`search`,[[`path`,{d:`m21 21-4.34-4.34`,key:`14j7rj`}],[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}]]),M=e(a(),1),N=t();function P({className:e,...t}){return(0,N.jsx)(`div`,{"data-slot":`empty`,className:i(`flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance`,e),...t})}function F({className:e,...t}){return(0,N.jsx)(`div`,{"data-slot":`empty-header`,className:i(`flex max-w-sm flex-col items-center gap-2`,e),...t})}var I=o(`mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0`,{variants:{variant:{default:`bg-transparent`,icon:`flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4`}},defaultVariants:{variant:`default`}});function L({className:e,variant:t=`default`,...n}){return(0,N.jsx)(`div`,{"data-slot":`empty-icon`,"data-variant":t,className:i(I({variant:t,className:e})),...n})}function R({className:e,...t}){return(0,N.jsx)(`div`,{"data-slot":`empty-title`,className:i(`font-heading text-sm font-medium tracking-tight`,e),...t})}function z({className:e,...t}){return(0,N.jsx)(`div`,{"data-slot":`empty-description`,className:i(`text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary`,e),...t})}var ie=o(`group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!`,{variants:{variant:{default:`bg-primary text-primary-foreground [a]:hover:bg-primary/80`,secondary:`bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80`,destructive:`bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20`,outline:`border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground`,ghost:`hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50`,link:`text-primary underline-offset-4 hover:underline`}},defaultVariants:{variant:`default`}});function B({className:e,variant:t=`default`,asChild:r=!1,...a}){return(0,N.jsx)(r?n:`span`,{"data-slot":`badge`,"data-variant":t,className:i(ie({variant:t}),e),...a})}var V=`/novel-default-cover.png`;function H(e){return e.type===`comic`?e.cover:V}function U(e,t){if(t.type===`novel`){e({to:`/read/novel/$id`,params:{id:t.id},state:{readerTitle:t.title}});return}e({to:`/read/comic/$id`,params:{id:t.id},state:{readerTitle:t.title}})}var W=`h-[180px]`,G=`h-16`,K=`h-[244px]`,q={reading:`bg-indigo-500 shadow-[0_0_6px_rgba(99,102,241,0.4)]`,unread:`bg-green-500 shadow-[0_0_6px_rgba(34,197,94,0.4)]`,completed:`bg-muted-foreground`};function J({item:e,view:t}){let n=d(),r=()=>{U(n,e)},a=E(e.progress),o=T(e);return t===`list`?(0,N.jsxs)(`button`,{type:`button`,onClick:r,className:`flex h-14 w-full items-stretch overflow-hidden rounded-md border border-[#25252a] bg-[#1a1a20] text-left transition-colors hover:border-[#3a3a44]`,children:[(0,N.jsx)(`div`,{className:`relative h-full w-10 shrink-0 overflow-hidden bg-muted`,children:(0,N.jsx)(`img`,{src:H(e),alt:``,className:`absolute inset-0 block size-full object-cover`,loading:`lazy`})}),(0,N.jsxs)(`div`,{className:`flex min-w-0 flex-1 items-center gap-4 px-3.5`,children:[(0,N.jsx)(`div`,{className:`min-w-0 flex-1 truncate text-xs font-semibold text-[#e2e2e8]`,children:e.title}),(0,N.jsx)(`div`,{className:`min-w-[100px]`,children:(0,N.jsx)(`div`,{className:`h-[3px] w-full overflow-hidden rounded-full bg-[#2a2a32]`,children:(0,N.jsx)(`div`,{className:i(`h-full rounded-full`,a===`unread`?`bg-muted-foreground`:`bg-indigo-500`),style:{width:`${o}%`}})})})]})]}):(0,N.jsxs)(`button`,{type:`button`,onClick:r,className:i(`group flex w-full flex-col overflow-hidden rounded-lg border border-[#25252a] bg-[#1a1a20] text-left transition-all hover:-translate-y-px hover:border-[#3a3a44]`,K),children:[(0,N.jsxs)(`div`,{className:i(`relative w-full shrink-0 overflow-hidden bg-muted`,W),children:[(0,N.jsx)(`img`,{src:H(e),alt:``,className:`absolute inset-0 block size-full object-cover`,loading:`lazy`}),(0,N.jsx)(B,{variant:`secondary`,className:`absolute top-1.5 left-1.5 border-0 bg-black/60 px-1.5 py-0 text-[10px] text-[#c2c2ca] backdrop-blur-sm`,children:e.type===`novel`?`小说`:`漫画`}),(0,N.jsx)(`span`,{className:i(`absolute top-1.5 right-1.5 size-2 rounded-full`,q[a])})]}),(0,N.jsxs)(`div`,{className:i(`flex shrink-0 flex-col justify-between p-2.5`,G),children:[(0,N.jsx)(`div`,{className:`truncate text-xs leading-4 font-semibold text-[#e2e2e8]`,children:e.title}),(0,N.jsx)(`div`,{className:`h-[3px] w-full overflow-hidden rounded-full bg-[#2a2a32]`,children:(0,N.jsx)(`div`,{className:i(`h-full rounded-full`,a===`unread`?`bg-muted-foreground`:`bg-indigo-500`),style:{width:`${o}%`}})})]})]})}function Y({items:e,view:t,filterLabel:n}){return e.length===0?(0,N.jsx)(P,{className:`border py-16`,children:(0,N.jsxs)(F,{children:[(0,N.jsx)(L,{variant:`icon`,children:(0,N.jsx)(A,{})}),(0,N.jsx)(R,{children:`未找到匹配的作品`}),(0,N.jsx)(z,{children:`尝试调整搜索词或筛选条件`})]})}):(0,N.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,N.jsx)(`div`,{className:`flex items-center justify-between py-2`,children:(0,N.jsxs)(`div`,{className:`text-xs text-muted-foreground`,children:[n,` · `,e.length,` 部作品`]})}),t===`list`?(0,N.jsxs)(`div`,{className:`mb-1.5 hidden grid-cols-[40px_1fr_1fr_80px_100px] gap-4 border-b px-3.5 py-1.5 text-[11px] font-semibold tracking-wide text-muted-foreground uppercase md:grid`,children:[(0,N.jsx)(`div`,{}),(0,N.jsx)(`div`,{children:`作品`}),(0,N.jsx)(`div`,{children:`作者`}),(0,N.jsx)(`div`,{className:`text-right`,children:`进度`}),(0,N.jsx)(`div`,{className:`text-right`,children:`状态`})]}):null,(0,N.jsx)(`div`,{className:i(t===`grid`?`grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3.5`:`flex flex-col gap-1.5`),children:e.map(e=>(0,N.jsx)(J,{item:e,view:t},e.id))})]})}var X=`h-8 border-[#2a2a32] bg-[#1a1a20] text-xs text-[#9f9fad] hover:bg-[#23232a] hover:text-[#d5d5dd]`;function ae({query:e,onQueryChange:t,sort:n,onSortChange:r,view:a,onViewChange:o,scanning:c=!1}){return(0,N.jsxs)(`header`,{className:`flex h-14 shrink-0 items-center gap-3 border-b px-5`,children:[(0,N.jsxs)(`div`,{className:`relative max-w-[520px] flex-1`,children:[(0,N.jsx)(j,{className:`pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground`}),(0,N.jsx)(x,{value:e,onChange:e=>t(e.target.value),placeholder:`搜索作品、作者...`,className:`h-[34px] border-[#2a2a32] bg-[#1a1a20] pl-8 text-[13px] text-[#e2e2e8] placeholder:text-[#6b6b78] focus-visible:border-indigo-500`}),e?(0,N.jsx)(s,{type:`button`,variant:`ghost`,size:`icon-xs`,className:`absolute top-1/2 right-1.5 -translate-y-1/2`,onClick:()=>t(``),children:(0,N.jsx)(ee,{})}):null]}),(0,N.jsxs)(`div`,{className:`ml-auto flex items-center gap-3`,children:[c?(0,N.jsxs)(`div`,{className:`flex h-8 items-center gap-1.5 text-xs text-muted-foreground`,children:[(0,N.jsx)(p,{className:`size-3.5 animate-spin`}),`扫描中...`]}):null,(0,N.jsxs)(h,{value:n,onValueChange:e=>r(e),children:[(0,N.jsx)(g,{size:`sm`,className:i(X,`h-8 min-w-[108px] px-2.5`),children:(0,N.jsx)(v,{})}),(0,N.jsx)(_,{children:(0,N.jsxs)(m,{children:[(0,N.jsx)(f,{value:`addedAt`,children:`最近添加`}),(0,N.jsx)(f,{value:`name`,children:`名称`})]})})]}),(0,N.jsxs)(C,{type:`single`,value:a,onValueChange:e=>{e&&o(e)},variant:`outline`,size:`sm`,className:`gap-0.5`,children:[(0,N.jsx)(S,{value:`grid`,"aria-label":`网格视图`,className:i(X,`px-2 data-[state=on]:border-[#3e3e52] data-[state=on]:bg-[#2a2a3a] data-[state=on]:text-[#e2e2e8]`),children:(0,N.jsx)(O,{})}),(0,N.jsx)(S,{value:`list`,"aria-label":`列表视图`,className:i(X,`px-2 data-[state=on]:border-[#3e3e52] data-[state=on]:bg-[#2a2a3a] data-[state=on]:text-[#e2e2e8]`),children:(0,N.jsx)(k,{})})]})]})]})}function oe(){let e=c({queryKey:u.recent,queryFn:w});return{items:e.data?.items??[],loading:e.isPending,error:e.error?e.error instanceof Error?e.error.message:`Failed to load recent reading`:null}}function se(){let{filter:e}=b(),{items:t,loading:n}=oe(),r=(0,M.useMemo)(()=>e===`novel`||e===`comic`?t.filter(t=>t.type===e):t,[e,t]);return(0,N.jsxs)(`section`,{className:`mb-2`,children:[(0,N.jsxs)(`div`,{className:`flex items-center gap-2 py-4 text-[13px] font-semibold text-foreground/80`,children:[(0,N.jsx)(D,{className:`size-3.5 text-indigo-500`}),`最近阅读`]}),n?(0,N.jsx)(`div`,{className:`grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3.5 pb-2`,children:Array.from({length:4},(e,t)=>(0,N.jsxs)(`div`,{className:i(`flex flex-col overflow-hidden rounded-lg border border-[#25252a] bg-[#1a1a20]`,K),children:[(0,N.jsx)(l,{className:i(`w-full shrink-0 rounded-none`,W)}),(0,N.jsxs)(`div`,{className:i(`flex flex-col justify-between p-2.5`,G),children:[(0,N.jsx)(l,{className:`h-3 w-4/5`}),(0,N.jsx)(l,{className:`h-[3px] w-full`})]})]},t))}):null,!n&&r.length===0?(0,N.jsx)(`div`,{className:`pb-2 text-xs text-muted-foreground`,children:`暂无阅读记录`}):null,!n&&r.length>0?(0,N.jsx)(`div`,{className:`grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3.5 pb-2`,children:r.map(e=>(0,N.jsx)(J,{item:e,view:`grid`},e.id))}):null]})}var Z=`cyreader:library-sort`,Q=`addedAt`,ce=new Set([`name`,`addedAt`]);function le(e){return typeof e==`string`&&ce.has(e)?e:Q}function $(){if(typeof window>`u`)return Q;try{let e=window.localStorage.getItem(Z);return e?le(JSON.parse(e)):Q}catch{return Q}}function ue(e){if(!(typeof window>`u`))try{window.localStorage.setItem(Z,JSON.stringify(e))}catch{}}function de(){let{filter:e,items:t,scanning:n}=b(),[r,i]=(0,M.useState)(``),[a,o]=(0,M.useState)(`grid`),[s,c]=(0,M.useState)(()=>$()),l=(0,M.useMemo)(()=>te(ne(t,e,r),s),[t,e,r,s]);return(0,N.jsxs)(`div`,{className:`flex h-full min-h-0 flex-col`,children:[(0,N.jsx)(ae,{query:r,onQueryChange:i,sort:s,onSortChange:e=>{c(e),ue(e)},view:a,onViewChange:o,scanning:n}),(0,N.jsx)(y,{className:`min-h-0 flex-1`,children:(0,N.jsxs)(`div`,{className:`px-5 pb-5`,children:[(0,N.jsx)(se,{}),(0,N.jsx)(Y,{items:l,view:a,filterLabel:re(e)})]})})]})}export{de as component};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Y as e,b as t,t as n,x as r}from"./utils-ChkMmPzE.js";import{i,r as a}from"./query-keys-CbbgiZx4.js";import{t as o}from"./useRouterState-
|
|
1
|
+
import{Y as e,b as t,t as n,x as r}from"./utils-ChkMmPzE.js";import{i,r as a}from"./query-keys-CbbgiZx4.js";import{t as o}from"./useRouterState-DDtlowFh.js";import{a as s,d as c,i as l,l as u,n as d,r as f,t as p,u as m}from"./save-reading-progress-DntE0i4u.js";import{t as h}from"./settings-2-8mnAuSax.js";import{a as g,i as _}from"./reading-BozmJNi1.js";import{t as v,y}from"./index-BS403FO9.js";import{n as b}from"./reading-progress-C5bkK-vF.js";var x=e(r(),1),S=t();function C({open:e,direction:t,onDirectionChange:n}){return e?(0,S.jsx)(`div`,{className:`absolute top-14 right-0 z-20 flex w-80 flex-col gap-4 border-b border-l border-[#25252a] bg-[#0a0a0f] p-4`,children:(0,S.jsxs)(`div`,{className:`flex items-center justify-between gap-3`,children:[(0,S.jsx)(`span`,{className:`text-sm text-[#d4c5a9]`,children:`阅读方向`}),(0,S.jsxs)(_,{type:`single`,value:t,onValueChange:e=>{(e===`ltr`||e===`rtl`)&&n(e)},children:[(0,S.jsx)(g,{value:`ltr`,"aria-label":`从左往右`,children:`左→右`}),(0,S.jsx)(g,{value:`rtl`,"aria-label":`从右往左`,children:`右→左`})]})]})}):null}function w(e){(0,x.useEffect)(()=>{let t=e.current;if(!t)return;let n=!1,r=0,i=0,a=null,o=null,s=0,c=()=>{s=0,o!==null&&(t.scrollLeft=o,o=null)},l=e=>{n&&(n=!1,a=null,s&&=(cancelAnimationFrame(s),0),o=null,t.hasPointerCapture(e)&&t.releasePointerCapture(e),t.style.cursor=`grab`)},u=e=>{e.button===0&&(n=!0,a=e.pointerId,r=e.clientX,i=t.scrollLeft,t.setPointerCapture(e.pointerId),t.style.cursor=`grabbing`)},d=e=>{!n||a!==e.pointerId||(o=i-(e.clientX-r),s||=requestAnimationFrame(c))},f=e=>{l(e.pointerId)};return t.style.cursor=`grab`,t.style.userSelect=`none`,t.addEventListener(`pointerdown`,u),t.addEventListener(`pointermove`,d),t.addEventListener(`pointerup`,f),t.addEventListener(`pointercancel`,f),()=>{s&&cancelAnimationFrame(s),t.removeEventListener(`pointerdown`,u),t.removeEventListener(`pointermove`,d),t.removeEventListener(`pointerup`,f),t.removeEventListener(`pointercancel`,f),t.style.cursor=``,t.style.userSelect=``}},[e])}function T(e,t,n){return`/static/${e}/${encodeURIComponent(t)}/${encodeURIComponent(n)}`}var E=2/3,D=.995;function O(e){return Number.isFinite(e)?Math.min(Math.max(e,0),1):0}function k(e,t){return Math.max(0,e-t)}function A(e,t,n,r=`ltr`){let i=k(t,n);if(i===0)return 0;let a=O(e/i);return r===`rtl`?1-a:a}function j(e,t,n,r=`ltr`){let i=k(t,n),a=O(e);return(r===`rtl`?1-a:a)*i}function M(e,t,n=`ltr`){e.scrollLeft=j(t,e.scrollWidth,e.clientWidth,n)}function N(e,t){return t<=1?0:Math.min(Math.max(e,0),t-1)/(t-1)}function P(e){return O(e)>=D}function F(e,t,n){if(t<=0)return 0;let r=Math.min(Math.max(e,0),t-1);return n===`rtl`?t-1-r:r}function I(e){return e<=0?200:Math.round(e*E)+8}function L(e,t,n){return e<=0||t<=0||n<=0?I(n):Math.round(e/t*n)+8}function R(e,t){return e.reduce((e,n)=>e+L(n.width,n.height,t),0)}function z({directoryId:e,title:t,pages:r,direction:i,initialScrollRatio:a,onScrollProgress:o}){let s=(0,x.useRef)(null),c=(0,x.useRef)(o),l=(0,x.useRef)(a),u=(0,x.useRef)(!0),d=(0,x.useRef)(!1),[f,p]=(0,x.useState)(0);c.current=o;let h=f>0,g=r.length,_=(0,x.useMemo)(()=>I(f),[f]);w(s);let v=(0,x.useCallback)(e=>F(e,g,i),[i,g]),y=m({horizontal:!0,count:g,enabled:h,getScrollElement:()=>s.current,estimateSize:(0,x.useCallback)(e=>{let t=r[v(e)];return t?L(t.width,t.height,f):_},[f,_,v,r]),overscan:4,initialOffset:(0,x.useCallback)(()=>{let e=s.current;if(!e||!h)return 0;let t=R(r,f);return j(l.current,t,e.clientWidth,i)},[f,i,h,r]),getItemKey:e=>r[v(e)]?.filename??e}),b=(0,x.useRef)(y.measure);b.current=y.measure;let C=(0,x.useCallback)(()=>{let e=s.current;e&&(d.current=!0,M(e,l.current,i),window.setTimeout(()=>{d.current=!1},0))},[i]);return(0,x.useLayoutEffect)(()=>{let e=s.current;if(!e)return;let t=()=>{p(e.clientHeight)};t();let n=new ResizeObserver(t);return n.observe(e),()=>{n.disconnect()}},[]),(0,x.useLayoutEffect)(()=>{l.current=a,u.current=!0},[i,a,r,t]),(0,x.useLayoutEffect)(()=>{g===0||!h||(b.current(),u.current&&C())},[C,f,i,h,g]),(0,x.useEffect)(()=>{if(g===0||!h||!u.current)return;let e=window.setTimeout(()=>{C(),u.current=!1},120);return()=>{window.clearTimeout(e)}},[C,a,h,g]),(0,x.useEffect)(()=>{let e=s.current;if(!e||!h)return;let t,n=()=>{t&&clearTimeout(t),t=setTimeout(()=>{if(d.current||u.current)return;let t=A(e.scrollLeft,e.scrollWidth,e.clientWidth,i);c.current(t)},100)};return e.addEventListener(`scroll`,n,{passive:!0}),()=>{e.removeEventListener(`scroll`,n),t&&clearTimeout(t)}},[i,h]),(0,S.jsx)(`div`,{ref:s,className:n(`reader-scrollbar-none h-full overflow-x-auto overflow-y-hidden`),children:(0,S.jsx)(`div`,{style:{width:y.getTotalSize(),height:`100%`,position:`relative`},children:y.getVirtualItems().map(n=>{let i=v(n.index),a=r[i];return a?(0,S.jsx)(`div`,{"data-index":n.index,className:`flex h-full items-center`,style:{position:`absolute`,top:0,left:0,height:`100%`,width:n.size,paddingRight:8,transform:`translateX(${n.start}px)`},children:(0,S.jsx)(`img`,{src:T(e,t,a.filename),alt:`第 ${i+1} 页`,draggable:!1,decoding:`async`,className:`h-full w-auto max-w-none select-none`})},n.key):null})})})}function B(e,t){return e?typeof e.scrollRatio==`number`?O(e.scrollRatio):typeof e.pageIndex==`number`?N(e.pageIndex,t):0:0}function V({id:e}){let t=o({select:e=>e.location.state?.readerTitle}),{data:r,isPending:m,error:g}=u(e),{settings:_,patchSettings:v}=s(`comic`),[w,T]=(0,x.useState)(0),[E,D]=(0,x.useState)(null),[k,A]=(0,x.useState)(!1),j=r&&r.type===`comic`?r:null,M=j?.pages.length??0,N=t??r?.title??`加载中...`,F=g?g instanceof Error?g.message:`加载失败`:null,I=(0,x.useMemo)(()=>b(w),[w]),L=(0,x.useCallback)((t,n)=>{j&&d(e,{type:`comic`,scrollRatio:t,completedAt:n})},[j,e]),R=(0,x.useCallback)(e=>{let t=O(e);T(t),L(t,P(t)?new Date().toISOString():null)},[L]),V=(0,x.useCallback)(e=>{D(w),v({direction:e})},[v,w]);return(0,x.useEffect)(()=>{D(null),T(0)},[e]),(0,x.useEffect)(()=>{if(!j||E!==null)return;let e=B(j.progress,M);D(e),T(e)},[j,M,E]),(0,x.useEffect)(()=>(l(e),()=>{f(e),p(e)}),[e]),(0,S.jsxs)(`div`,{className:`relative flex h-screen flex-col overflow-hidden bg-[#0a0a0f] text-[#d4c5a9]`,children:[(0,S.jsxs)(`header`,{className:`relative z-10 flex h-14 shrink-0 items-center gap-3 border-b border-[#25252a] px-4`,children:[(0,S.jsx)(i,{type:`button`,variant:`ghost`,size:`sm`,asChild:!0,children:(0,S.jsxs)(y,{to:`/`,children:[(0,S.jsx)(c,{"data-icon":`inline-start`}),`返回`]})}),(0,S.jsx)(`div`,{className:`flex-1 truncate text-center text-sm font-medium`,children:N}),(0,S.jsx)(i,{type:`button`,variant:`ghost`,size:`icon-sm`,onClick:()=>A(e=>!e),className:n(k&&`bg-[#25252a]`),children:(0,S.jsx)(h,{})})]}),(0,S.jsx)(C,{open:k,direction:_.direction,onDirectionChange:V}),(0,S.jsxs)(`div`,{className:`relative min-h-0 flex-1`,children:[m&&!j?(0,S.jsx)(`div`,{className:`flex size-full items-center justify-center`,children:(0,S.jsx)(a,{className:`h-[70vh] w-full max-w-3xl`})}):null,F?(0,S.jsx)(`div`,{className:`p-8 text-sm text-red-400`,children:F}):null,!m&&!F&&r&&r.type!==`comic`?(0,S.jsx)(`div`,{className:`p-8 text-sm text-red-400`,children:`作品类型不匹配`}):null,j&&M>0&&E!==null?(0,S.jsx)(z,{directoryId:j.directoryId,title:j.title,pages:j.pages,direction:_.direction,initialScrollRatio:E,onScrollProgress:R},`${e}-${_.direction}`):null,j&&M===0?(0,S.jsx)(`div`,{className:`flex size-full items-center justify-center text-sm text-muted-foreground`,children:`暂无页面`}):null]}),(0,S.jsxs)(`footer`,{className:`flex h-11 shrink-0 items-center justify-center border-t border-[#25252a] text-xs opacity-80`,children:[`已读 `,I,`%`]})]})}function H(){let{id:e}=v.useParams();return(0,S.jsx)(V,{id:e})}export{H as component};
|