fix(cbz): ComicInfo metadata + CBZ page count + WebDAV i18n (#4282)
* fix(cbz,i18n): ComicInfo metadata + CBZ page count + WebDAV i18n Closes #4253 (ComicInfo.xml not read) and #4255 (CBZ shows "1 page left"). CBZ / ComicInfo (foliate-js submodule + Readest derivation): - comic-book.js: find ComicInfo.xml in subdirectories too, parse description / subject / identifier / published / series fields beyond the prior name+position pair. Series Count populates the canonical `belongsTo.series.total`; no top-level duplication. - bookService.ts / readerStore.ts: derive `metadata.seriesTotal` from `belongsTo.series.total` in parallel to the existing series / seriesIndex derivation. - ProgressBar / FooterBar / DesktopFooterBar: drop the hard-coded `pagesLeft = 1` for fixed-layout books and compute it from `section.total - section.current`. FooterBar uses `FIXED_LAYOUT_FORMATS.has(bookFormat)` so CBZ picks `section` (correct image count) instead of `pageinfo` (locations). - ProgressBar: switch the remaining-pages text to "in book" for fixed-layout titles (no chapter structure) and keep "in chapter" for reflowable books. WebDAV refactor for translation coverage: - WebDAVBrowsePane / SyncHistoryPanel called `t(...)` (passed as a prop) instead of `_(...)`. The i18next-scanner only looks for `_`, so ~53 strings were unreachable and shipped in English to every locale. Switched both components to call `useTranslation()` themselves; helpers that aren't React FCs take `_: TranslationFunc` so the scanner sees the literal calls. - WebDAVClient.checkConnection now returns a `code` discriminator (`SERVER_URL_REQUIRED` / `AUTH_FAILED` / `ROOT_NOT_FOUND` / `UNEXPECTED_STATUS` / `NETWORK`); raw English `message` is reserved for the dev console. New `formatConnectError` and `formatSyncError` helpers in WebDAVForm translate via a switch where each branch is a literal `_('...')`. Same treatment for the sync-failure path that previously surfaced raw e.message. - "Syncing 0 / {{total}}" is now parameterized as "Syncing {{n}} / {{total}}" with n=0 at startup so the digit formats naturally and the template can be reused mid-sync. - "Cleanup · {{count}} book(s)" hard-coded options used unsupported ternary; rewrote as plural-aware key. i18n scanner fix (i18next-scanner.config.cjs): - vinyl-fs walked into directories whose names end in source-file extensions (Next.js route folder `runtime-config.js/`, Playwright screenshot folder `*.test.tsx/`) and crashed with EISDIR. Resolved by expanding globs via `fs.globSync` and filtering to files only before handing to the scanner. TypeScript-syntax sites that broke esprima during extraction: - WebDAVBrowsePane / WebDAVForm: `(e as Error).message` and `failed[0]!.title` inside `_(..., options)` arguments. Replaced with `e instanceof Error ? e.message : String(e)` and `failed[0]?.title ?? ''` — also runtime-safer. User-facing em-dash cleanup: - Removed em-dashes from translation keys across SyncHistoryPanel / WebDAVForm / WebDAVBrowsePane / SyncPassphraseSection / send/page / replicaCryptoMiddleware / AIPanel. Tagline in `layout.tsx` kept. Locale translations: - ~2400 translations applied across all 33 locales for the keys that were either newly extractable, freshly worded, or pre-existing but untranslated. Zero `__STRING_NOT_TRANSLATED__` remain after the run. Misc: - next.config.mjs: drop `eslint.ignoreDuringBuilds: true` so build runs the same lint as CI. - Collection type: add `total?: string` for ComicInfo series count. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(test): fix vitest invocation, run with 4 workers `pnpm test:pr:web` was chaining `pnpm test -- --watch=false`, which pnpm expanded into: dotenv -e .env -e .env.test.local -- vitest -- --watch=false The second `--` made vitest treat `--watch=false` as a positional file pattern, not a flag. Vitest then fell back to defaults (in CI's non-TTY env that still meant a one-shot run, so the suite passed), but the worker pool was effectively serialized for big chunks of the 243-file run — wall ~90 s on a 4-vCPU runner where the parallel-sum of phases was ~236 s (≈2.6× effective parallelism). Replace the chained pnpm invocation with a direct call to `vitest run --maxWorkers=4`, matching the 4 vCPUs the GH Actions ubuntu-latest runner provides. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { Insets } from '@/types/misc';
|
||||
import type { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -55,26 +55,47 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
|
||||
|
||||
const { page: current = 0, pages: total = 0 } = view?.renderer || {};
|
||||
const pagesLeft = bookData?.isFixedLayout
|
||||
? 1
|
||||
? pageInfo
|
||||
? Math.max(pageInfo.total - pageInfo.current, 1)
|
||||
: 0
|
||||
: Math.min(Math.max(total - current, 1), pageInfo ? pageInfo.total - pageInfo.current : total);
|
||||
const showPagesLeft = total > 0 || bookData?.isFixedLayout;
|
||||
const showPagesLeft = pagesLeft > 0 && (total > 0 || !!bookData?.isFixedLayout);
|
||||
// Fixed-layout formats (CBZ, PDF) have no chapter structure — every page is
|
||||
// its own section — so the remaining count is the whole book, not a chapter.
|
||||
const remainingInBook = !!bookData?.isFixedLayout;
|
||||
const timeLeftStr = showPagesLeft
|
||||
? _('{{time}} min left in chapter', {
|
||||
time: formatNumber(
|
||||
Math.round((pagesLeft * SIZE_PER_LOC) / SIZE_PER_TIME_UNIT),
|
||||
localize,
|
||||
lang,
|
||||
),
|
||||
})
|
||||
? remainingInBook
|
||||
? _('{{time}} min left in book', {
|
||||
time: formatNumber(
|
||||
Math.round((pagesLeft * SIZE_PER_LOC) / SIZE_PER_TIME_UNIT),
|
||||
localize,
|
||||
lang,
|
||||
),
|
||||
})
|
||||
: _('{{time}} min left in chapter', {
|
||||
time: formatNumber(
|
||||
Math.round((pagesLeft * SIZE_PER_LOC) / SIZE_PER_TIME_UNIT),
|
||||
localize,
|
||||
lang,
|
||||
),
|
||||
})
|
||||
: '';
|
||||
const pagesLeftStr = showPagesLeft
|
||||
? localize
|
||||
? _('{{number}} pages left in chapter', {
|
||||
number: formatNumber(pagesLeft, localize, lang),
|
||||
})
|
||||
: _('{{count}} pages left in chapter', {
|
||||
count: pagesLeft,
|
||||
})
|
||||
? remainingInBook
|
||||
? _('{{number}} pages left in book', {
|
||||
number: formatNumber(pagesLeft, localize, lang),
|
||||
})
|
||||
: _('{{number}} pages left in chapter', {
|
||||
number: formatNumber(pagesLeft, localize, lang),
|
||||
})
|
||||
: remainingInBook
|
||||
? _('{{count}} pages left in book', {
|
||||
count: pagesLeft,
|
||||
})
|
||||
: _('{{count}} pages left in chapter', {
|
||||
count: pagesLeft,
|
||||
})
|
||||
: '';
|
||||
|
||||
const [progressBarMode, setProgressBarMode] = useState<string>(viewSettings.progressInfoMode);
|
||||
@@ -225,12 +246,27 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
|
||||
) : viewSettings.showRemainingPages && showPagesLeft ? (
|
||||
<span className='text-start'>
|
||||
{localize ? (
|
||||
<Trans
|
||||
i18nKey='{{number}} pages left in chapter'
|
||||
values={{ number: formatNumber(pagesLeft, localize, lang) }}
|
||||
>
|
||||
<span className='pages-left-number'>{'{{number}}'}</span>
|
||||
<span className='pages-left-label'>{' pages left in chapter'}</span>
|
||||
remainingInBook ? (
|
||||
<Trans
|
||||
i18nKey='{{number}} pages left in book'
|
||||
values={{ number: formatNumber(pagesLeft, localize, lang) }}
|
||||
>
|
||||
<span className='pages-left-number'>{'{{number}}'}</span>
|
||||
<span className='pages-left-label'>{' pages left in book'}</span>
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey='{{number}} pages left in chapter'
|
||||
values={{ number: formatNumber(pagesLeft, localize, lang) }}
|
||||
>
|
||||
<span className='pages-left-number'>{'{{number}}'}</span>
|
||||
<span className='pages-left-label'>{' pages left in chapter'}</span>
|
||||
</Trans>
|
||||
)
|
||||
) : remainingInBook ? (
|
||||
<Trans i18nKey='{{count}} pages left in book' count={pagesLeft}>
|
||||
<span className='pages-left-number'>{'{{count}}'}</span>
|
||||
<span className='pages-left-label'>{' pages left in book'}</span>
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans i18nKey='{{count}} pages left in chapter' count={pagesLeft}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { formatProgress } from '@/utils/progress';
|
||||
import { FooterBarChildProps } from './types';
|
||||
import type { FooterBarChildProps } from './types';
|
||||
import { getNavigationIcon } from './utils';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ import { useSpatialNavigation } from '@/app/reader/hooks/useSpatialNavigation';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { FIXED_LAYOUT_FORMATS } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useDeviceControlStore } from '@/store/deviceStore';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { FooterBarProps, NavigationHandlers, FooterBarChildProps } from './types';
|
||||
import type { FooterBarProps, NavigationHandlers, FooterBarChildProps } from './types';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { RSVPControl } from '../rsvp';
|
||||
import MobileFooterBar from './MobileFooterBar';
|
||||
@@ -46,7 +47,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
const pointerInDoc = docs.some(({ doc }) => doc?.body?.style.cursor === 'pointer');
|
||||
|
||||
const progressInfo = useMemo(
|
||||
() => (bookFormat === 'PDF' ? section : pageinfo),
|
||||
() => (FIXED_LAYOUT_FORMATS.has(bookFormat) ? section : pageinfo),
|
||||
[bookFormat, section, pageinfo],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PageInfo } from '@/types/book';
|
||||
import type { BookFormat, PageInfo } from '@/types/book';
|
||||
import { Insets } from '@/types/misc';
|
||||
|
||||
export interface FooterBarProps {
|
||||
bookKey: string;
|
||||
bookFormat: string;
|
||||
bookFormat: BookFormat;
|
||||
section?: PageInfo;
|
||||
pageinfo?: PageInfo;
|
||||
isHoveredAnim: boolean;
|
||||
|
||||
Reference in New Issue
Block a user