feat: remaining time in TTS mode (#3300)

* feat: add test

* chore: update test

* feat: add translations

* feat: add tts time

* feat: add tts time components

* feat: update test

* feat: fixes and update ui components

* refactor: revert translations and ui
This commit is contained in:
Mohammed Efaz
2026-02-14 22:42:37 +06:00
committed by GitHub
parent e75a3d254e
commit b9f6578127
3 changed files with 128 additions and 0 deletions
@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest';
import { estimateTTSTime } from '@/utils/ttsTime';
import { BookProgress } from '@/types/book';
const createProgress = (values: {
sectionMin: number;
totalMin: number;
pageCurrent: number;
pageTotal: number;
}) => {
return {
timeinfo: {
section: values.sectionMin,
total: values.totalMin,
},
pageinfo: {
current: values.pageCurrent,
total: values.pageTotal,
},
} as BookProgress;
};
describe('estimateTTSTime', () => {
it('estimates chapter and book remaining time at 1x', () => {
const progress = createProgress({
sectionMin: 10,
totalMin: 60,
pageCurrent: 49,
pageTotal: 100,
});
const result = estimateTTSTime(progress, 1, 1_700_000_000_000);
expect(result.chapterRemainingSec).toBe(600);
expect(result.bookRemainingSec).toBe(3600);
expect(result.finishAtTimestamp).toBe(1_700_003_600_000);
});
it('scales estimates by playback rate', () => {
const progress = createProgress({
sectionMin: 12,
totalMin: 90,
pageCurrent: 44,
pageTotal: 90,
});
const result = estimateTTSTime(progress, 1.5, 1000);
expect(result.chapterRemainingSec).toBe(480);
expect(result.bookRemainingSec).toBe(3600);
expect(result.finishAtTimestamp).toBe(3_601_000);
});
it('falls back safely for missing progress', () => {
const result = estimateTTSTime(null, 2, 1000);
expect(result.chapterRemainingSec).toBeNull();
expect(result.bookRemainingSec).toBeNull();
expect(result.finishAtTimestamp).toBeNull();
});
it('uses book remaining to compute finish time', () => {
const progress = createProgress({
sectionMin: 1,
totalMin: 2,
pageCurrent: 99,
pageTotal: 100,
});
const result = estimateTTSTime(progress, 1, 1000);
expect(result.bookRemainingSec).toBe(120);
expect(result.finishAtTimestamp).toBe(121000);
});
});
@@ -16,6 +16,7 @@ import { throttle } from '@/utils/throttle';
import { isCfiInLocation } from '@/utils/cfi';
import { getLocale } from '@/utils/misc';
import { invokeUseBackgroundAudio } from '@/utils/bridge';
import { estimateTTSTime } from '@/utils/ttsTime';
import { useTTSMediaSession } from './useTTSMediaSession';
interface UseTTSControlProps {
@@ -224,6 +225,11 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
};
const viewSettings = getViewSettings(bookKey);
const ttsTime = useMemo(() => {
const rate = viewSettings?.ttsRate ?? 1;
return estimateTTSTime(progress, rate);
}, [progress, viewSettings?.ttsRate]);
const getTTSTargetLang = useCallback((): string | null => {
const vs = getViewSettings(bookKey);
const ttsReadAloudText = vs?.ttsReadAloudText;
@@ -641,6 +647,9 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
showBackToCurrentTTSLocation,
timeoutOption,
timeoutTimestamp,
chapterRemainingSec: ttsTime.chapterRemainingSec,
bookRemainingSec: ttsTime.bookRemainingSec,
finishAtTimestamp: ttsTime.finishAtTimestamp,
handleTogglePlay,
handleBackward,
handleForward,
+44
View File
@@ -0,0 +1,44 @@
import { BookProgress } from '@/types/book';
const toSeconds = (minutes?: number) => {
if (typeof minutes !== 'number' || !Number.isFinite(minutes) || minutes < 0) return null;
return Math.round(minutes * 60);
};
const getSafeRate = (rate?: number) => {
if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) return 1;
return rate;
};
const applyRate = (seconds: number | null, rate: number) => {
if (seconds === null) return null;
return Math.max(0, Math.round(seconds / rate));
};
export type TTSTimeEstimate = {
chapterRemainingSec: number | null;
bookRemainingSec: number | null;
finishAtTimestamp: number | null;
};
export const estimateTTSTime = (
progress: BookProgress | null,
rate?: number,
now = Date.now(),
): TTSTimeEstimate => {
const safeRate = getSafeRate(rate);
const chapterRemainingBaseSec = toSeconds(progress?.timeinfo?.section);
const bookRemainingBaseSec = toSeconds(progress?.timeinfo?.total);
const chapterRemainingSec = applyRate(chapterRemainingBaseSec, safeRate);
const bookRemainingSec = applyRate(bookRemainingBaseSec, safeRate);
const finishAtTimestamp =
bookRemainingSec !== null && bookRemainingSec > 0 ? now + bookRemainingSec * 1000 : null;
return {
chapterRemainingSec,
bookRemainingSec,
finishAtTimestamp,
};
};