fix: Minor Fixes and Optimizations (#566)

Closes #557
This commit is contained in:
nyagami
2023-03-05 11:13:00 +07:00
committed by GitHub
parent d65170c85a
commit 08860c0715
18 changed files with 456 additions and 563 deletions
-1
View File
@@ -7,7 +7,6 @@ module.exports = {
{
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
rules: {
'prettier/prettier': ['error', { 'endOfLine': 'auto' }],
'@typescript-eslint/no-shadow': ['warn'],
'no-shadow': 'off',
'no-undef': 'off',
-33
View File
@@ -21,7 +21,6 @@
"@react-navigation/stack": "^6.0.11",
"@reduxjs/toolkit": "^1.7.2",
"@shopify/flash-list": "^1.2.1",
"buffer": "^6.0.3",
"cheerio": "^1.0.0-rc.10",
"color": "^4.2.3",
"dayjs": "^1.11.4",
@@ -5325,29 +5324,6 @@
"node-int64": "^0.4.0"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
@@ -19937,15 +19913,6 @@
"node-int64": "^0.4.0"
}
},
"buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
-1
View File
@@ -29,7 +29,6 @@
"@react-navigation/stack": "^6.0.11",
"@reduxjs/toolkit": "^1.7.2",
"@shopify/flash-list": "^1.2.1",
"buffer": "^6.0.3",
"cheerio": "^1.0.0-rc.10",
"color": "^4.2.3",
"dayjs": "^1.11.4",
+1 -4
View File
@@ -6,7 +6,6 @@ import { ChapterItem } from '../types';
import * as cheerio from 'cheerio';
import RNFetchBlob from 'rn-fetch-blob';
import { txnErrorCallback } from '@database/utils/helpers';
import { LoadingImageSrc } from '@screens/reader/utils/LoadImage';
const db = SQLite.openDatabase('lnreader.db');
@@ -297,9 +296,7 @@ const downloadImages = async (
if (fileurl.charAt(0) === '-') {
return loadedCheerio.html();
}
elem.replaceWith(
`<img class='load-icon' src="${LoadingImageSrc}" file-path="${fileurl}">`,
);
elem.attr('src', `file://${fileurl}`);
const exists = await RNFetchBlob.fs.exists(fileurl).catch(() => {
showToast(
`Unexpected storage error!\nRemove ${fileurl} and try downloading again`,
@@ -94,10 +94,8 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
navigate(
'Novel' as never,
{
novelName: item.novelName,
novelCover: item.novelCover,
novelUrl: item.novelUrl,
sourceId,
...item,
sourceId: sourceId,
} as never,
),
[sourceId],
+1 -1
View File
@@ -118,7 +118,7 @@ const HistoryScreen = () => {
<SectionList
contentContainerStyle={styles.listContainer}
sections={groupHistoryByDate(searchText ? searchResults : history)}
keyExtractor={item => item.chapterId.toString()}
keyExtractor={(item, index) => 'history' + index}
renderSectionHeader={({ section: { date } }) => (
<Text style={[styles.dateHeader, { color: theme.onSurface }]}>
{dayjs(date).calendar()}
+311 -315
View File
@@ -358,237 +358,273 @@ const Novel = ({ route, navigation }) => {
const [editInfoModal, showEditInfoModal] = useState(false);
const downloadCustomChapterModal = useBoolean();
if (loading) {
return <NovelScreenLoading theme={theme} />;
}
return (
<Provider>
<View style={[styles.container, { backgroundColor: theme.background }]}>
{loading && <NovelScreenLoading theme={theme} />}
{selected.length === 0 && (
<View
style={{
position: 'absolute',
zIndex: 1,
height: StatusBar.currentHeight + 54,
width: '100%',
flexDirection: 'row',
justifyContent: 'space-between',
}}
>
<IconButton
icon="arrow-left"
iconColor={theme.onBackground}
size={24}
style={{ marginTop: StatusBar.currentHeight + 8 }}
onPress={() => navigation.goBack()}
/>
<Row>
<Portal>
{selected.length === 0 ? (
<View
style={{
position: 'absolute',
height: StatusBar.currentHeight + 54,
width: '100%',
flexDirection: 'row',
justifyContent: 'space-between',
}}
>
<IconButton
icon="share-variant"
icon="arrow-left"
iconColor={theme.onBackground}
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
}}
onPress={() =>
Share.share({
message: novel.sourceUrl,
})
}
size={24}
style={{ marginTop: StatusBar.currentHeight + 8 }}
onPress={() => navigation.goBack()}
/>
<IconButton
icon="text-box-search-outline"
iconColor={theme.onBackground}
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
}}
onPress={() => showJumpToChapterModal(true)}
/>
<Menu
visible={downloadMenu}
onDismiss={() => showDownloadMenu(false)}
anchor={
<IconButton
icon="download-outline"
iconColor={theme.onBackground}
size={24}
style={{
marginTop: StatusBar.currentHeight + 8,
}}
onPress={() => showDownloadMenu(true)}
/>
}
contentStyle={{ backgroundColor: theme.surface2 }}
>
<Menu.Item
title="Next chapter"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{ color: theme.onSurface }}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
[
chapters.find(
chapter =>
!!chapter.read === false &&
!!chapter.downloaded === false,
),
],
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Next 5 chapter"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
.filter(
chapter =>
!!chapter.read === false &&
!!chapter.downloaded === false,
)
.slice(0, 5),
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Next 10 chapter"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
.filter(
chapter =>
!!chapter.read === false &&
!!chapter.downloaded === false,
)
.slice(0, 10),
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Custom"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{ color: theme.onSurface }}
onPress={() => {
downloadCustomChapterModal.setTrue();
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Unread"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters.filter(chapter => !!chapter.read === false),
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="All"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters,
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Delete downloads"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
<Row>
<IconButton
icon="share-variant"
iconColor={theme.onBackground}
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
}}
onPress={() =>
dispatch(deleteAllChaptersAction(sourceId, chapters))
Share.share({
message: novel.sourceUrl,
})
}
/>
</Menu>
<IconButton
icon="text-box-search-outline"
iconColor={theme.onBackground}
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
}}
onPress={() => showJumpToChapterModal(true)}
/>
<Menu
visible={extraMenu}
onDismiss={() => showExtraMenu(false)}
anchor={
<IconButton
icon="dots-vertical"
iconColor={theme.onBackground}
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
marginRight: 16,
<Menu
visible={downloadMenu}
onDismiss={() => showDownloadMenu(false)}
anchor={
<IconButton
icon="download-outline"
iconColor={theme.onBackground}
size={24}
style={{
marginTop: StatusBar.currentHeight + 8,
}}
onPress={() => showDownloadMenu(true)}
/>
}
contentStyle={{ backgroundColor: theme.surface2 }}
>
<Menu.Item
title="Next chapter"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{ color: theme.onSurface }}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
[
chapters.find(
chapter =>
!!chapter.read === false &&
!!chapter.downloaded === false,
),
],
),
);
showDownloadMenu(false);
}}
onPress={() => showExtraMenu(true)}
/>
}
contentStyle={{
backgroundColor: theme.surface2,
<Menu.Item
title="Next 5 chapter"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
.filter(
chapter =>
!!chapter.read === false &&
!!chapter.downloaded === false,
)
.slice(0, 5),
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Next 10 chapter"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
.filter(
chapter =>
!!chapter.read === false &&
!!chapter.downloaded === false,
)
.slice(0, 10),
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Custom"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{ color: theme.onSurface }}
onPress={() => {
downloadCustomChapterModal.setTrue();
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Unread"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters.filter(chapter => !!chapter.read === false),
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="All"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters,
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Delete downloads"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() =>
dispatch(deleteAllChaptersAction(sourceId, chapters))
}
/>
</Menu>
<Menu
visible={extraMenu}
onDismiss={() => showExtraMenu(false)}
anchor={
<IconButton
icon="dots-vertical"
iconColor={theme.onBackground}
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
marginRight: 16,
}}
onPress={() => showExtraMenu(true)}
/>
}
contentStyle={{
backgroundColor: theme.surface2,
}}
>
<Menu.Item
title="Edit info"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() => {
showEditInfoModal(true);
showExtraMenu(false);
}}
/>
<Menu.Item
title="Edit cover"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={setCustomNovelCover}
/>
</Menu>
</Row>
</View>
) : (
<Animated.View
entering={FadeIn.duration(150)}
exiting={FadeOut.duration(150)}
style={{
position: 'absolute',
width: '100%',
elevation: 2,
backgroundColor: theme.surface2,
paddingTop: StatusBar.currentHeight,
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 8,
}}
>
<Appbar.Action
icon="close"
iconColor={theme.onBackground}
onPress={() => setSelected([])}
/>
<Appbar.Content
title={`${selected.length}`}
titleStyle={{ color: theme.onSurface }}
/>
<Appbar.Action
icon="select-all"
iconColor={theme.onBackground}
onPress={() => {
setSelected(chapters);
}}
>
<Menu.Item
title="Edit info"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={() => {
showEditInfoModal(true);
showExtraMenu(false);
}}
/>
<Menu.Item
title="Edit cover"
style={{ backgroundColor: theme.surface2 }}
titleStyle={{
color: theme.onSurface,
}}
onPress={setCustomNovelCover}
/>
</Menu>
</Row>
</View>
)}
/>
</Animated.View>
)}
</Portal>
<View style={{ minHeight: 3, flex: 1 }}>
<FlashList
ref={ref => (flatlistRef.current = ref)}
ref={flatlistRef}
estimatedItemSize={64}
data={!loading && chapters}
data={chapters}
extraData={[downloadQueue, selected]}
removeClippedSubviews={true}
maxToRenderPerBatch={5}
@@ -603,7 +639,6 @@ const Novel = ({ route, navigation }) => {
novel={novel}
theme={theme}
filter={filter}
loading={loading}
lastRead={lastReadChapter}
setCustomNovelCover={setCustomNovelCover}
dispatch={dispatch}
@@ -617,28 +652,25 @@ const Novel = ({ route, navigation }) => {
refreshControl={refreshControl()}
/>
</View>
{useFabForContinueReading &&
!loading &&
chapters.length > 0 &&
lastReadChapter && (
<FAB
style={[
styles.fab,
{ backgroundColor: theme.primary, marginBottom: bottomInset },
]}
small
color={theme.onPrimary}
uppercase={false}
label={novel.unread ? 'Start' : 'Resume'}
icon="play"
onPress={() => {
navigation.navigate(
'Chapter',
openChapter(novel, lastReadChapter),
);
}}
/>
)}
{useFabForContinueReading && chapters.length > 0 && lastReadChapter && (
<FAB
style={[
styles.fab,
{ backgroundColor: theme.primary, marginBottom: bottomInset },
]}
small
color={theme.onPrimary}
uppercase={false}
label={novel.unread ? 'Start' : 'Resume'}
icon="play"
onPress={() => {
navigation.navigate(
'Chapter',
openChapter(novel, lastReadChapter),
);
}}
/>
)}
<Portal>
<Actionbar
active={selected.length > 0}
@@ -663,84 +695,48 @@ const Novel = ({ route, navigation }) => {
</Text>
</Snackbar>
</Portal>
{!loading && (
<Portal>
<JumpToChapterModal
modalVisible={jumpToChapterModal}
hideModal={() => showJumpToChapterModal(false)}
chapters={chapters}
novel={novel}
chapterListRef={flatlistRef.current}
navigation={navigation}
/>
<EditInfoModal
modalVisible={editInfoModal}
hideModal={() => showEditInfoModal(false)}
novel={novel}
theme={theme}
dispatch={dispatch}
/>
<DownloadCustomChapterModal
modalVisible={downloadCustomChapterModal.value}
hideModal={downloadCustomChapterModal.setFalse}
novel={novel}
chapters={chapters}
theme={theme}
dispatch={dispatch}
/>
<NovelBottomSheet
novelUrl={novelUrl}
bottomSheetRef={novelBottomSheetRef}
dispatch={dispatch}
sortAndFilterChapters={sortAndFilterChapters}
novelId={novel.novelId}
sort={sort}
theme={theme}
filter={filter}
showChapterTitles={showChapterTitles}
/>
<TrackSheet
bottomSheetRef={trackerSheetRef}
novelId={novel.novelId}
novelName={novel.novelName}
theme={theme}
/>
</Portal>
)}
{selected.length > 0 ? (
<Animated.View
entering={FadeIn.duration(150)}
exiting={FadeOut.duration(150)}
style={{
position: 'absolute',
zIndex: 20,
width: Dimensions.get('window').width,
elevation: 2,
backgroundColor: theme.surface2,
paddingTop: StatusBar.currentHeight,
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 8,
}}
>
<Appbar.Action
icon="close"
iconColor={theme.onBackground}
onPress={() => setSelected([])}
/>
<Appbar.Content
title={`${selected.length}`}
titleStyle={{ color: theme.onSurface }}
/>
<Appbar.Action
icon="select-all"
iconColor={theme.onBackground}
onPress={() => {
setSelected(chapters);
}}
/>
</Animated.View>
) : null}
<Portal>
<JumpToChapterModal
modalVisible={jumpToChapterModal}
hideModal={() => showJumpToChapterModal(false)}
chapters={chapters}
novel={novel}
chapterListRef={flatlistRef.current}
navigation={navigation}
/>
<EditInfoModal
modalVisible={editInfoModal}
hideModal={() => showEditInfoModal(false)}
novel={novel}
theme={theme}
dispatch={dispatch}
/>
<DownloadCustomChapterModal
modalVisible={downloadCustomChapterModal.value}
hideModal={downloadCustomChapterModal.setFalse}
novel={novel}
chapters={chapters}
theme={theme}
dispatch={dispatch}
/>
<NovelBottomSheet
novelUrl={novelUrl}
bottomSheetRef={novelBottomSheetRef}
dispatch={dispatch}
sortAndFilterChapters={sortAndFilterChapters}
novelId={novel.novelId}
sort={sort}
theme={theme}
filter={filter}
showChapterTitles={showChapterTitles}
/>
<TrackSheet
bottomSheetRef={trackerSheetRef}
novelId={novel.novelId}
novelName={novel.novelName}
theme={theme}
/>
</Portal>
</View>
</Provider>
);
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { StyleSheet, Text, View, TextInput } from 'react-native';
import { IconButton, Modal, Portal } from 'react-native-paper';
import { Button, IconButton, Modal, Portal } from 'react-native-paper';
import { downloadAllChaptersAction } from '../../../redux/novel/novel.actions';
const DownloadCustomChapterModal = ({
@@ -91,6 +91,13 @@ const DownloadCustomChapterModal = ({
onPress={() => setText(prevState => prevState + 10)}
/>
</View>
<Button
onPress={onSubmit}
textColor={theme.onPrimary}
buttonColor={theme.primary}
>
Download
</Button>
</Modal>
</Portal>
);
@@ -54,8 +54,11 @@ const EditInfoModal = ({ theme, hideModal, modalVisible, novel, dispatch }) => {
horizontal
showsHorizontalScrollIndicator={false}
>
{status.map(item => (
<View style={{ borderRadius: 8, overflow: 'hidden' }} key={item}>
{status.map((item, index) => (
<View
style={{ borderRadius: 8, overflow: 'hidden' }}
key={'novelInfo' + index}
>
<Pressable
style={{
backgroundColor:
@@ -133,7 +136,7 @@ const EditInfoModal = ({ theme, hideModal, modalVisible, novel, dispatch }) => {
contentContainerStyle={{ marginVertical: 8 }}
horizontal
data={info.genre.split(',')}
keyExtractor={item => item}
keyExtractor={(item, index) => 'novelTag' + index}
renderItem={({ item }) => (
<GenreChip theme={theme} onPress={() => removeTag(item)}>
{item}
@@ -37,7 +37,7 @@ const CoverImage = ({ children, source, theme, hideBackdrop }) => {
<View
style={{
flex: 1,
backgroundColor: `${theme.background}B4`,
backgroundColor: color(theme.background).alpha(0.7).string(),
}}
>
{source.uri ? (
@@ -57,7 +57,7 @@ const CoverImage = ({ children, source, theme, hideBackdrop }) => {
}
};
const NovelThumbnail = ({ source, setCustomNovelCover }) => {
const NovelThumbnail = ({ source, theme, setCustomNovelCover }) => {
const [expanded, setExpanded] = useState(false);
if (!expanded) {
@@ -69,24 +69,15 @@ const NovelThumbnail = ({ source, setCustomNovelCover }) => {
} else {
return (
<Portal>
<IconButton
icon="close"
style={{
position: 'absolute',
top: StatusBar.currentHeight,
left: 0,
zIndex: 10,
}}
onPress={() => setExpanded(false)}
/>
<IconButton
icon="pencil-outline"
style={{
position: 'absolute',
top: StatusBar.currentHeight,
right: 0,
top: StatusBar.currentHeight + 10,
right: 10,
zIndex: 10,
}}
iconColor={theme.onBackground}
onPress={setCustomNovelCover}
/>
<Pressable
@@ -33,7 +33,6 @@ const NovelInfoHeader = ({
novel,
theme,
filter,
loading,
chapters,
lastRead,
navigation,
@@ -58,13 +57,13 @@ const NovelInfoHeader = ({
return (
<>
<CoverImage
source={{ uri: !loading ? novel.novelCover : item.novelCover }}
source={{ uri: novel.novelCover }}
theme={theme}
hideBackdrop={hideBackdrop}
>
<NovelInfoContainer>
<NovelThumbnail
source={{ uri: !loading ? novel.novelCover : item.novelCover }}
source={{ uri: novel.novelCover }}
theme={theme}
setCustomNovelCover={setCustomNovelCover}
/>
@@ -85,7 +84,7 @@ const NovelInfoHeader = ({
</NovelTitle>
<>
<NovelAuthor theme={theme}>
{!loading && novel.author ? novel.author : 'Unknown author'}
{novel.author || 'Unknown author'}
</NovelAuthor>
<Row>
<MaterialCommunityIcons
@@ -95,58 +94,52 @@ const NovelInfoHeader = ({
style={{ marginRight: 4 }}
/>
<NovelInfo theme={theme}>
{!loading
? (novel.status || 'Unknown status') + ' • ' + novel.source
: 'Unknown status • Unknown source'}
{(novel.status || 'Unknown status') + ' • ' + novel.source}
</NovelInfo>
</Row>
</>
</View>
</NovelInfoContainer>
</CoverImage>
{!loading && (
<>
<NovelScreenButtonGroup
novel={novel}
handleFollowNovel={() => {
dispatch(followNovelAction(novel));
if (
novel.followed &&
chapters.some(chapter => chapter.downloaded === 1)
) {
deleteDownloadsSnackbar.setTrue();
}
}}
handleTrackerSheet={() => trackerSheetRef.current.expand()}
theme={theme}
<>
<NovelScreenButtonGroup
novel={novel}
handleFollowNovel={() => {
dispatch(followNovelAction(novel));
if (
novel.followed &&
chapters.some(chapter => chapter.downloaded === 1)
) {
deleteDownloadsSnackbar.setTrue();
}
}}
handleTrackerSheet={() => trackerSheetRef.current.expand()}
theme={theme}
/>
<NovelSummary
summary={novel.novelSummary}
isExpanded={!novel.followed}
theme={theme}
/>
{novel.genre ? <NovelGenres theme={theme} genre={novel.genre} /> : null}
<ReadButton novel={novel} chapters={chapters} lastRead={lastRead} />
<Pressable
style={styles.bottomsheet}
onPress={() => novelBottomSheetRef.current.expand()}
android_ripple={{
color: color(theme.primary).alpha(0.12).string(),
}}
>
<Text style={[{ color: theme.onSurface }, styles.chapters]}>
{`${chapters.length} ${getString('novelScreen.chapters')}`}
</Text>
<IconButton
icon="filter-variant"
iconColor={filter ? filterColor(theme.isDark) : theme.onSurface}
size={24}
/>
<NovelSummary
summary={novel.novelSummary}
isExpanded={!novel.followed}
theme={theme}
/>
{novel.genre ? (
<NovelGenres theme={theme} genre={novel.genre} />
) : null}
<ReadButton novel={novel} chapters={chapters} lastRead={lastRead} />
<Pressable
style={styles.bottomsheet}
onPress={() => novelBottomSheetRef.current.expand()}
android_ripple={{
color: color(theme.primary).alpha(0.12).string(),
}}
>
<Text style={[{ color: theme.onSurface }, styles.chapters]}>
{`${chapters.length} ${getString('novelScreen.chapters')}`}
</Text>
<IconButton
icon="filter-variant"
iconColor={filter ? filterColor(theme.isDark) : theme.onSurface}
size={24}
/>
</Pressable>
</>
)}
</Pressable>
</>
</>
);
};
+36 -33
View File
@@ -325,9 +325,7 @@ const ChapterContent = ({ route, navigation }) => {
nextChapter={nextChapter}
webViewRef={webViewRef}
onLayout={() => {
if (useVolumeButtons) {
onLayout();
}
useVolumeButtons && onLayout();
scrollTo(position?.position);
}}
onPress={hideHeader}
@@ -335,39 +333,44 @@ const ChapterContent = ({ route, navigation }) => {
navigateToChapterBySwipe={navigateToChapterBySwipe}
onWebViewNavigationStateChange={onWebViewNavigationStateChange}
/>
<BottomInfoBar scrollPercentage={position?.percentage || 0} />
<BottomInfoBar
scrollPercentage={
position?.percentage || Math.round(minScroll.current) || 0
}
/>
<Portal>
<ReaderBottomSheetV2 bottomSheetRef={readerSheetRef} />
</Portal>
<ReaderAppbar
bookmark={bookmark}
novelName={novelName}
chapterId={chapterId}
chapterName={chapterName || chapter.chapterName}
hide={hidden}
tts={startTts}
textToSpeech={ttsStatus}
theme={theme}
/>
<ReaderSeekBar
hide={hidden}
theme={theme}
minScroll={minScroll.current}
verticalSeekbar={verticalSeekbar}
percentage={position?.percentage}
scrollTo={scrollTo}
/>
<ReaderFooter
hide={hidden}
theme={theme}
chapterUrl={chapterUrl}
nextChapter={nextChapter}
prevChapter={prevChapter}
readerSheetRef={readerSheetRef}
scrollTo={scrollTo}
navigateToChapterBySwipe={navigateToChapterBySwipe}
openDrawer={openDrawer}
/>
{!hidden && (
<>
<ReaderAppbar
bookmark={bookmark}
novelName={novelName}
chapterId={chapterId}
chapterName={chapterName || chapter.chapterName}
tts={startTts}
textToSpeech={ttsStatus}
theme={theme}
/>
<ReaderSeekBar
theme={theme}
minScroll={minScroll.current}
verticalSeekbar={verticalSeekbar}
percentage={position?.percentage}
scrollTo={scrollTo}
/>
<ReaderFooter
theme={theme}
chapterUrl={chapterUrl}
nextChapter={nextChapter}
prevChapter={prevChapter}
readerSheetRef={readerSheetRef}
scrollTo={scrollTo}
navigateToChapterBySwipe={navigateToChapterBySwipe}
openDrawer={openDrawer}
/>
</>
)}
</>
);
};
@@ -14,7 +14,6 @@ const ReaderAppbar = ({
novelName,
chapterId,
chapterName,
hide,
tts,
textToSpeech,
theme,
@@ -23,10 +22,6 @@ const ReaderAppbar = ({
const { goBack } = useNavigation();
const [bookmarked, setBookmarked] = useState(bookmark);
if (hide) {
return null;
}
return (
<Animated.View
entering={FadeIn.duration(150)}
@@ -8,7 +8,6 @@ import color from 'color';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
const ChapterFooter = ({
hide,
theme,
chapterUrl,
nextChapter,
@@ -26,16 +25,11 @@ const ChapterFooter = ({
const insets = useSafeAreaInsets();
if (hide) {
return null;
}
return (
<Animated.View
entering={FadeIn.duration(150)}
exiting={FadeOut.duration(150)}
style={styles.footer}
active={hide}
animationDuration={150}
>
<View
@@ -7,7 +7,6 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDeviceOrientation } from '@hooks/useDeviceOrientation';
const VerticalScrollbar = ({
hide,
theme,
minScroll = 0,
verticalSeekbar,
@@ -22,10 +21,6 @@ const VerticalScrollbar = ({
};
const screenOrientation = useDeviceOrientation();
if (hide) {
return null;
}
if (screenOrientation === 'potrait' && verticalSeekbar) {
return (
<View
+30 -83
View File
@@ -71,6 +71,7 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
<WebView
ref={webViewRef}
style={{ backgroundColor }}
allowFileAccess={true}
originWhitelist={['*']}
scalesPageToFit={true}
showsVerticalScrollIndicator={false}
@@ -90,53 +91,16 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
case 'prev':
navigateToChapterBySwipe('SWIPE_RIGHT');
break;
case 'imgfiles':
if (event.data) {
if (Array.isArray(event.data.imgs)) {
const promises: Promise<{ data: any; id: number }>[] = [];
if (event.data.type === 'online') {
for (let i = 0; i < event.data.imgs.length; i++) {
const { url, id } = event.data.imgs[i];
if (url && id) {
promises.push(
RNFetchBlob.fetch('get', url, headers).then(res => ({
data: res.base64(),
id: id,
})),
);
}
}
} else {
for (let i = 0; i < event.data.imgs.length; i++) {
const { url, id } = event.data.imgs[i];
if (url && id) {
promises.push(
RNFetchBlob.fs.readFile(url, 'base64').then(base64 => {
return { data: base64, id: id };
}),
);
}
}
}
Promise.all(promises)
.then(datas => {
const inject = datas.reduce((p, data) => {
return (
p +
`document.querySelector("img[file-id='${data.id}']").setAttribute("src", "data:image/jpg;base64,${data.data}");
document.querySelector("img[file-id='${data.id}']").classList.remove("load-icon");`
);
}, '');
webViewRef.current?.injectJavaScript(
inject +
'window.requestAnimationFrame(()=>sendHeight());',
);
})
.catch(e => {
e; // CloudFlare is too strong :D
});
}
case 'imgfile':
if (event.data && typeof event.data === 'string') {
RNFetchBlob.fetch('get', event.data, headers).then(res => {
const base64 = res.base64();
webViewRef.current?.injectJavaScript(
`document.querySelector("img[delayed-src='${event.data}']").src="data:image/jpg;base64,${base64}";
document.querySelector("img[delayed-src='${event.data}']").classList.remove("load-icon");
sendHeight(500);`,
);
});
}
break;
case 'scrollend':
@@ -151,6 +115,7 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
const contentHeight = Math.round(Number(event.data));
minScroll.current = (layoutHeight / contentHeight) * 100;
}
break;
}
}}
source={{
@@ -261,22 +226,13 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
</chapter>
</div>
<script>
const isOffline = !!document.querySelector("input[offline]")
if(isOffline){
imgs = [...document.querySelectorAll("img")].map((img, index)=>{
img.setAttribute("file-id", index.toString());
return {url:img.getAttribute("file-path"), id:img.getAttribute("file-id")};
if(!document.querySelector("input[offline]") && ${!!headers}){
document.querySelectorAll("img").forEach(img => {
window.ReactNativeWebView.postMessage(JSON.stringify({type:"imgfile",data:img.getAttribute("delayed-src")}));
});
window.ReactNativeWebView.postMessage(JSON.stringify({type:"imgfiles",data:{imgs:imgs,type:"offline"}}));
}else if(${!!headers}){
imgs = [...document.querySelectorAll("img")].map((img, index)=>{
img.setAttribute("file-id", index.toString());
return {url:img.getAttribute("delayed-src"), id:img.getAttribute("file-id")};
});
window.ReactNativeWebView.postMessage(JSON.stringify({type:"imgfiles",data:{imgs:imgs,type:"online"}}));
}
var scrollTimeout;
window.addEventListener("scroll", (event) => {
window.clearTimeout( scrollTimeout );
scrollTimeout = setTimeout(() => {
@@ -293,17 +249,17 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
);
}, 100);
});
const sendHeight = () => {
window.ReactNativeWebView.postMessage(
JSON.stringify(
{
type:"height",
data:document.body.scrollHeight
}
)
let sendHeightTimeout;
const sendHeight = (timeOut) => {
clearTimeout(sendHeightTimeout);
sendHeightTimeout = setTimeout(
window.ReactNativeWebView.postMessage(
JSON.stringify({type:"height",data:document.body.scrollHeight})
), timeOut
);
}
window.requestAnimationFrame(()=>sendHeight());
sendHeight(1000);
</script>
<div class="infoText">
${getString(
@@ -325,28 +281,19 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
swipeGestures
? `
<script>
var initialX = null;
var initialY = null;
let initialX = null;
let initialY = null;
document.addEventListener("touchstart", e => {
initialX = e.changedTouches[0].screenX;
initialY = e.changedTouches[0].screenY;
});
document.addEventListener("touchend", e => {
if (initialX === null || initialY === null) return;
var currentX = e.changedTouches[0].screenX;
var currentY = e.changedTouches[0].screenY;
var diffX = currentX - initialX;
var diffY = currentY - initialY;
let diffX = e.changedTouches[0].screenX - initialX;
let diffY = e.changedTouches[0].screenY - initialY;
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 10) {
e.preventDefault();
if (diffX < 0) {
window.ReactNativeWebView.postMessage(JSON.stringify({type:"next"}))
} else {
window.ReactNativeWebView.postMessage(JSON.stringify({type:"prev"}))
}
window.ReactNativeWebView.postMessage(JSON.stringify({type: diffX<0 ? "next" : "prev"}))
}
initialX = null;
initialY = null;
});
</script>`
: ''
@@ -13,16 +13,25 @@ export const sanitizeChapterText = (
html: string,
options?: Options,
): string => {
if (html) {
html = html.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
}
let text = sanitizeHtml(html, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'input']),
allowedTags: sanitizeHtml.defaults.allowedTags.concat([
'img',
'input',
'i',
'em',
'b',
'a',
]),
allowedAttributes: {
'img': ['src', 'type', 'file-path', 'file-id', 'offline', 'class'],
'img': ['src', 'class'],
'a': ['href'],
'input': ['type', 'offline'],
},
allowedSchemes: ['data', 'http', 'https'],
allowedSchemes: ['data', 'http', 'https', 'file'],
});
if (text) {
if (options?.removeExtraParagraphSpacing) {
text = text.replace(/<\s*br[^>]*>/gi, '\n').replace(/\n{2,}/g, '\n\n');
@@ -49,6 +58,5 @@ export const sanitizeChapterText = (
text =
"Chapter is empty.\n\nReport on <a href='https://github.com/LNReader/lnreader-sources/issues/new/choose'>github</a> if it's available in webview.";
}
return text;
};
@@ -169,6 +169,7 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
label={getString(
'moreScreen.settingsScreen.generalSettingsScreen.updateLibrary',
)}
description={'Not recommended for low devices'}
value={updateLibraryOnLaunch}
onPress={() =>
dispatch(