Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae8d055689 | |||
| 1607f80a0a |
@@ -15,6 +15,12 @@ body.page-reader > #LNReader-chapter {
|
||||
column-fill: auto;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
body.page-reader.no-animation > #LNReader-chapter {
|
||||
transition: none !important;
|
||||
}
|
||||
body.page-reader.no-animation .transition-chapter {
|
||||
transition: none !important;
|
||||
}
|
||||
.transition-chapter {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
+42
-13
@@ -524,7 +524,8 @@ van.derive(() => {
|
||||
}
|
||||
});
|
||||
|
||||
window.pageReader = new (function () {
|
||||
var pageReader;
|
||||
window.pageReader = pageReader = new (function () {
|
||||
const config =
|
||||
typeof initialPageReaderConfig === 'undefined'
|
||||
? {}
|
||||
@@ -544,18 +545,21 @@ window.pageReader = new (function () {
|
||||
document.getElementsByClassName('transition-chapter')[0];
|
||||
if (!this.chapterEnding) return;
|
||||
}
|
||||
const noAnim = Boolean(
|
||||
reader.generalSettings?.val?.pageReaderDisableAnimation,
|
||||
);
|
||||
this.chapterEnding.style.transition = 'unset';
|
||||
if (bool) {
|
||||
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
||||
requestAnimationFrame(() => {
|
||||
if (!instant) {
|
||||
if (!instant && !noAnim) {
|
||||
this.chapterEnding.style.transition = 'transform 200ms';
|
||||
}
|
||||
this.chapterEnding.style.transform = 'translateX(-100vw)';
|
||||
});
|
||||
this.chapterEndingVisible.val = true;
|
||||
} else {
|
||||
if (!instant) {
|
||||
if (!instant && !noAnim) {
|
||||
this.chapterEnding.style.transition = 'transform 200ms';
|
||||
}
|
||||
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
||||
@@ -579,26 +583,35 @@ window.pageReader = new (function () {
|
||||
return;
|
||||
}
|
||||
destPage = parseInt(destPage, 10);
|
||||
const noAnim = Boolean(
|
||||
reader.generalSettings?.val?.pageReaderDisableAnimation,
|
||||
);
|
||||
if (destPage < 0) {
|
||||
if (!reader.prevChapter) return;
|
||||
document.getElementsByClassName('transition-chapter')[0].innerText =
|
||||
reader.prevChapter.name;
|
||||
this.showChapterEnding(true, false, true);
|
||||
this.showChapterEnding(true, noAnim, true);
|
||||
this.chapterNavigationPending = true;
|
||||
setTimeout(() => {
|
||||
reader.post({ type: 'prev' });
|
||||
}, 200);
|
||||
setTimeout(
|
||||
() => {
|
||||
reader.post({ type: 'prev' });
|
||||
},
|
||||
noAnim ? 0 : 200,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (destPage >= this.totalPages.val) {
|
||||
if (!reader.nextChapter) return;
|
||||
document.getElementsByClassName('transition-chapter')[0].innerText =
|
||||
reader.nextChapter.name;
|
||||
this.showChapterEnding(true);
|
||||
this.showChapterEnding(true, noAnim);
|
||||
this.chapterNavigationPending = true;
|
||||
setTimeout(() => {
|
||||
reader.post({ type: 'next' });
|
||||
}, 200);
|
||||
setTimeout(
|
||||
() => {
|
||||
reader.post({ type: 'next' });
|
||||
},
|
||||
noAnim ? 0 : 200,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.page.val = destPage;
|
||||
@@ -606,7 +619,7 @@ window.pageReader = new (function () {
|
||||
'translateX(-' + destPage * 100 + '%)';
|
||||
|
||||
const newProgress = parseInt(
|
||||
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
|
||||
((this.page.val + 1) / this.totalPages.val) * 100,
|
||||
10,
|
||||
);
|
||||
|
||||
@@ -687,6 +700,18 @@ window.pageReader = new (function () {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
van.derive(() => {
|
||||
document.body.classList.toggle(
|
||||
'no-animation',
|
||||
Boolean(reader.generalSettings.val.pageReaderDisableAnimation),
|
||||
);
|
||||
reader.chapterElement?.style.removeProperty('transition');
|
||||
const chapterEnding =
|
||||
this.chapterEnding ||
|
||||
document.getElementsByClassName('transition-chapter')[0];
|
||||
chapterEnding?.style.removeProperty('transition');
|
||||
});
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -905,7 +930,11 @@ window.addEventListener('load', () => {
|
||||
if (Math.abs(diffX) > 8 || Math.abs(diffY) > 8) {
|
||||
pageReader.ignoreClickUntil = Date.now() + 400;
|
||||
}
|
||||
reader.chapterElement.style.transition = 'transform 200ms';
|
||||
reader.chapterElement.style.transition = reader.generalSettings.val
|
||||
.pageReaderDisableAnimation
|
||||
? 'none'
|
||||
: 'transform 200ms';
|
||||
reader.chapterElement.style.removeProperty('transition');
|
||||
const diffXPercentage = diffX / reader.layoutWidth;
|
||||
if (diffXPercentage < -0.3) {
|
||||
pageReader.movePage(pageReader.page.val + 1);
|
||||
|
||||
@@ -785,7 +785,9 @@ export const getPrevChapter = async (
|
||||
.orderBy(
|
||||
desc(castInt(chapterSchema.page)),
|
||||
desc(castInt(chapterSchema.position)),
|
||||
desc(chapterSchema.id),
|
||||
)
|
||||
.limit(1)
|
||||
.get();
|
||||
};
|
||||
|
||||
@@ -814,7 +816,9 @@ export const getNextChapter = async (
|
||||
.orderBy(
|
||||
asc(castInt(chapterSchema.page)),
|
||||
asc(castInt(chapterSchema.position)),
|
||||
asc(chapterSchema.id),
|
||||
)
|
||||
.limit(1)
|
||||
.get();
|
||||
};
|
||||
|
||||
|
||||
@@ -1008,6 +1008,114 @@ describe('ChapterQueries', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('chapter neighbors', () => {
|
||||
it('limits rows transferred by both neighbor lookups to one', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
for (let position = 0; position < 5; position++) {
|
||||
await insertTestChapter(testDb, novelId, { position });
|
||||
}
|
||||
const client = testDb.sqlite as typeof testDb.sqlite & {
|
||||
executeRawAsync: typeof testDb.sqlite.executeRaw;
|
||||
};
|
||||
const executeRaw = jest.spyOn(client, 'executeRawAsync');
|
||||
try {
|
||||
expect((await getNextChapter(novelId, 0, '1'))?.position).toBe(1);
|
||||
expect((await getPrevChapter(novelId, 4, '1'))?.position).toBe(3);
|
||||
expect(executeRaw).toHaveBeenCalledTimes(2);
|
||||
for (const call of executeRaw.mock.calls) {
|
||||
expect(call[0]).toMatch(/limit \?/i);
|
||||
expect(call[1]?.at(-1)).toBe(1);
|
||||
}
|
||||
for (const result of executeRaw.mock.results) {
|
||||
expect(await result.value).toHaveLength(1);
|
||||
}
|
||||
} finally {
|
||||
executeRaw.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns no neighbor before the first or after the last chapter', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
await insertTestChapter(testDb, novelId, { page: '1', position: 0 });
|
||||
await insertTestChapter(testDb, novelId, { page: '2', position: 1 });
|
||||
|
||||
expect(await getPrevChapter(novelId, 0, '1')).toBeUndefined();
|
||||
expect(await getNextChapter(novelId, 1, '2')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('breaks duplicate position ties by id in the navigation direction', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
const firstId = await insertTestChapter(testDb, novelId, { position: 1 });
|
||||
const secondId = await insertTestChapter(testDb, novelId, {
|
||||
position: 1,
|
||||
});
|
||||
|
||||
expect((await getNextChapter(novelId, 0, '1'))?.id).toBe(firstId);
|
||||
expect((await getPrevChapter(novelId, 2, '1'))?.id).toBe(secondId);
|
||||
expect(await getNextChapter(novelId, 1, '1')).toBeUndefined();
|
||||
expect(await getPrevChapter(novelId, 1, '1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves numeric-looking page comparisons and integer page ordering', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
const page1 = await insertTestChapter(testDb, novelId, { page: '1' });
|
||||
const page02 = await insertTestChapter(testDb, novelId, { page: '02' });
|
||||
const page2 = await insertTestChapter(testDb, novelId, { page: '2' });
|
||||
const page10 = await insertTestChapter(testDb, novelId, { page: '10' });
|
||||
|
||||
// The cast parameter gives comparisons numeric affinity: '02' and '2'
|
||||
// compare equally, and page 10 sorts after page 2.
|
||||
expect((await getPrevChapter(novelId, 0, '2'))?.id).toBe(page1);
|
||||
expect((await getPrevChapter(novelId, 0, '10'))?.id).toBe(page2);
|
||||
expect((await getNextChapter(novelId, 0, '02'))?.id).toBe(page10);
|
||||
expect((await getPrevChapter(novelId, 0, '02'))?.id).toBe(page1);
|
||||
expect((await getNextChapter(novelId, 0, '1'))?.id).toBe(page02);
|
||||
});
|
||||
|
||||
it('preserves integer-cast behavior for source-defined page labels', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
await insertTestChapter(testDb, novelId, {
|
||||
page: 'Volume A',
|
||||
position: 2,
|
||||
});
|
||||
const chapterId = await insertTestChapter(testDb, novelId, {
|
||||
page: 'Volume B',
|
||||
position: 0,
|
||||
});
|
||||
|
||||
// Labels cast to zero, so existing eligibility treats both as later
|
||||
// pages and sorting resolves their equal page casts by position.
|
||||
expect(await getPrevChapter(novelId, 1, 'Volume A')).toBeUndefined();
|
||||
expect((await getNextChapter(novelId, 1, 'Volume A'))?.id).toBe(
|
||||
chapterId,
|
||||
);
|
||||
});
|
||||
|
||||
it('skips excluded chapters at the previous page boundary', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
const chapterId = await insertTestChapter(testDb, novelId, {
|
||||
page: '1',
|
||||
position: 0,
|
||||
scanlator: null,
|
||||
});
|
||||
await insertTestChapter(testDb, novelId, {
|
||||
page: '1',
|
||||
position: 1,
|
||||
scanlator: 'Excluded',
|
||||
});
|
||||
|
||||
expect((await getPrevChapter(novelId, 0, '2', ['Excluded']))?.id).toBe(
|
||||
chapterId,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPrevChapter', () => {
|
||||
it('should return previous chapter in same page', async () => {
|
||||
const testDb = getTestDb();
|
||||
|
||||
@@ -66,6 +66,8 @@ export const initialChapterGeneralSettings = {
|
||||
bionicReading: false,
|
||||
tapToScroll: false,
|
||||
TTSEnable: true,
|
||||
pageReaderInvertVolumeButtons: false,
|
||||
pageReaderDisableAnimation: false,
|
||||
};
|
||||
|
||||
export const initialChapterReaderSettings = {
|
||||
|
||||
@@ -194,6 +194,8 @@ export interface ChapterGeneralSettings {
|
||||
bionicReading: boolean;
|
||||
tapToScroll: boolean;
|
||||
TTSEnable: boolean;
|
||||
pageReaderInvertVolumeButtons: boolean;
|
||||
pageReaderDisableAnimation: boolean;
|
||||
}
|
||||
|
||||
export interface ReaderTheme {
|
||||
@@ -319,6 +321,8 @@ export const initialChapterGeneralSettings: ChapterGeneralSettings = {
|
||||
bionicReading: false,
|
||||
tapToScroll: false,
|
||||
TTSEnable: true,
|
||||
pageReaderInvertVolumeButtons: false,
|
||||
pageReaderDisableAnimation: false,
|
||||
};
|
||||
|
||||
export const initialChapterReaderSettings: ChapterReaderSettings = {
|
||||
|
||||
@@ -668,6 +668,10 @@
|
||||
"padding": "Padding",
|
||||
"pageReader": "Paged reading (Experimental)",
|
||||
"pageReaderDescription": "Split the chapter into pages instead of scrolling. Experimental — may break formatting in some novels",
|
||||
"invertVolumeButtons": "Invert volume buttons",
|
||||
"invertVolumeButtonsDescription": "Swap volume up and down buttons for previous and next page",
|
||||
"disablePageTransitions": "Disable page transition animation",
|
||||
"disablePageTransitionsDescription": "Turn pages instantly without slide animation",
|
||||
"removeExtraSpacing": "Remove extra spacing",
|
||||
"removeExtraSpacingDescription": "Collapse extra blank lines between paragraphs",
|
||||
"scrollAmount": "Scroll amount (screen height by default)",
|
||||
|
||||
@@ -662,8 +662,12 @@
|
||||
"lineHeight": "Line height",
|
||||
"navigation": "Navigation",
|
||||
"padding": "Khoảng đệm",
|
||||
"pageReader": "Paged reading (Experimental)",
|
||||
"pageReaderDescription": "Split the chapter into pages instead of scrolling. Experimental — may break formatting in some novels",
|
||||
"pageReader": "Đọc phân trang (Thử nghiệm)",
|
||||
"pageReaderDescription": "Phân chương ra thành trang thay vì scroll. Thử nghiệm - có thể làm hỏng format một số tiểu thuyết",
|
||||
"invertVolumeButtons": "Đảo nút âm lượng",
|
||||
"invertVolumeButtonsDescription": "Đảo nút tăng và giảm âm lượng cho trang trước và sau",
|
||||
"disablePageTransitions": "Tắt hoạt hình chuyển trạng thái trang",
|
||||
"disablePageTransitionsDescription": "Chuyển trang ngay lập tức mà không dùng hoạt hình trượt",
|
||||
"removeExtraSpacing": "Remove extra spacing",
|
||||
"removeExtraSpacingDescription": "Collapse extra blank lines between paragraphs",
|
||||
"scrollAmount": "Scroll amount (screen height by default)",
|
||||
|
||||
@@ -566,6 +566,10 @@ export interface StringMap {
|
||||
'readerScreen.bottomSheet.padding': 'string';
|
||||
'readerScreen.bottomSheet.pageReader': 'string';
|
||||
'readerScreen.bottomSheet.pageReaderDescription': 'string';
|
||||
'readerScreen.bottomSheet.invertVolumeButtons': 'string';
|
||||
'readerScreen.bottomSheet.invertVolumeButtonsDescription': 'string';
|
||||
'readerScreen.bottomSheet.disablePageTransitions': 'string';
|
||||
'readerScreen.bottomSheet.disablePageTransitionsDescription': 'string';
|
||||
'readerScreen.bottomSheet.removeExtraSpacing': 'string';
|
||||
'readerScreen.bottomSheet.removeExtraSpacingDescription': 'string';
|
||||
'readerScreen.bottomSheet.scrollAmount': 'string';
|
||||
|
||||
@@ -138,6 +138,19 @@ const navigationPreferences: GeneralPreference[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const pagedModePreferences: GeneralPreference[] = [
|
||||
{
|
||||
key: 'pageReaderInvertVolumeButtons',
|
||||
label: 'invertVolumeButtons',
|
||||
description: 'invertVolumeButtonsDescription',
|
||||
},
|
||||
{
|
||||
key: 'pageReaderDisableAnimation',
|
||||
label: 'disablePageTransitions',
|
||||
description: 'disablePageTransitionsDescription',
|
||||
},
|
||||
];
|
||||
|
||||
const GeneralTab: React.FC = React.memo(() => {
|
||||
const theme = useTheme();
|
||||
const { setChapterGeneralSettings, ...settings } =
|
||||
@@ -182,7 +195,18 @@ const GeneralTab: React.FC = React.memo(() => {
|
||||
<List.SubHeader theme={theme}>
|
||||
{getString('readerScreen.bottomSheet.navigation')}
|
||||
</List.SubHeader>
|
||||
{navigationPreferences.map(renderPreference)}
|
||||
{navigationPreferences.map(item => {
|
||||
if (item.key === 'pageReader') {
|
||||
return (
|
||||
<React.Fragment key={item.key}>
|
||||
{renderPreference(item)}
|
||||
{settings.pageReader &&
|
||||
pagedModePreferences.map(renderPreference)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return renderPreference(item);
|
||||
})}
|
||||
</BottomSheetScrollView>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -374,7 +374,9 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
|
||||
</head>
|
||||
<body class="${
|
||||
chapterGeneralSettings.pageReader ? 'page-reader' : ''
|
||||
}">
|
||||
} ${
|
||||
chapterGeneralSettings.pageReaderDisableAnimation ? 'no-animation' : ''
|
||||
}">
|
||||
<div class="transition-chapter" style="transform: ${
|
||||
isNextChapterScreenVisible
|
||||
? 'translateX(-100%)'
|
||||
@@ -437,8 +439,8 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
|
||||
chapter,
|
||||
chapterGeneralSettings,
|
||||
processedHtml,
|
||||
customJS,
|
||||
customCSS,
|
||||
customJS,
|
||||
customCSS,
|
||||
initialReaderSettings,
|
||||
novel,
|
||||
plugin,
|
||||
@@ -449,31 +451,31 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
onTouchStart={onTouchStart}
|
||||
style={{ backgroundColor: readerSettings.theme }}
|
||||
allowFileAccess={true}
|
||||
originWhitelist={['*']}
|
||||
scalesPageToFit={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
javaScriptEnabled={true}
|
||||
webviewDebuggingEnabled={__DEV__}
|
||||
onShouldStartLoadWithRequest={({ url }) => {
|
||||
if (isPluginIssueReportUrl(url)) {
|
||||
void Linking.openURL(url);
|
||||
return false;
|
||||
}
|
||||
if (isChapterRefreshUrl(url)) {
|
||||
refetch();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
`if (window.reader && window.reader.batteryLevel) {
|
||||
<>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
onTouchStart={onTouchStart}
|
||||
style={{ backgroundColor: readerSettings.theme }}
|
||||
allowFileAccess={true}
|
||||
originWhitelist={['*']}
|
||||
scalesPageToFit={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
javaScriptEnabled={true}
|
||||
webviewDebuggingEnabled={__DEV__}
|
||||
onShouldStartLoadWithRequest={({ url }) => {
|
||||
if (isPluginIssueReportUrl(url)) {
|
||||
void Linking.openURL(url);
|
||||
return false;
|
||||
}
|
||||
if (isChapterRefreshUrl(url)) {
|
||||
refetch();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
`if (window.reader && window.reader.batteryLevel) {
|
||||
window.reader.batteryLevel.val = ${lastKnownBatteryLevel};
|
||||
}`,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react-native';
|
||||
import useChapter from '../useChapter';
|
||||
import NativeFile from '@modules/native-file';
|
||||
import NativeVolumeButtonListener from '@modules/native-volume-button-listener';
|
||||
|
||||
const mockUseNovelActions = jest.fn();
|
||||
const mockUseChapterGeneralSettings = jest.fn();
|
||||
@@ -172,6 +173,9 @@ describe('useChapter', () => {
|
||||
autoScrollOffset: 100,
|
||||
useVolumeButtons: false,
|
||||
volumeButtonsOffset: 100,
|
||||
pageReader: false,
|
||||
pageReaderInvertVolumeButtons: false,
|
||||
pageReaderDisableAnimation: false,
|
||||
});
|
||||
mockUseLibrarySettings.mockReturnValue({ incognitoMode: false });
|
||||
mockUseAppSettings.mockReturnValue({
|
||||
@@ -423,4 +427,108 @@ describe('useChapter', () => {
|
||||
expect(result.current.chapter.id).toBe(nextChapter.id);
|
||||
expect(result.current.chapterText).toBe('SANITIZED:next body');
|
||||
});
|
||||
|
||||
it('injects scripts supporting paged and normal modes when volume buttons are pressed', async () => {
|
||||
const store = createStore();
|
||||
mockUseNovelActions.mockReturnValue(store.state);
|
||||
const mockInjectJavaScript = jest.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useChapter(
|
||||
{ current: { injectJavaScript: mockInjectJavaScript } as any },
|
||||
initialChapter,
|
||||
novel,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current.chapterContext.loading).toBe(false),
|
||||
);
|
||||
|
||||
const volumeUpCall = (
|
||||
NativeVolumeButtonListener.addListener as jest.Mock
|
||||
).mock.calls.find(([event]) => event === 'VolumeUp');
|
||||
const volumeDownCall = (
|
||||
NativeVolumeButtonListener.addListener as jest.Mock
|
||||
).mock.calls.find(([event]) => event === 'VolumeDown');
|
||||
|
||||
expect(volumeUpCall).toBeDefined();
|
||||
expect(volumeDownCall).toBeDefined();
|
||||
|
||||
// Trigger VolumeUp callback
|
||||
volumeUpCall[1]();
|
||||
expect(mockInjectJavaScript).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'window.pageReader.movePage((window.pageReader.page?.val ?? 0) - 1)',
|
||||
),
|
||||
);
|
||||
expect(mockInjectJavaScript).toHaveBeenCalledWith(
|
||||
expect.stringContaining('window.scrollBy'),
|
||||
);
|
||||
|
||||
// Trigger VolumeDown callback
|
||||
volumeDownCall[1]();
|
||||
expect(mockInjectJavaScript).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'window.pageReader.movePage((window.pageReader.page?.val ?? 0) + 1)',
|
||||
),
|
||||
);
|
||||
expect(mockInjectJavaScript).toHaveBeenCalledWith(
|
||||
expect.stringContaining('window.scrollBy'),
|
||||
);
|
||||
});
|
||||
|
||||
it('inverts volume button directions in paged mode when pageReaderInvertVolumeButtons is enabled', async () => {
|
||||
const store = createStore();
|
||||
mockUseNovelActions.mockReturnValue(store.state);
|
||||
mockUseChapterGeneralSettings.mockReturnValue({
|
||||
autoScroll: false,
|
||||
autoScrollInterval: 1,
|
||||
autoScrollOffset: 100,
|
||||
useVolumeButtons: false,
|
||||
volumeButtonsOffset: 100,
|
||||
pageReader: true,
|
||||
pageReaderInvertVolumeButtons: true,
|
||||
pageReaderDisableAnimation: false,
|
||||
});
|
||||
const mockInjectJavaScript = jest.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useChapter(
|
||||
{ current: { injectJavaScript: mockInjectJavaScript } as any },
|
||||
initialChapter,
|
||||
novel,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current.chapterContext.loading).toBe(false),
|
||||
);
|
||||
|
||||
const volumeUpCall = (
|
||||
NativeVolumeButtonListener.addListener as jest.Mock
|
||||
).mock.calls.find(([event]) => event === 'VolumeUp');
|
||||
const volumeDownCall = (
|
||||
NativeVolumeButtonListener.addListener as jest.Mock
|
||||
).mock.calls.find(([event]) => event === 'VolumeDown');
|
||||
|
||||
expect(volumeUpCall).toBeDefined();
|
||||
expect(volumeDownCall).toBeDefined();
|
||||
|
||||
// VolumeUp callback should move +1 (forward) when inverted
|
||||
volumeUpCall[1]();
|
||||
expect(mockInjectJavaScript).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'window.pageReader.movePage((window.pageReader.page?.val ?? 0) + 1)',
|
||||
),
|
||||
);
|
||||
|
||||
// VolumeDown callback should move -1 (backward) when inverted
|
||||
volumeDownCall[1]();
|
||||
expect(mockInjectJavaScript).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'window.pageReader.movePage((window.pageReader.page?.val ?? 0) - 1)',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,6 +74,8 @@ export default function useChapter(
|
||||
autoScrollOffset,
|
||||
useVolumeButtons,
|
||||
volumeButtonsOffset,
|
||||
pageReader,
|
||||
pageReaderInvertVolumeButtons,
|
||||
} = useChapterGeneralSettings();
|
||||
const { incognitoMode } = useLibrarySettings();
|
||||
const { timeTrackingEnabled, inactivityTimeoutMs } = useAppSettings();
|
||||
@@ -124,15 +126,32 @@ export default function useChapter(
|
||||
Math.round(Dimensions.get('window').height * 0.75),
|
||||
);
|
||||
|
||||
const volumeUpDelta = pageReaderInvertVolumeButtons ? 1 : -1;
|
||||
const volumeDownDelta = pageReaderInvertVolumeButtons ? -1 : 1;
|
||||
|
||||
useEventListener(NativeVolumeButtonListener, 'VolumeUp', () => {
|
||||
webViewRef.current?.injectJavaScript(`(()=>{
|
||||
window.scrollBy({top: -${volumeButtonOffset}, behavior: 'smooth'})
|
||||
const isPaged = ${Boolean(pageReader)};
|
||||
if (isPaged && window.pageReader) {
|
||||
window.pageReader.movePage((window.pageReader.page?.val ?? 0) ${
|
||||
volumeUpDelta >= 0 ? '+' : '-'
|
||||
} 1);
|
||||
} else {
|
||||
window.scrollBy({top: -${volumeButtonOffset}, behavior: 'smooth'});
|
||||
}
|
||||
})()`);
|
||||
});
|
||||
|
||||
useEventListener(NativeVolumeButtonListener, 'VolumeDown', () => {
|
||||
webViewRef.current?.injectJavaScript(`(()=>{
|
||||
window.scrollBy({top: ${volumeButtonOffset}, behavior: 'smooth'})
|
||||
const isPaged = ${Boolean(pageReader)};
|
||||
if (isPaged && window.pageReader) {
|
||||
window.pageReader.movePage((window.pageReader.page?.val ?? 0) ${
|
||||
volumeDownDelta >= 0 ? '+' : '-'
|
||||
} 1);
|
||||
} else {
|
||||
window.scrollBy({top: ${volumeButtonOffset}, behavior: 'smooth'});
|
||||
}
|
||||
})()`);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ const NavigationTab: React.FC = () => {
|
||||
verticalSeekbar = true,
|
||||
swipeGestures = false,
|
||||
pageReader = false,
|
||||
pageReaderInvertVolumeButtons = false,
|
||||
pageReaderDisableAnimation = false,
|
||||
autoScroll = false,
|
||||
autoScrollInterval = 10,
|
||||
autoScrollOffset = null,
|
||||
@@ -119,6 +121,38 @@ const NavigationTab: React.FC = () => {
|
||||
onPress={() => setChapterGeneralSettings({ pageReader: !pageReader })}
|
||||
theme={theme}
|
||||
/>
|
||||
{pageReader && (
|
||||
<>
|
||||
<SettingSwitch
|
||||
label={getString('readerScreen.bottomSheet.invertVolumeButtons')}
|
||||
description={getString(
|
||||
'readerScreen.bottomSheet.invertVolumeButtonsDescription',
|
||||
)}
|
||||
value={pageReaderInvertVolumeButtons}
|
||||
onPress={() =>
|
||||
setChapterGeneralSettings({
|
||||
pageReaderInvertVolumeButtons: !pageReaderInvertVolumeButtons,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SettingSwitch
|
||||
label={getString(
|
||||
'readerScreen.bottomSheet.disablePageTransitions',
|
||||
)}
|
||||
description={getString(
|
||||
'readerScreen.bottomSheet.disablePageTransitionsDescription',
|
||||
)}
|
||||
value={pageReaderDisableAnimation}
|
||||
onPress={() =>
|
||||
setChapterGeneralSettings({
|
||||
pageReaderDisableAnimation: !pageReaderDisableAnimation,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
|
||||
Reference in New Issue
Block a user