Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae8d055689 | |||
| 1607f80a0a |
@@ -15,6 +15,12 @@ body.page-reader > #LNReader-chapter {
|
|||||||
column-fill: auto;
|
column-fill: auto;
|
||||||
transition: transform 200ms;
|
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 {
|
.transition-chapter {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
|||||||
+42
-13
@@ -524,7 +524,8 @@ van.derive(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.pageReader = new (function () {
|
var pageReader;
|
||||||
|
window.pageReader = pageReader = new (function () {
|
||||||
const config =
|
const config =
|
||||||
typeof initialPageReaderConfig === 'undefined'
|
typeof initialPageReaderConfig === 'undefined'
|
||||||
? {}
|
? {}
|
||||||
@@ -544,18 +545,21 @@ window.pageReader = new (function () {
|
|||||||
document.getElementsByClassName('transition-chapter')[0];
|
document.getElementsByClassName('transition-chapter')[0];
|
||||||
if (!this.chapterEnding) return;
|
if (!this.chapterEnding) return;
|
||||||
}
|
}
|
||||||
|
const noAnim = Boolean(
|
||||||
|
reader.generalSettings?.val?.pageReaderDisableAnimation,
|
||||||
|
);
|
||||||
this.chapterEnding.style.transition = 'unset';
|
this.chapterEnding.style.transition = 'unset';
|
||||||
if (bool) {
|
if (bool) {
|
||||||
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (!instant) {
|
if (!instant && !noAnim) {
|
||||||
this.chapterEnding.style.transition = 'transform 200ms';
|
this.chapterEnding.style.transition = 'transform 200ms';
|
||||||
}
|
}
|
||||||
this.chapterEnding.style.transform = 'translateX(-100vw)';
|
this.chapterEnding.style.transform = 'translateX(-100vw)';
|
||||||
});
|
});
|
||||||
this.chapterEndingVisible.val = true;
|
this.chapterEndingVisible.val = true;
|
||||||
} else {
|
} else {
|
||||||
if (!instant) {
|
if (!instant && !noAnim) {
|
||||||
this.chapterEnding.style.transition = 'transform 200ms';
|
this.chapterEnding.style.transition = 'transform 200ms';
|
||||||
}
|
}
|
||||||
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
||||||
@@ -579,26 +583,35 @@ window.pageReader = new (function () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
destPage = parseInt(destPage, 10);
|
destPage = parseInt(destPage, 10);
|
||||||
|
const noAnim = Boolean(
|
||||||
|
reader.generalSettings?.val?.pageReaderDisableAnimation,
|
||||||
|
);
|
||||||
if (destPage < 0) {
|
if (destPage < 0) {
|
||||||
if (!reader.prevChapter) return;
|
if (!reader.prevChapter) return;
|
||||||
document.getElementsByClassName('transition-chapter')[0].innerText =
|
document.getElementsByClassName('transition-chapter')[0].innerText =
|
||||||
reader.prevChapter.name;
|
reader.prevChapter.name;
|
||||||
this.showChapterEnding(true, false, true);
|
this.showChapterEnding(true, noAnim, true);
|
||||||
this.chapterNavigationPending = true;
|
this.chapterNavigationPending = true;
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
reader.post({ type: 'prev' });
|
() => {
|
||||||
}, 200);
|
reader.post({ type: 'prev' });
|
||||||
|
},
|
||||||
|
noAnim ? 0 : 200,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (destPage >= this.totalPages.val) {
|
if (destPage >= this.totalPages.val) {
|
||||||
if (!reader.nextChapter) return;
|
if (!reader.nextChapter) return;
|
||||||
document.getElementsByClassName('transition-chapter')[0].innerText =
|
document.getElementsByClassName('transition-chapter')[0].innerText =
|
||||||
reader.nextChapter.name;
|
reader.nextChapter.name;
|
||||||
this.showChapterEnding(true);
|
this.showChapterEnding(true, noAnim);
|
||||||
this.chapterNavigationPending = true;
|
this.chapterNavigationPending = true;
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
reader.post({ type: 'next' });
|
() => {
|
||||||
}, 200);
|
reader.post({ type: 'next' });
|
||||||
|
},
|
||||||
|
noAnim ? 0 : 200,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.page.val = destPage;
|
this.page.val = destPage;
|
||||||
@@ -606,7 +619,7 @@ window.pageReader = new (function () {
|
|||||||
'translateX(-' + destPage * 100 + '%)';
|
'translateX(-' + destPage * 100 + '%)';
|
||||||
|
|
||||||
const newProgress = parseInt(
|
const newProgress = parseInt(
|
||||||
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
|
((this.page.val + 1) / this.totalPages.val) * 100,
|
||||||
10,
|
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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -905,7 +930,11 @@ window.addEventListener('load', () => {
|
|||||||
if (Math.abs(diffX) > 8 || Math.abs(diffY) > 8) {
|
if (Math.abs(diffX) > 8 || Math.abs(diffY) > 8) {
|
||||||
pageReader.ignoreClickUntil = Date.now() + 400;
|
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;
|
const diffXPercentage = diffX / reader.layoutWidth;
|
||||||
if (diffXPercentage < -0.3) {
|
if (diffXPercentage < -0.3) {
|
||||||
pageReader.movePage(pageReader.page.val + 1);
|
pageReader.movePage(pageReader.page.val + 1);
|
||||||
|
|||||||
@@ -785,7 +785,9 @@ export const getPrevChapter = async (
|
|||||||
.orderBy(
|
.orderBy(
|
||||||
desc(castInt(chapterSchema.page)),
|
desc(castInt(chapterSchema.page)),
|
||||||
desc(castInt(chapterSchema.position)),
|
desc(castInt(chapterSchema.position)),
|
||||||
|
desc(chapterSchema.id),
|
||||||
)
|
)
|
||||||
|
.limit(1)
|
||||||
.get();
|
.get();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -814,7 +816,9 @@ export const getNextChapter = async (
|
|||||||
.orderBy(
|
.orderBy(
|
||||||
asc(castInt(chapterSchema.page)),
|
asc(castInt(chapterSchema.page)),
|
||||||
asc(castInt(chapterSchema.position)),
|
asc(castInt(chapterSchema.position)),
|
||||||
|
asc(chapterSchema.id),
|
||||||
)
|
)
|
||||||
|
.limit(1)
|
||||||
.get();
|
.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', () => {
|
describe('getPrevChapter', () => {
|
||||||
it('should return previous chapter in same page', async () => {
|
it('should return previous chapter in same page', async () => {
|
||||||
const testDb = getTestDb();
|
const testDb = getTestDb();
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export const initialChapterGeneralSettings = {
|
|||||||
bionicReading: false,
|
bionicReading: false,
|
||||||
tapToScroll: false,
|
tapToScroll: false,
|
||||||
TTSEnable: true,
|
TTSEnable: true,
|
||||||
|
pageReaderInvertVolumeButtons: false,
|
||||||
|
pageReaderDisableAnimation: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialChapterReaderSettings = {
|
export const initialChapterReaderSettings = {
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ export interface ChapterGeneralSettings {
|
|||||||
bionicReading: boolean;
|
bionicReading: boolean;
|
||||||
tapToScroll: boolean;
|
tapToScroll: boolean;
|
||||||
TTSEnable: boolean;
|
TTSEnable: boolean;
|
||||||
|
pageReaderInvertVolumeButtons: boolean;
|
||||||
|
pageReaderDisableAnimation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReaderTheme {
|
export interface ReaderTheme {
|
||||||
@@ -319,6 +321,8 @@ export const initialChapterGeneralSettings: ChapterGeneralSettings = {
|
|||||||
bionicReading: false,
|
bionicReading: false,
|
||||||
tapToScroll: false,
|
tapToScroll: false,
|
||||||
TTSEnable: true,
|
TTSEnable: true,
|
||||||
|
pageReaderInvertVolumeButtons: false,
|
||||||
|
pageReaderDisableAnimation: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialChapterReaderSettings: ChapterReaderSettings = {
|
export const initialChapterReaderSettings: ChapterReaderSettings = {
|
||||||
|
|||||||
@@ -668,6 +668,10 @@
|
|||||||
"padding": "Padding",
|
"padding": "Padding",
|
||||||
"pageReader": "Paged reading (Experimental)",
|
"pageReader": "Paged reading (Experimental)",
|
||||||
"pageReaderDescription": "Split the chapter into pages instead of scrolling. Experimental — may break formatting in some novels",
|
"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",
|
"removeExtraSpacing": "Remove extra spacing",
|
||||||
"removeExtraSpacingDescription": "Collapse extra blank lines between paragraphs",
|
"removeExtraSpacingDescription": "Collapse extra blank lines between paragraphs",
|
||||||
"scrollAmount": "Scroll amount (screen height by default)",
|
"scrollAmount": "Scroll amount (screen height by default)",
|
||||||
|
|||||||
@@ -662,8 +662,12 @@
|
|||||||
"lineHeight": "Line height",
|
"lineHeight": "Line height",
|
||||||
"navigation": "Navigation",
|
"navigation": "Navigation",
|
||||||
"padding": "Khoảng đệm",
|
"padding": "Khoảng đệm",
|
||||||
"pageReader": "Paged reading (Experimental)",
|
"pageReader": "Đọc phân trang (Thử nghiệm)",
|
||||||
"pageReaderDescription": "Split the chapter into pages instead of scrolling. Experimental — may break formatting in some novels",
|
"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",
|
"removeExtraSpacing": "Remove extra spacing",
|
||||||
"removeExtraSpacingDescription": "Collapse extra blank lines between paragraphs",
|
"removeExtraSpacingDescription": "Collapse extra blank lines between paragraphs",
|
||||||
"scrollAmount": "Scroll amount (screen height by default)",
|
"scrollAmount": "Scroll amount (screen height by default)",
|
||||||
|
|||||||
@@ -566,6 +566,10 @@ export interface StringMap {
|
|||||||
'readerScreen.bottomSheet.padding': 'string';
|
'readerScreen.bottomSheet.padding': 'string';
|
||||||
'readerScreen.bottomSheet.pageReader': 'string';
|
'readerScreen.bottomSheet.pageReader': 'string';
|
||||||
'readerScreen.bottomSheet.pageReaderDescription': '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.removeExtraSpacing': 'string';
|
||||||
'readerScreen.bottomSheet.removeExtraSpacingDescription': 'string';
|
'readerScreen.bottomSheet.removeExtraSpacingDescription': 'string';
|
||||||
'readerScreen.bottomSheet.scrollAmount': '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 GeneralTab: React.FC = React.memo(() => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { setChapterGeneralSettings, ...settings } =
|
const { setChapterGeneralSettings, ...settings } =
|
||||||
@@ -182,7 +195,18 @@ const GeneralTab: React.FC = React.memo(() => {
|
|||||||
<List.SubHeader theme={theme}>
|
<List.SubHeader theme={theme}>
|
||||||
{getString('readerScreen.bottomSheet.navigation')}
|
{getString('readerScreen.bottomSheet.navigation')}
|
||||||
</List.SubHeader>
|
</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>
|
</BottomSheetScrollView>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -374,7 +374,9 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
|
|||||||
</head>
|
</head>
|
||||||
<body class="${
|
<body class="${
|
||||||
chapterGeneralSettings.pageReader ? 'page-reader' : ''
|
chapterGeneralSettings.pageReader ? 'page-reader' : ''
|
||||||
}">
|
} ${
|
||||||
|
chapterGeneralSettings.pageReaderDisableAnimation ? 'no-animation' : ''
|
||||||
|
}">
|
||||||
<div class="transition-chapter" style="transform: ${
|
<div class="transition-chapter" style="transform: ${
|
||||||
isNextChapterScreenVisible
|
isNextChapterScreenVisible
|
||||||
? 'translateX(-100%)'
|
? 'translateX(-100%)'
|
||||||
@@ -437,8 +439,8 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
|
|||||||
chapter,
|
chapter,
|
||||||
chapterGeneralSettings,
|
chapterGeneralSettings,
|
||||||
processedHtml,
|
processedHtml,
|
||||||
customJS,
|
customJS,
|
||||||
customCSS,
|
customCSS,
|
||||||
initialReaderSettings,
|
initialReaderSettings,
|
||||||
novel,
|
novel,
|
||||||
plugin,
|
plugin,
|
||||||
@@ -449,31 +451,31 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WebView
|
<WebView
|
||||||
ref={webViewRef}
|
ref={webViewRef}
|
||||||
onTouchStart={onTouchStart}
|
onTouchStart={onTouchStart}
|
||||||
style={{ backgroundColor: readerSettings.theme }}
|
style={{ backgroundColor: readerSettings.theme }}
|
||||||
allowFileAccess={true}
|
allowFileAccess={true}
|
||||||
originWhitelist={['*']}
|
originWhitelist={['*']}
|
||||||
scalesPageToFit={true}
|
scalesPageToFit={true}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
javaScriptEnabled={true}
|
javaScriptEnabled={true}
|
||||||
webviewDebuggingEnabled={__DEV__}
|
webviewDebuggingEnabled={__DEV__}
|
||||||
onShouldStartLoadWithRequest={({ url }) => {
|
onShouldStartLoadWithRequest={({ url }) => {
|
||||||
if (isPluginIssueReportUrl(url)) {
|
if (isPluginIssueReportUrl(url)) {
|
||||||
void Linking.openURL(url);
|
void Linking.openURL(url);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isChapterRefreshUrl(url)) {
|
if (isChapterRefreshUrl(url)) {
|
||||||
refetch();
|
refetch();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
onLoadEnd={() => {
|
onLoadEnd={() => {
|
||||||
webViewRef.current?.injectJavaScript(
|
webViewRef.current?.injectJavaScript(
|
||||||
`if (window.reader && window.reader.batteryLevel) {
|
`if (window.reader && window.reader.batteryLevel) {
|
||||||
window.reader.batteryLevel.val = ${lastKnownBatteryLevel};
|
window.reader.batteryLevel.val = ${lastKnownBatteryLevel};
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { act, renderHook, waitFor } from '@testing-library/react-native';
|
import { act, renderHook, waitFor } from '@testing-library/react-native';
|
||||||
import useChapter from '../useChapter';
|
import useChapter from '../useChapter';
|
||||||
import NativeFile from '@modules/native-file';
|
import NativeFile from '@modules/native-file';
|
||||||
|
import NativeVolumeButtonListener from '@modules/native-volume-button-listener';
|
||||||
|
|
||||||
const mockUseNovelActions = jest.fn();
|
const mockUseNovelActions = jest.fn();
|
||||||
const mockUseChapterGeneralSettings = jest.fn();
|
const mockUseChapterGeneralSettings = jest.fn();
|
||||||
@@ -172,6 +173,9 @@ describe('useChapter', () => {
|
|||||||
autoScrollOffset: 100,
|
autoScrollOffset: 100,
|
||||||
useVolumeButtons: false,
|
useVolumeButtons: false,
|
||||||
volumeButtonsOffset: 100,
|
volumeButtonsOffset: 100,
|
||||||
|
pageReader: false,
|
||||||
|
pageReaderInvertVolumeButtons: false,
|
||||||
|
pageReaderDisableAnimation: false,
|
||||||
});
|
});
|
||||||
mockUseLibrarySettings.mockReturnValue({ incognitoMode: false });
|
mockUseLibrarySettings.mockReturnValue({ incognitoMode: false });
|
||||||
mockUseAppSettings.mockReturnValue({
|
mockUseAppSettings.mockReturnValue({
|
||||||
@@ -423,4 +427,108 @@ describe('useChapter', () => {
|
|||||||
expect(result.current.chapter.id).toBe(nextChapter.id);
|
expect(result.current.chapter.id).toBe(nextChapter.id);
|
||||||
expect(result.current.chapterText).toBe('SANITIZED:next body');
|
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,
|
autoScrollOffset,
|
||||||
useVolumeButtons,
|
useVolumeButtons,
|
||||||
volumeButtonsOffset,
|
volumeButtonsOffset,
|
||||||
|
pageReader,
|
||||||
|
pageReaderInvertVolumeButtons,
|
||||||
} = useChapterGeneralSettings();
|
} = useChapterGeneralSettings();
|
||||||
const { incognitoMode } = useLibrarySettings();
|
const { incognitoMode } = useLibrarySettings();
|
||||||
const { timeTrackingEnabled, inactivityTimeoutMs } = useAppSettings();
|
const { timeTrackingEnabled, inactivityTimeoutMs } = useAppSettings();
|
||||||
@@ -124,15 +126,32 @@ export default function useChapter(
|
|||||||
Math.round(Dimensions.get('window').height * 0.75),
|
Math.round(Dimensions.get('window').height * 0.75),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const volumeUpDelta = pageReaderInvertVolumeButtons ? 1 : -1;
|
||||||
|
const volumeDownDelta = pageReaderInvertVolumeButtons ? -1 : 1;
|
||||||
|
|
||||||
useEventListener(NativeVolumeButtonListener, 'VolumeUp', () => {
|
useEventListener(NativeVolumeButtonListener, 'VolumeUp', () => {
|
||||||
webViewRef.current?.injectJavaScript(`(()=>{
|
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', () => {
|
useEventListener(NativeVolumeButtonListener, 'VolumeDown', () => {
|
||||||
webViewRef.current?.injectJavaScript(`(()=>{
|
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,
|
verticalSeekbar = true,
|
||||||
swipeGestures = false,
|
swipeGestures = false,
|
||||||
pageReader = false,
|
pageReader = false,
|
||||||
|
pageReaderInvertVolumeButtons = false,
|
||||||
|
pageReaderDisableAnimation = false,
|
||||||
autoScroll = false,
|
autoScroll = false,
|
||||||
autoScrollInterval = 10,
|
autoScrollInterval = 10,
|
||||||
autoScrollOffset = null,
|
autoScrollOffset = null,
|
||||||
@@ -119,6 +121,38 @@ const NavigationTab: React.FC = () => {
|
|||||||
onPress={() => setChapterGeneralSettings({ pageReader: !pageReader })}
|
onPress={() => setChapterGeneralSettings({ pageReader: !pageReader })}
|
||||||
theme={theme}
|
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>
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
|
|||||||
Reference in New Issue
Block a user