diff --git a/app.json b/app.json index 84d324b1..afdc4963 100644 --- a/app.json +++ b/app.json @@ -30,7 +30,11 @@ }, "package": "com.rajarsheechatterjee.LNReader" }, - + "androidStatusBar": { + "barStyle": "light-content", + "backgroundColor": "#00000000", + "translucent": true + }, "androidNavigationBar": { "barStyle": "light-content", "backgroundColor": "#000000" diff --git a/package-lock.json b/package-lock.json index bb4829c6..d8a73937 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3969,6 +3969,11 @@ "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-9.1.2.tgz", "integrity": "sha512-IFBQPimeZtdqtvmqQZEmiI0D4j9xE53tSZxLFrFPplNWzgDefk+dP+flof3duf9JgCQTsvOUU2h2/XwjD3vBTA==" }, + "expo-speech": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/expo-speech/-/expo-speech-9.1.0.tgz", + "integrity": "sha512-5yIMVo1auAwClVnlJnCRyJqKS82IeQXzTCamP4iP4LmHDR5+9sae/D/mD5mX7KV3brWf3n0sEyFhuU7WqgMQvg==" + }, "expo-splash-screen": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.10.2.tgz", diff --git a/package.json b/package.json index 43ecb25c..97128df6 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "redux-devtools-extension": "^2.13.9", "redux-persist": "^6.0.0", "redux-thunk": "^2.3.0", - "rn-sliding-up-panel": "^2.4.4" + "rn-sliding-up-panel": "^2.4.4", + "expo-speech": "~9.1.0" }, "devDependencies": { "@babel/core": "~7.9.0" diff --git a/src/Database/queries/ChapterQueries.js b/src/Database/queries/ChapterQueries.js index 8b2b1e5a..334700f6 100644 --- a/src/Database/queries/ChapterQueries.js +++ b/src/Database/queries/ChapterQueries.js @@ -56,7 +56,7 @@ export const getChapters = (novelId, sort, filter) => { const getChapterQuery = `SELECT * FROM downloads WHERE downloadChapterId = ?`; -export const getChapter = async (chapterId) => { +export const getChapterFromDB = async (chapterId) => { return new Promise((resolve, reject) => db.transaction((tx) => { tx.executeSql( @@ -71,6 +71,36 @@ export const getChapter = async (chapterId) => { ); }; +export const getNextChapterFromDB = async (novelId, chapterId) => { + return new Promise((resolve, reject) => + db.transaction((tx) => { + tx.executeSql( + `SELECT * FROM chapters WHERE novelId = ? AND chapterId > ?`, + [novelId, chapterId], + (tx, results) => { + resolve(results.rows.item(0)); + }, + (txObj, error) => console.log("Error ", error) + ); + }) + ); +}; + +export const getPrevChapterFromDB = async (novelId, chapterId) => { + return new Promise((resolve, reject) => + db.transaction((tx) => { + tx.executeSql( + `SELECT * FROM chapters WHERE novelId = ? AND chapterId < ?`, + [novelId, chapterId], + (tx, results) => { + resolve(results.rows.item(results.rows.length - 1)); + }, + (txObj, error) => console.log("Error ", error) + ); + }) + ); +}; + const markChapterReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE chapterId = ?`; export const markChapterRead = async (chapterId) => { diff --git a/src/Database/queries/UpdateQueries.js b/src/Database/queries/UpdateQueries.js index 20fdccaa..7e80780b 100644 --- a/src/Database/queries/UpdateQueries.js +++ b/src/Database/queries/UpdateQueries.js @@ -2,7 +2,7 @@ import * as SQLite from "expo-sqlite"; const db = SQLite.openDatabase("lnreader.db"); const getUpdatesQuery = ` - SELECT chapters.chapterId, chapters.chapterUrl, chapters.chapterName, novels.novelUrl, novels.novelId, novels.novelCover, novels.novelName, novels.sourceId, updates.updateTime, updates.updateId + SELECT chapters.chapterId, chapters.read, chapters.downloaded, chapters.chapterUrl, chapters.chapterName, novels.novelUrl, novels.novelId, novels.novelCover, novels.novelName, novels.sourceId, updates.updateTime, updates.updateId FROM updates JOIN chapters ON updates.chapterId = chapters.chapterId diff --git a/src/Hooks/reduxHooks.js b/src/Hooks/reduxHooks.js index e7d866ac..69d19344 100644 --- a/src/Hooks/reduxHooks.js +++ b/src/Hooks/reduxHooks.js @@ -89,6 +89,20 @@ const useContinueReading = (chapters, novelId) => { return { lastReadChapter, position }; }; +const usePosition = (novelId, chapterId) => { + let novel, position; + + novel = findNovel(novelId); + + if (novel) { + if (novel.position) { + position = novel.position[chapterId] || null; + } + } + + return position; +}; + const useTrackingStatus = () => { const tracker = useSelector((state) => state.trackerReducer); @@ -113,4 +127,5 @@ export { useChapter, useSavedSettings, useLibraryFilters, + usePosition, }; diff --git a/src/Screens/Chapter/Chapter.js b/src/Screens/Chapter/Chapter.js index 5c85a46a..86604788 100644 --- a/src/Screens/Chapter/Chapter.js +++ b/src/Screens/Chapter/Chapter.js @@ -4,68 +4,95 @@ import { View, Text, ActivityIndicator, - ScrollView, - InteractionManager, + Button, } from "react-native"; -import Constants from "expo-constants"; - -import { Appbar, Portal } from "react-native-paper"; -import { CollapsibleHeaderScrollView } from "react-native-collapsible-header-views"; - -import ReaderSheet from "./components/ReaderSheet"; - -import { insertHistoryAction } from "../../redux/history/history.actions"; -import { - getChapterAction, - saveScrollPosition, -} from "../../redux/chapter/chapter.actions"; -import { markChapterReadAction } from "../../redux/novel/novel.actions"; -import { updateChaptersRead } from "../../redux/tracker/tracker.actions"; import { useDispatch } from "react-redux"; -import { parseChapterNumber } from "../../Services/updates"; +import Constants from "expo-constants"; +import { Portal } from "react-native-paper"; +import { CollapsibleHeaderScrollView } from "react-native-collapsible-header-views"; + +import { getChapterFromDB } from "../../Database/queries/ChapterQueries"; +import { fetchChapter } from "../../Services/Source/source"; +import { showToast } from "../../Hooks/showToast"; import { - useChapter, + usePosition, useReaderSettings, useTheme, useTrackingStatus, } from "../../Hooks/reduxHooks"; +import { updateChaptersRead } from "../../redux/tracker/tracker.actions"; +import { insertHistoryAction } from "../../redux/history/history.actions"; import { readerBackground, readerLineHeight, readerTextColor, } from "./readerStyleController"; +import { markChapterReadAction } from "../../redux/novel/novel.actions"; +import { saveScrollPosition } from "../../redux/preferences/preference.actions"; +import { parseChapterNumber } from "../../Services/updates"; + +import ChapterAppbar from "./components/ChapterAppbar"; +import ReaderSheet from "./components/ReaderSheet"; const Chapter = ({ route, navigation }) => { - const { chapterId, sourceId, chapterUrl, novelUrl, novelId, position } = - route.params; + const { + sourceId, + chapterId, + chapterUrl, + novelId, + novelUrl, + novelName, + chapterName, + } = route.params; + let scrollViewRef; let readerSheetRef = useRef(null); const theme = useTheme(); - const reader = useReaderSettings(); const dispatch = useDispatch(); + const reader = useReaderSettings(); const { tracker, trackedNovels } = useTrackingStatus(); + const position = usePosition(novelId, chapterId); const isTracked = trackedNovels.find((obj) => obj.novelId === novelId); - const { chapter, loading } = useChapter(); + const [chapter, setChapter] = useState({}); + const [loading, setLoading] = useState(true); const [scrollPercentage, setScrollPercentage] = useState(0); const [firstLayout, setFirstLayout] = useState(true); - let scrollViewRef; + const getChapter = async (chapterId) => { + try { + if (chapterId) { + const chapterDownloaded = await getChapterFromDB(chapterId); + + if (chapterDownloaded) { + setChapter(chapterDownloaded); + } else { + const res = await fetchChapter( + sourceId, + novelUrl, + chapterUrl + ); + setChapter(res); + } + } else { + const res = await fetchChapter(sourceId, novelUrl, chapterUrl); + setChapter(res); + } + + setLoading(false); + } catch (error) { + showToast(error.message); + } + }; useEffect(() => { - dispatch(getChapterAction(sourceId, novelUrl, chapterUrl, chapterId)); + getChapter(chapterId); dispatch(insertHistoryAction(novelId, chapterId)); }, []); - let chapterNumber; - - if (!loading) { - chapterNumber = parseChapterNumber(chapter.chapterName); - } - const isCloseToBottom = ({ layoutMeasurement, contentOffset, @@ -78,39 +105,40 @@ const Chapter = ({ route, navigation }) => { ); }; - const updateTracker = () => + const updateTracker = () => { + const chapterNumber = parseChapterNumber(chapterName); + isTracked && - chapterNumber && - chapterNumber > isTracked.my_list_status.num_chapters_read && - dispatch( - updateChaptersRead( - isTracked.id, - tracker.access_token, - chapterNumber - ) + chapterNumber && + chapterNumber > isTracked.my_list_status.num_chapters_read && + dispatch( + updateChaptersRead( + isTracked.id, + tracker.access_token, + chapterNumber + ) + ); + }; + + const onScroll = ({ nativeEvent }) => { + const offsetY = nativeEvent.contentOffset.y; + const position = + nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height; + + const percentage = Math.round( + (position / nativeEvent.contentSize.height) * 100 ); - const navigateToPreviousChapter = () => - navigation.replace("Chapter", { - chapterUrl: chapter.prevChapter, - chapterId: chapterId - 1, - sourceId, - novelUrl, - novelId, - chapterName: chapter.chapterName, - }); + setScrollPercentage(percentage); + dispatch(saveScrollPosition(offsetY, percentage, chapterId, novelId)); - const navigateToNextChapter = () => - navigation.replace("Chapter", { - chapterUrl: chapter.nextChapter, - sourceId, - novelUrl, - novelId, - chapterId: chapterId + 1, - chapterName: chapter.chapterName, - }); + if (isCloseToBottom(nativeEvent)) { + dispatch(markChapterReadAction(chapterId, novelId)); + updateTracker(); + } + }; - const scrollToInitialPosition = () => { + const scrollToSavedProgress = () => { if (position && firstLayout) { position.percentage < 100 && scrollViewRef.getNode().scrollTo({ @@ -137,74 +165,28 @@ const Chapter = ({ route, navigation }) => { }, ]; - const onScroll = ({ nativeEvent }) => { - const offsetY = nativeEvent.contentOffset.y; - const position = - nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height; - - const percentage = Math.round( - (position / nativeEvent.contentSize.height) * 100 - ); - - setScrollPercentage(percentage); - dispatch(saveScrollPosition(offsetY, percentage, chapterId, novelId)); - - if (isCloseToBottom(nativeEvent)) { - dispatch(markChapterReadAction(chapterId, novelId)); - updateTracker(); - } - }; - return ( <> { - scrollViewRef = ref; - }} + ref={(ref) => (scrollViewRef = ref)} CollapsibleHeaderComponent={ - - navigation.goBack()} - color="#FFFFFF" - size={26} - style={{ marginRight: 0 }} - /> - - {!loading && ( - <> - - - - )} - readerSheetRef.current.show()} - color="#FFFFFF" - /> - + } - headerHeight={Constants.statusBarHeight + 60} + headerHeight={Constants.statusBarHeight + 56} contentContainerStyle={[ - styles.container, + styles.screenContainer, { backgroundColor: readerBackground(reader.theme) }, ]} onScroll={onScroll} @@ -219,7 +201,7 @@ const Chapter = ({ route, navigation }) => { ) : ( {chapter.chapterText.trim()} @@ -249,7 +231,7 @@ const Chapter = ({ route, navigation }) => { export default Chapter; const styles = StyleSheet.create({ - container: { + screenContainer: { flexGrow: 1, paddingVertical: 10, }, diff --git a/src/Screens/Chapter/components/ChapterAppbar.js b/src/Screens/Chapter/components/ChapterAppbar.js new file mode 100644 index 00000000..bd7314d5 --- /dev/null +++ b/src/Screens/Chapter/components/ChapterAppbar.js @@ -0,0 +1,95 @@ +import React, { useEffect, useState } from "react"; + +import { Appbar } from "react-native-paper"; +import { + getNextChapterFromDB, + getPrevChapterFromDB, +} from "../../../Database/queries/ChapterQueries"; + +const ChapterAppbar = ({ + navigation, + sourceId, + novelId, + novelName, + novelUrl, + chapterId, + chapterName, + theme, + readerSheetRef, +}) => { + const [nextChapter, setNextChapter] = useState({}); + const [prevChapter, setPrevChapter] = useState({}); + + const setPrevAndNextChap = async () => { + const nextChap = await getNextChapterFromDB(novelId, chapterId); + const prevChap = await getPrevChapterFromDB(novelId, chapterId); + + setNextChapter(nextChap); + setPrevChapter(prevChap); + }; + + useEffect(() => { + setPrevAndNextChap(); + }, []); + + const navigateToPrevChapter = () => + navigation.replace("Chapter", { + chapterUrl: prevChapter.chapterUrl, + chapterId: prevChapter.chapterId, + sourceId, + novelUrl, + novelId, + chapterName: prevChapter.chapterName, + novelName: novelName, + }); + + const navigateToNextChapter = () => + navigation.replace("Chapter", { + chapterUrl: nextChapter.nextChapter, + sourceId, + novelUrl, + novelId, + chapterId: nextChapter.chapterId, + chapterName: nextChapter.chapterName, + novelName: novelName, + }); + + return ( + + navigation.goBack()} + color="#FFFFFF" + size={26} + style={{ marginRight: 0 }} + /> + + + + readerSheetRef.current.show()} + color="#FFFFFF" + /> + + ); +}; + +export default ChapterAppbar; diff --git a/src/Screens/History/components/HistoryCard.js b/src/Screens/History/components/HistoryCard.js index eaf482c3..25648c9c 100644 --- a/src/Screens/History/components/HistoryCard.js +++ b/src/Screens/History/components/HistoryCard.js @@ -77,6 +77,7 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation, theme }) => { novelUrl: item.novelUrl, novelId: item.novelId, chapterName: item.chapterName, + novelName: item.novelName, }) } /> diff --git a/src/Screens/More/components/GridSizeModal.js b/src/Screens/More/components/GridSizeModal.js index 9dcde7a0..0d0a28b9 100644 --- a/src/Screens/More/components/GridSizeModal.js +++ b/src/Screens/More/components/GridSizeModal.js @@ -13,11 +13,11 @@ const GridSizeModal = ({ theme, }) => { const gridSizes = { - 1: "XS", - 2: "S", + 5: "XS", + 4: "S", 3: "M", - 4: "L", - 5: "XL", + 2: "L", + 1: "XL", }; return ( diff --git a/src/Screens/Novel/Novel.js b/src/Screens/Novel/Novel.js index 3353bd9c..47515231 100644 --- a/src/Screens/Novel/Novel.js +++ b/src/Screens/Novel/Novel.js @@ -81,6 +81,7 @@ const Novel = ({ route, navigation }) => { position={position} downloading={downloading} selected={selected} + novelName={novelName} setSelected={setSelected} chapterActionsSheetRef={chapterActionsSheetRef} /> diff --git a/src/Screens/Novel/components/ChapterActionsSheet.js b/src/Screens/Novel/components/ChapterActionsSheet.js index fc57da4d..df318dfa 100644 --- a/src/Screens/Novel/components/ChapterActionsSheet.js +++ b/src/Screens/Novel/components/ChapterActionsSheet.js @@ -43,6 +43,7 @@ const ChapterActionsSheet = ({ paddingBottom: 8, fontSize: 16, }} + numberOfLines={1} > {selected?.chapterName} diff --git a/src/Screens/Novel/components/ChapterItem.js b/src/Screens/Novel/components/ChapterItem.js index 21791242..a20a92bb 100644 --- a/src/Screens/Novel/components/ChapterItem.js +++ b/src/Screens/Novel/components/ChapterItem.js @@ -9,6 +9,7 @@ import { const ChapterItem = ({ novelUrl, + novelName, chapter, sourceId, dispatch, @@ -31,9 +32,7 @@ const ChapterItem = ({ chapterId: chapter.chapterId, chapterUrl: chapter.chapterUrl, chapterName: chapter.chapterName, - position: !chapter.read - ? position && position[chapter.chapterId] - : 0, + novelName: novelName, }); const displayDownloadButton = () => { @@ -179,7 +178,7 @@ const ChapterItem = ({ { novelId: lastRead.novelId, sourceId: novel.sourceId, chapterName: lastRead.chapterName, + novelName: novel.novelName, }); }} > diff --git a/src/Screens/Updates/Updates.js b/src/Screens/Updates/Updates.js index ff0abacb..c4fc2b3d 100644 --- a/src/Screens/Updates/Updates.js +++ b/src/Screens/Updates/Updates.js @@ -47,6 +47,7 @@ const Updates = ({ navigation }) => { novelUrl: item.novelUrl, chapterName: item.chapterName, novelId: item.novelId, + novelName: item.novelName, }); const renderItem = ({ item }) => ( diff --git a/src/Screens/Updates/components/UpdatesItem.js b/src/Screens/Updates/components/UpdatesItem.js index e0627678..a3acc0a1 100644 --- a/src/Screens/Updates/components/UpdatesItem.js +++ b/src/Screens/Updates/components/UpdatesItem.js @@ -18,20 +18,24 @@ const UpdateCard = ({ item, theme, onPress }) => { /> {item.novelName} {`${item.chapterName} ยท ${moment( diff --git a/src/Services/Source/source.js b/src/Services/Source/source.js index 9f6ea76d..3d17effa 100644 --- a/src/Services/Source/source.js +++ b/src/Services/Source/source.js @@ -12,7 +12,6 @@ export const fetchSources = async () => { export const fetchNovel = async (sourceId, novelUrl) => { const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}`; - console.log(url); let data = await fetch(url); let res = await data.json(); diff --git a/src/redux/chapter/chapter.actions.js b/src/redux/chapter/chapter.actions.js deleted file mode 100644 index 06bf4a84..00000000 --- a/src/redux/chapter/chapter.actions.js +++ /dev/null @@ -1,36 +0,0 @@ -import { getChapter } from "../../Database/queries/ChapterQueries"; -import { fetchChapter } from "../../Services/Source/source"; -import { SAVE_SCROLL_POSITION } from "../preferences/preference.types"; -import { CHAPTER_LOADING, GET_CHAPTER } from "./chapter.types"; - -export const getChapterAction = - (sourceId, novelUrl, chapterUrl, chapterId) => async (dispatch) => { - dispatch({ type: CHAPTER_LOADING }); - - /** - * Check if chapter is downloaded. - */ - let chapter = await getChapter(chapterId); - - if (!chapter) { - /** - * Fetch chapter from source. - */ - chapter = await fetchChapter(sourceId, novelUrl, chapterUrl); - } - - dispatch({ type: GET_CHAPTER, payload: chapter }); - }; - -export const saveScrollPosition = - (position, percentage, chapterId, novelId) => async (dispatch) => { - dispatch({ - type: SAVE_SCROLL_POSITION, - payload: { - position, - percentage, - chapterId, - novelId, - }, - }); - }; diff --git a/src/redux/chapter/chapter.reducer.js b/src/redux/chapter/chapter.reducer.js deleted file mode 100644 index dfb4a64b..00000000 --- a/src/redux/chapter/chapter.reducer.js +++ /dev/null @@ -1,21 +0,0 @@ -import { CHAPTER_LOADING, GET_CHAPTER } from "./chapter.types"; - -const initialState = { - chapter: {}, - loading: true, -}; - -const chapterReducer = (state = initialState, action) => { - const { type, payload } = action; - - switch (type) { - case CHAPTER_LOADING: - return { ...state, loading: true }; - case GET_CHAPTER: - return { ...state, chapter: payload, loading: false }; - default: - return state; - } -}; - -export default chapterReducer; diff --git a/src/redux/chapter/chapter.types.js b/src/redux/chapter/chapter.types.js deleted file mode 100644 index 1022ae3b..00000000 --- a/src/redux/chapter/chapter.types.js +++ /dev/null @@ -1,2 +0,0 @@ -export const GET_CHAPTER = "GET_CHAPTER"; -export const CHAPTER_LOADING = "CHAPTER_LOADING"; diff --git a/src/redux/preferences/preference.actions.js b/src/redux/preferences/preference.actions.js new file mode 100644 index 00000000..7ac2eed9 --- /dev/null +++ b/src/redux/preferences/preference.actions.js @@ -0,0 +1,14 @@ +import { SAVE_SCROLL_POSITION } from "../preferences/preference.types"; + +export const saveScrollPosition = + (position, percentage, chapterId, novelId) => async (dispatch) => { + dispatch({ + type: SAVE_SCROLL_POSITION, + payload: { + position, + percentage, + chapterId, + novelId, + }, + }); + }; diff --git a/src/redux/source/source.actions.js b/src/redux/source/source.actions.js index 39d373a1..2a315599 100644 --- a/src/redux/source/source.actions.js +++ b/src/redux/source/source.actions.js @@ -11,9 +11,11 @@ export const getSourcesAction = () => async (dispatch) => { }; export const searchSourcesAction = (searchText) => async (dispatch) => { + const res = searchText.toLowerCase(); + dispatch({ type: SEARCH_SOURCES, - payload: searchText, + payload: res, }); }; diff --git a/src/redux/source/source.reducers.js b/src/redux/source/source.reducers.js index 4800ae05..e680145b 100644 --- a/src/redux/source/source.reducers.js +++ b/src/redux/source/source.reducers.js @@ -27,7 +27,7 @@ const sourceReducer = (state = initialState, action) => { return { ...state, search: state.sources.filter((source) => - source.sourceName.includes(payload) + source.sourceName.toLowerCase().includes(payload) ), }; default: