Compare commits

...

29 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 4b5ee1667c Release v1.0.19 2021-05-31 22:54:49 +05:30
Rajarshee Chatterjee d5b3915ef1 Minor fixes 2021-05-31 21:20:17 +05:30
Rajarshee Chatterjee 7b41f10dd6 Add empty view component 2021-05-31 21:19:29 +05:30
Rajarshee Chatterjee b12eef2699 Global search from novel name 2021-05-31 21:18:26 +05:30
Rajarshee Chatterjee 7a8c8ded24 Add option to select text 2021-05-31 21:16:41 +05:30
Rajarshee Chatterjee b6e8b0057b Use tabs in chapter filter sheet 2021-05-31 17:08:48 +05:30
Rajarshee Chatterjee c06ce93581 Global search from library 2021-05-31 16:00:48 +05:30
Rajarshee Chatterjee c4c156eb1d Minor fixes 2021-05-31 15:38:19 +05:30
Rajarshee Chatterjee f1d86b798e UI fixes 2021-05-31 15:38:05 +05:30
Rajarshee Chatterjee 4f1aa7c2cc History fixes 2021-05-31 15:37:07 +05:30
Rajarshee Chatterjee f8199b4e4e Reorganize library filter sheet 2021-05-31 15:32:42 +05:30
Rajarshee Chatterjee 82e7111a40 Add searchbar in history screen (#24) 2021-05-31 09:13:15 +05:30
Rajarshee Chatterjee a15369967f Add custom accent color 2021-05-30 18:19:59 +05:30
Rajarshee Chatterjee 5a936bea02 Rename extensions to sources 2021-05-30 18:19:39 +05:30
Rajarshee Chatterjee 13b49dfd21 Minor UI changes 2021-05-30 18:19:14 +05:30
Rajarshee Chatterjee 689185bab6 Display images if links are present in chapterText 2021-05-30 18:18:25 +05:30
Rajarshee Chatterjee 917dea2261 Fix white border between screens 2021-05-29 18:59:53 +05:30
Rajarshee Chatterjee 6afcdcf96e Reorganize novel details 2021-05-29 17:15:30 +05:30
Rajarshee Chatterjee b74066111f Parse images in chapter and reorganize reader sheet 2021-05-29 12:04:24 +05:30
Rajarshee Chatterjee b42fb6319b Add hide chapter titles 2021-05-28 17:11:23 +05:30
Rajarshee Chatterjee cb05231335 Group updates and history by date 2021-05-28 10:30:07 +05:30
Rajarshee Chatterjee 7819c7dcdb Add selecting chapters logic 2021-05-27 10:37:43 +05:30
Rajarshee Chatterjee 9a7e1f7e53 Refactor Chapter 2021-05-25 10:32:21 +05:30
Rajarshee Chatterjee 2c2e814a0d Fix reader sheet buttons 2021-05-20 10:17:33 +05:30
Rajarshee Chatterjee f4fc3d096f Refactor NovelHeader 2021-05-17 09:50:01 +05:30
Rajarshee Chatterjee d230b64846 Refactor settings and minor fixes 2021-05-16 18:44:37 +05:30
Rajarshee Chatterjee a280fc95e1 Reset progress if marked read 2021-05-16 16:36:42 +05:30
Rajarshee Chatterjee 1f87a50762 Fix scroll to saved progress 2021-05-16 08:58:39 +05:30
Rajarshee Chatterjee 99945aa0d8 Release v1.0.18 2021-05-14 23:29:08 +05:30
86 changed files with 3228 additions and 1779 deletions
+3 -1
View File
@@ -12,7 +12,9 @@ import { fonts } from "./src/Theme/fonts";
const App = () => {
const [loaded] = useFonts(fonts);
useEffect(() => createDB(), []);
useEffect(() => {
createDB();
}, []);
if (!loaded) {
return null;
+7 -3
View File
@@ -3,13 +3,13 @@
"name": "LNReader",
"slug": "LNReader",
"scheme": "lnreader",
"version": "1.0.16",
"version": "1.0.19",
"orientation": "default",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#222831"
"backgroundColor": "#000000"
},
"updates": {
"fallbackToCacheTimeout": 0,
@@ -30,7 +30,11 @@
},
"package": "com.rajarsheechatterjee.LNReader"
},
"androidStatusBar": {
"barStyle": "light-content",
"backgroundColor": "#00000000",
"translucent": true
},
"androidNavigationBar": {
"barStyle": "light-content",
"backgroundColor": "#000000"
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 28 KiB

+21 -33
View File
@@ -3819,14 +3819,6 @@
"uuid": "^3.4.0"
}
},
"expo-app-loading": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/expo-app-loading/-/expo-app-loading-1.0.3.tgz",
"integrity": "sha512-zLheUgBCQoyybJrGxw81MWfxvlnHb7zatjLbmkNosqMwqq3MOPC/P4HwF6qYsrVUFO6PIzb4yq3lFO3RkCCfhw==",
"requires": {
"expo-splash-screen": "~0.10.2"
}
},
"expo-application": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-3.1.2.tgz",
@@ -3914,15 +3906,6 @@
"url-parse": "^1.4.4"
}
},
"expo-media-library": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-12.0.2.tgz",
"integrity": "sha512-0gi4ny4IldYg3uWHjwP+P04VRe5Ea9BYflRILpQWBzcivppzEzTSYFQKUTQEXuZsWB9PA5uTQd2UaB+4q/dcJQ==",
"requires": {
"@expo/config-plugins": "^1.0.18",
"unimodules-permissions-interface": "~6.1.0"
}
},
"expo-notifications": {
"version": "0.11.6",
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.11.6.tgz",
@@ -3969,13 +3952,10 @@
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-9.1.2.tgz",
"integrity": "sha512-IFBQPimeZtdqtvmqQZEmiI0D4j9xE53tSZxLFrFPplNWzgDefk+dP+flof3duf9JgCQTsvOUU2h2/XwjD3vBTA=="
},
"expo-splash-screen": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.10.2.tgz",
"integrity": "sha512-ngelW7g5yFqk3LWbyLDDxi3LYXEfgGFCJddL/Q8S/C1pMUc5foW2j9i/q+akK8i5mjYtSx3+Bk/qbyX92QIF/w==",
"requires": {
"@expo/configure-splash-screen": "0.3.4"
}
"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-sqlite": {
"version": "9.1.0",
@@ -7434,6 +7414,14 @@
"resolved": "https://registry.npmjs.org/react-native-easing-gradient/-/react-native-easing-gradient-0.1.2.tgz",
"integrity": "sha512-oeSJ8nSrCxiCO5mLEVc59Evva4unjvH6AQ8vJFyJ7PIKRybIV5JC+OBU8VnF+V7oQ9dnDBpx2qT5Mh+oxd+/tw=="
},
"react-native-fit-image": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz",
"integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==",
"requires": {
"prop-types": "^15.5.10"
}
},
"react-native-gesture-handler": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz",
@@ -7478,6 +7466,11 @@
"redux-saga": "^1.0.2"
}
},
"react-native-pager-view": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-5.0.12.tgz",
"integrity": "sha512-QmHUnQeP2qcxDofEOnKRmoUue0RaT55lhNJDfcQ1/SNuxif4Q2UyvDfqeItm1+toaE5tVnXqoreZh82FqUqnvw=="
},
"react-native-paper": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-4.8.1.tgz",
@@ -7488,11 +7481,6 @@
"react-native-iphone-x-helper": "^1.3.1"
}
},
"react-native-picker-scrollview": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/react-native-picker-scrollview/-/react-native-picker-scrollview-1.0.1.tgz",
"integrity": "sha512-6N4IuAwLVDLoO3IUCW0QzWJrafXVi7WylofaPg4PNPfPRFOYAsGKMN5yEPw463tWtmbBxP6vZEIOfGl93d9Aeg=="
},
"react-native-reanimated": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-2.1.0.tgz",
@@ -7530,10 +7518,10 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.0.0.tgz",
"integrity": "sha512-35II5oxTaVp3OP8y0eLPOPpQkxG4fRKQ+dL1YSE1we5kCZFOU0l/Rn0T79HbyUu1LPwUZr6lZupPs0ULnRyMuQ=="
},
"react-native-swipe-list-view": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/react-native-swipe-list-view/-/react-native-swipe-list-view-3.2.7.tgz",
"integrity": "sha512-lyTspC/vBco4yRFGou6X8m/ce1eGtSCxGAxlEzpZDin97kwOQTeV+PuQxoGFiCs9wgLQCoFHvRmOuIreSn8fkQ=="
"react-native-tab-view": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-3.0.1.tgz",
"integrity": "sha512-L7qXtYz5Z+NdWyu38zaJrlHA/8H6rNFRZXe3ydym12UEEPPoefbVZ71b9YK2O6QP0u4pUlzMpMVM/Lsz2DJ4jg=="
},
"react-native-vector-icons": {
"version": "7.1.0",
+5 -5
View File
@@ -16,17 +16,16 @@
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "^41.0.1",
"expo-app-loading": "^1.0.1",
"expo-background-fetch": "~9.1.0",
"expo-document-picker": "^9.1.2",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-media-library": "~12.0.2",
"expo-notifications": "~0.11.6",
"expo-permissions": "^12.0.1",
"expo-sharing": "~9.1.2",
"expo-speech": "~9.1.0",
"expo-sqlite": "~9.1.0",
"expo-status-bar": "~1.0.4",
"expo-task-manager": "~9.1.0",
@@ -38,14 +37,14 @@
"react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
"react-native-collapsible-header-views": "^1.1.2",
"react-native-easing-gradient": "^0.1.2",
"react-native-fit-image": "^1.5.5",
"react-native-gesture-handler": "~1.10.2",
"react-native-offline": "^5.7.0",
"react-native-paper": "^4.5.0",
"react-native-picker-scrollview": "^1.0.1",
"react-native-reanimated": "~2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-swipe-list-view": "^3.2.7",
"react-native-tab-view": "^3.0.1",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "~0.13.12",
"react-native-webview": "11.2.3",
@@ -54,7 +53,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",
"react-native-pager-view": "5.0.12"
},
"devDependencies": {
"@babel/core": "~7.9.0"
+2 -1
View File
@@ -10,7 +10,7 @@ import Constants from "expo-constants";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
export const Appbar = ({ title, onBackAction, style }) => {
export const Appbar = ({ title, onBackAction, style, children }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
return (
@@ -24,6 +24,7 @@ export const Appbar = ({ title, onBackAction, style }) => {
title={title}
titleStyle={{ color: theme.textColorPrimary }}
/>
{children}
</MaterialAppbar.Header>
);
};
+21
View File
@@ -0,0 +1,21 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
const ScreenContainer = ({ children, theme }) => (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
{children}
</View>
);
const Row = ({ children, style }) => (
<View style={[styles.row, style]}>{children}</View>
);
export { ScreenContainer, Row };
const styles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
},
});
+5 -2
View File
@@ -2,7 +2,7 @@ import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { useSelector } from "react-redux";
const EmptyView = ({ icon, description }) => {
const EmptyView = ({ icon, description, style, children, iconStyle }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
return (
@@ -11,6 +11,8 @@ const EmptyView = ({ icon, description }) => {
style={[
styles.emptyViewIcon,
{ color: theme.textColorSecondary },
style,
iconStyle,
]}
>
{icon}
@@ -19,10 +21,12 @@ const EmptyView = ({ icon, description }) => {
style={[
styles.emptyViewText,
{ color: theme.textColorSecondary },
style,
]}
>
{description}
</Text>
{children}
</View>
);
};
@@ -37,7 +41,6 @@ const styles = StyleSheet.create({
},
emptyViewIcon: {
fontSize: 45,
fontWeight: "bold",
},
emptyViewText: {
fontWeight: "bold",
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
import { Text, View } from "react-native";
import { IconButton } from "react-native-paper";
import { Row } from "./Common";
import EmptyView from "./EmptyView";
const ErrorView = ({ theme, onRetry, error, openWebView }) => (
<View style={{ flex: 1, justifyContent: "center" }}>
<EmptyView
icon="(; ̄Д ̄)"
description={error}
style={{ color: theme.textColorSecondary }}
>
<IconButton
icon="reload"
size={25}
style={{ margin: 0, marginTop: 24 }}
color={theme.textColorSecondary}
onPress={onRetry}
/>
<Text style={{ color: theme.textColorPrimary }}>Retry</Text>
</EmptyView>
</View>
);
export default ErrorView;
+10 -5
View File
@@ -1,6 +1,8 @@
import React from "react";
import { StyleSheet } from "react-native";
import { List, Divider as PaperDivider } from "react-native-paper";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
const ListSection = ({ children }) => (
<List.Section style={styles.listSection}>{children}</List.Section>
@@ -21,11 +23,13 @@ const ListItem = ({
onPress,
theme,
right,
iconColor,
titleStyle,
style,
}) => (
<List.Item
title={title}
style={[!right && { paddingVertical: 12 }]}
style={style}
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
description={description}
descriptionStyle={{ color: theme.textColorSecondary }}
@@ -41,10 +45,11 @@ const ListItem = ({
}
right={() =>
right && (
<List.Icon
color="#47a84a"
icon={right}
style={{ marginVertical: 0 }}
<MaterialCommunityIcons
name={right}
color={iconColor || "#47a84a"}
size={23}
style={{ marginRight: 16, textAlignVertical: "center" }}
/>
)
}
+12 -3
View File
@@ -9,7 +9,8 @@ import ListView from "./ListView";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const theme = useTheme();
const { displayMode, itemsPerRow } = useSettings();
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
useSettings();
const height = {
1: 550,
@@ -53,6 +54,7 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
);
const unreadBadge = () =>
showUnreadBadges &&
item.chaptersUnread && (
<Text
style={[
@@ -61,6 +63,9 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
]}
>
{item.chaptersUnread}
@@ -68,6 +73,7 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
);
const downloadBadge = () =>
showDownloadBadges &&
item.chaptersDownloaded && (
<Text
style={[
@@ -76,6 +82,9 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
]}
>
{item.chaptersDownloaded}
@@ -83,7 +92,7 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
);
return displayMode !== 2 ? (
<View style={{ flex: 1 / itemsPerRow }}>
<View style={{ flex: 1 / novelsPerRow }}>
<TouchableRipple
borderless
centered
@@ -94,7 +103,7 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
<>
<ImageBackground
source={{ uri: item.novelCover }}
style={{ height: height[itemsPerRow] }}
style={{ height: height[novelsPerRow] }}
imageStyle={[
{ borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
+7 -11
View File
@@ -1,5 +1,6 @@
import React from "react";
import { RefreshControl, StyleSheet, FlatList } from "react-native";
import { StyleSheet, FlatList } from "react-native";
import { useSettings } from "../Hooks/reduxHooks";
const NovelList = ({
@@ -8,20 +9,15 @@ const NovelList = ({
refreshControl,
ListEmptyComponent,
}) => {
const { displayMode, itemsPerRow } = useSettings();
const getItemsPerRow = () => {
if (displayMode === 2) {
return 1;
} else {
return itemsPerRow;
}
};
const { displayMode, novelsPerRow } = useSettings();
const getNovelsPerRow = () => (displayMode === 2 ? 1 : novelsPerRow);
return (
<FlatList
contentContainerStyle={styles.flatListCont}
numColumns={getItemsPerRow()}
key={getItemsPerRow()}
numColumns={getNovelsPerRow()}
key={getNovelsPerRow()}
data={data}
keyExtractor={(item) => item.novelUrl}
renderItem={renderItem}
+6 -2
View File
@@ -8,7 +8,7 @@ export const RadioButtonGroup = ({ children, value, onValueChange }) => (
</MaterialRadioButton.Group>
);
export const RadioButton = ({ value, label, theme }) => (
export const RadioButton = ({ value, label, theme, labelStyle }) => (
<View style={styles.radioButtonContainer}>
<MaterialRadioButton
value={value}
@@ -16,7 +16,11 @@ export const RadioButton = ({ value, label, theme }) => (
uncheckedColor={theme.textColorSecondary}
/>
<Text
style={[styles.radioButtonLabel, { color: theme.textColorPrimary }]}
style={[
styles.radioButtonLabel,
{ color: theme.textColorPrimary },
labelStyle,
]}
>
{label}
</Text>
+10 -3
View File
@@ -4,7 +4,7 @@ import { TouchableRipple, IconButton } from "react-native-paper";
import Constants from "expo-constants";
import { useSettings } from "../Hooks/reduxHooks";
import { useDispatch } from "react-redux";
import { setDisplayMode } from "../redux/settings/settings.actions";
import { setAppSettings } from "../redux/settings/settings.actions";
export const Searchbar = ({
theme,
@@ -20,6 +20,7 @@ export const Searchbar = ({
onSubmitEditing,
migrate,
onPressMigrate,
filter,
}) => {
const searchRef = useRef(null);
const dispatch = useDispatch();
@@ -88,14 +89,20 @@ export const Searchbar = ({
style={{ marginRight: 0 }}
size={23}
onPress={() =>
dispatch(setDisplayMode(getDisplayMode()))
dispatch(
setAppSettings("displayMode", getDisplayMode())
)
}
/>
)}
{right && (
<IconButton
icon={right}
color={theme.textColorSecondary}
color={
!filter
? theme.textColorSecondary
: theme.filterColor
}
style={{ marginRight: 0 }}
size={23}
onPress={onPressRight}
-8
View File
@@ -1,8 +0,0 @@
import React from "react";
import { View, Text as RNText, Image, StyleSheet } from "react-native";
const Text = () => {};
export default UpdateCard;
const styles = StyleSheet.create({});
+33 -2
View File
@@ -40,6 +40,7 @@ const getChaptersQuery = (sort, filter) =>
}`;
export const getChapters = (novelId, sort, filter) => {
// console.log(getChaptersQuery(sort, filter));
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
@@ -56,7 +57,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 +72,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) => {
@@ -160,7 +191,7 @@ export const deleteChapter = async (chapterId) => {
tx.executeSql(
updateIsDownloadedQuery,
[chapterId],
(tx, res) => console.log(`Chaptermarked not downapdeod deleted`),
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
tx.executeSql(
+12 -6
View File
@@ -8,7 +8,10 @@ const getHistoryQuery = `
ON history.historyChapterId = chapters.chapterId
JOIN novels
ON history.historyNovelId = novels.novelId
ORDER BY history.historyTimeRead DESC`;
GROUP BY novels.novelId
HAVING history.historyTimeRead = MAX(history.historyTimeRead)
ORDER BY history.historyTimeRead DESC
`;
export const getHistory = async () => {
return new Promise((resolve, reject) => {
@@ -41,15 +44,18 @@ export const insertHistory = async (novelId, chapterId) => {
});
};
export const deleteHistory = async (novelId) => {
export const deleteHistory = async (historyId) => {
db.transaction((tx) => {
tx.executeSql("DELETE FROM history WHERE historyNovelId = ?", [
novelId,
]);
tx.executeSql(
"DELETE FROM history WHERE historyId = ?",
[historyId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
export const deleteAllHistory = () => {
export const deleteAllHistory = async () => {
db.transaction((tx) => {
tx.executeSql("DELETE FROM history; VACCUM;");
});
+24 -22
View File
@@ -38,32 +38,34 @@ export const getLibrary = (sort, filter) => {
);
};
// const searchLibraryQuery = ``;
const searchLibraryQuery = (searchText, sort, filter) =>
`
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
FROM chapters
WHERE chapters.read = 0
GROUP BY chapters.novelId
) AS C
ON novels.novelId = C.novelId
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
FROM chapters
WHERE chapters.downloaded = 1
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%' OR source LIKE '%${searchText}%' ${
filter ? "AND " + filter : ""
}
${sort ? "ORDER BY " + sort : ""} `;
export const searchLibrary = (searchText) => {
export const searchLibrary = (searchText, sort, filter) => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
`
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
FROM chapters
WHERE chapters.read = 0
GROUP BY chapters.novelId
) AS C
ON novels.novelId = C.novelId
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
FROM chapters
WHERE chapters.downloaded = 1
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%'
GROUP BY novels.novelId
`,
searchLibraryQuery(searchText, sort, filter),
null,
(txObj, { rows: { _array } }) => resolve(_array),
(txObj, error) => console.log("Error ", error)
+3 -2
View File
@@ -2,13 +2,14 @@ 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
JOIN novels
ON updates.novelId = novels.novelId
ORDER BY updates.updateTime DESC`;
ORDER BY updates.updateTime DESC
`;
export const getUpdates = async () => {
return new Promise((resolve, reject) => {
+2 -2
View File
@@ -1,8 +1,8 @@
export const createHistoryTableQuery = `
CREATE TABLE IF NOT EXISTS history(
historyId INTEGER PRIMARY KEY AUTOINCREMENT,
historyChapterId INTEGER NOT NULL,
historyNovelId INTEGER NOT NULL UNIQUE,
historyChapterId INTEGER NOT NULL UNIQUE,
historyNovelId INTEGER NOT NULL,
historyTimeRead DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (historyChapterId) REFERENCES chapters(chapterId)
ON DELETE CASCADE
+1 -1
View File
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
};
const isNewVersion = (versionTag) => {
let currentVersion = "v1.0.17";
let currentVersion = "v1.0.19";
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, "");
+21 -6
View File
@@ -1,4 +1,5 @@
import { shallowEqual, useSelector } from "react-redux";
import { showChapterTitlesAction } from "../redux/novel/novel.actions";
const useTheme = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
@@ -13,11 +14,9 @@ const useReaderSettings = () => {
};
const useSettings = () => {
const { displayMode, itemsPerRow } = useSelector(
(state) => state.settingsReducer
);
const settings = useSelector((state) => state.settingsReducer);
return { displayMode, itemsPerRow };
return settings;
};
const useLibrary = () => {
@@ -46,7 +45,7 @@ const findNovel = (novelId) => {
};
const usePreferences = (novelId) => {
let sort, filter, position;
let sort, filter, position, showChapterTitles;
const novel = findNovel(novelId);
@@ -54,9 +53,10 @@ const usePreferences = (novelId) => {
sort = novel.sort;
filter = novel.filter;
position = novel.position;
showChapterTitles = novel.showChapterTitles;
}
return { sort, filter, position };
return { sort, filter, position, showChapterTitles };
};
const useSavedSettings = () => {
@@ -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,
};
+76 -72
View File
@@ -1,7 +1,8 @@
import React from "react";
import { View } from "react-native";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { useTheme } from "../Hooks/reduxHooks";
import Library from "../Screens/Library/Library";
import Updates from "../Screens/Updates/Updates";
@@ -9,83 +10,86 @@ import History from "../Screens/History/History";
import Browse from "../Screens/Browse/Browse";
import More from "../Screens/More/More";
import { useTheme } from "../Hooks/reduxHooks";
const Tab = createMaterialBottomTabNavigator();
const BottomNavigator = () => {
const theme = useTheme();
return (
<Tab.Navigator
// shifting={false}
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccent}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="History"
component={History}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="history"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
/>
</Tab.Navigator>
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Tab.Navigator
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccent}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="History"
component={History}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="history"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
/>
</Tab.Navigator>
</View>
);
};
+4 -6
View File
@@ -33,8 +33,10 @@ const MainNavigator = () => {
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
return (
<NavigationContainer>
<View style={{ flex: 1, backgroundColor: theme.colorPrimary }}>
<NavigationContainer
theme={{ colors: { background: theme.colorPrimaryDark } }}
>
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen
@@ -45,10 +47,6 @@ const MainNavigator = () => {
name="Novel"
component={Novel}
options={{
headerTitle: "",
headerShown: true,
headerTransparent: true,
headerTintColor: "white",
...TransitionPresets.RevealFromBottomAndroid,
}}
/>
+42 -6
View File
@@ -1,24 +1,60 @@
import React from "react";
import { View } from "react-native";
import { useTheme } from "../Hooks/reduxHooks";
import { createStackNavigator } from "@react-navigation/stack";
import About from "../Screens/More/About";
import Settings from "../Screens/More/Settings";
import Trackers from "../Screens/More/Trackers";
import { View } from "react-native";
import { useTheme } from "../Hooks/reduxHooks";
import TrackerSettings from "../Screens/More/TrackerSettings";
import ReaderSettings from "../Screens/More/ReaderSettings";
import BackupSettings from "../Screens/More/BackupSettings";
import AdvancedSettings from "../Screens/More/AdvancedSettings";
import GeneralSettings from "../Screens/More/GeneralSettings";
const Stack = createStackNavigator();
const stackNavigatorConfig = { headerShown: false };
const SettingsStack = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen
name="GeneralSettings"
component={GeneralSettings}
/>
<Stack.Screen
name="ReaderSettings"
component={ReaderSettings}
/>
<Stack.Screen
name="TrackerSettings"
component={TrackerSettings}
/>
<Stack.Screen
name="BackupSettings"
component={BackupSettings}
/>
<Stack.Screen
name="AdvancedSettings"
component={AdvancedSettings}
/>
</Stack.Navigator>
</View>
);
};
const MoreStack = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimary }}>
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen name="Trackers" component={Trackers} />
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
</Stack.Navigator>
</View>
+1 -1
View File
@@ -81,7 +81,7 @@ const Browse = ({ navigation }) => {
clearSearchbar={clearSearchbar}
right="book-search"
onPressRight={() => navigation.navigate("GlobalSearch")}
migrate="weather-cloudy-arrow-right"
migrate="swap-vertical-variant"
onPressMigrate={() => navigation.navigate("Migration")}
/>
+19 -3
View File
@@ -17,11 +17,16 @@ import GlobalSearchNovelList from "./components/GlobalSearchNovelList";
import { ProgressBar } from "react-native-paper";
import { showToast } from "../../Hooks/showToast";
const GlobalSearch = ({ navigation }) => {
const GlobalSearch = ({ route, navigation }) => {
const theme = useTheme();
let novelName = "";
if (route.params) {
novelName = route.params.novelName;
}
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState("");
const [searchText, setSearchText] = useState(novelName);
const [searchResults, setSearchResults] = useState("");
const { sources, pinned } = useSelector((state) => state.sourceReducer);
const [progress, setProgress] = useState(0);
@@ -32,6 +37,10 @@ const GlobalSearch = ({ navigation }) => {
(source) => pinned.indexOf(source.sourceId) !== -1
);
useEffect(() => {
novelName && onSubmitEditing();
}, []);
const clearSearchbar = () => {
setSearchText("");
};
@@ -125,7 +134,14 @@ const GlobalSearch = ({ navigation }) => {
clearSearchbar={clearSearchbar}
/>
{progress < 1 && progress > 0 && pinned && (
<ProgressBar color={theme.colorAccent} progress={progress} />
<ProgressBar
color={
progress > 0.9
? theme.colorPrimaryDark
: theme.colorAccent
}
progress={progress}
/>
)}
<FlatList
contentContainerStyle={{
+6 -2
View File
@@ -70,8 +70,12 @@ const GlobalSearch = ({ navigation, route }) => {
},
]);
setLoading(false);
});
setProgress((progress) => progress + 1 / pinnedSources.length);
})
.finally(() =>
setProgress(
(progress) => progress + 1 / pinnedSources.length
)
);
}, 1000 * index)
);
};
@@ -2,7 +2,7 @@ import React from "react";
import { StyleSheet, View, Text, ImageBackground } from "react-native";
import { TouchableRipple } from "react-native-paper";
import { useSettings, useTheme } from "../../../Hooks/reduxHooks";
import { useTheme } from "../../../Hooks/reduxHooks";
const GlobalSearchNovelCover = ({ item, onPress, libraryStatus }) => {
const theme = useTheme();
+187 -126
View File
@@ -1,67 +1,115 @@
import React, { useState, useEffect, useRef } from "react";
import {
StyleSheet,
View,
Text,
ActivityIndicator,
ScrollView,
} 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 { StyleSheet, View, Text, ActivityIndicator } from "react-native";
import { useDispatch } from "react-redux";
import { parseChapterNumber } from "../../Services/updates";
import Constants from "expo-constants";
import { IconButton, 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 {
errorTextColor,
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";
import EmptyView from "../../Components/EmptyView";
import FitImage from "react-native-fit-image";
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 [error, setError] = useState();
const [scrollPercentage, setScrollPercentage] = useState(0);
const [firstLayout, setFirstLayout] = useState(true);
const [selectText, setSelectText] = useState(false);
const [images, setImages] = useState([]);
const getChapter = async (chapterId) => {
try {
if (chapterId) {
const chapterDownloaded = await getChapterFromDB(chapterId);
if (chapterDownloaded) {
setChapter(chapterDownloaded);
parseImages(chapterDownloaded.chapterText);
} else {
const res = await fetchChapter(
sourceId,
novelUrl,
chapterUrl
);
parseImages(res.chapterText);
setChapter(res);
}
} else {
const res = await fetchChapter(sourceId, novelUrl, chapterUrl);
parseImages(res.chapterText);
setChapter(res);
}
setLoading(false);
} catch (error) {
setError(error.message);
setLoading(false);
showToast(error.message);
}
};
const parseImages = (chapterText) => {
let imageLinks = chapterText.match(/\[https.*?\]/g, "");
let chapterTextWithoutLinks;
if (imageLinks) {
imageLinks = imageLinks.map((link) => link.slice(1, -1));
setImages(imageLinks);
}
};
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,
@@ -74,54 +122,24 @@ 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
)
);
const navigateToPreviousChapter = () =>
navigation.replace("Chapter", {
chapterUrl: chapter.prevChapter,
chapterId: chapterId - 1,
sourceId,
novelUrl,
novelId,
chapterName: chapter.chapterName,
});
const navigateToNextChapter = () =>
navigation.replace("Chapter", {
chapterUrl: chapter.nextChapter,
sourceId,
novelUrl,
novelId,
chapterId: chapterId + 1,
chapterName: chapter.chapterName,
});
const readerStyles = [
{
paddingVertical: 16,
paddingBottom: 32,
fontSize: reader.textSize,
color: readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
},
];
chapterNumber &&
Number.isInteger(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;
@@ -130,7 +148,7 @@ const Chapter = ({ route, navigation }) => {
);
setScrollPercentage(percentage);
dispatch(saveScrollPosition(position, percentage, chapterId, novelId));
dispatch(saveScrollPosition(offsetY, percentage, chapterId, novelId));
if (isCloseToBottom(nativeEvent)) {
dispatch(markChapterReadAction(chapterId, novelId));
@@ -138,58 +156,86 @@ const Chapter = ({ route, navigation }) => {
}
};
const scrollToSavedProgress = () => {
if (position && firstLayout) {
position.percentage < 100 &&
scrollViewRef.getNode().scrollTo({
x: 0,
y: position.position,
animated: false,
});
setFirstLayout(false);
}
};
const readerStyles = [
{
paddingVertical: 16,
paddingBottom: 32,
fontSize: reader.textSize,
color: readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize, reader.lineHeight),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
},
];
return (
<>
<CollapsibleHeaderScrollView
headerContainerBackgroundColor="rgba(0,0,0,0.4)"
headerContainerBackgroundColor="rgba(0,0,0,0.5)"
ref={(ref) => (scrollViewRef = ref)}
CollapsibleHeaderComponent={
<Appbar.Header
style={{ backgroundColor: "transparent", elevation: 0 }}
>
<Appbar.BackAction
onPress={() => navigation.goBack()}
color="#FFFFFF"
size={26}
style={{ marginRight: 0 }}
/>
<Appbar.Content
title={loading ? "Chapter" : chapter.chapterName}
titleStyle={{ color: "#FFFFFF" }}
/>
{!loading && (
<>
<Appbar.Action
icon="chevron-left"
size={26}
disabled={!chapter.prevChapter}
onPress={navigateToPreviousChapter}
color="#FFFFFF"
/>
<Appbar.Action
icon="chevron-right"
size={26}
disabled={!chapter.nextChapter}
onPress={navigateToNextChapter}
color="#FFFFFF"
/>
</>
)}
<Appbar.Action
icon="dots-vertical"
size={26}
onPress={() => readerSheetRef.current.show()}
color="#FFFFFF"
/>
</Appbar.Header>
<ChapterAppbar
navigation={navigation}
sourceId={sourceId}
novelId={novelId}
novelUrl={novelUrl}
novelName={novelName}
chapterName={chapterName}
chapterId={chapterId}
chapter={chapter}
theme={theme}
readerSheetRef={readerSheetRef}
/>
}
headerHeight={Constants.statusBarHeight + 60}
headerHeight={Constants.statusBarHeight + 56}
contentContainerStyle={[
styles.container,
styles.screenContainer,
{ backgroundColor: readerBackground(reader.theme) },
]}
onScroll={onScroll}
showsVerticalScrollIndicator={false}
>
{loading ? (
{error ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<EmptyView
icon="(-_-;)・・・"
description={error}
style={{ color: errorTextColor(reader.theme) }}
>
<IconButton
icon="reload"
size={25}
style={{ margin: 0, marginTop: 16 }}
color={errorTextColor(reader.theme)}
onPress={() => {
getChapter(chapterId);
setLoading(true);
setError();
}}
/>
<Text
style={{ color: errorTextColor(reader.theme) }}
>
Retry
</Text>
</EmptyView>
</View>
) : loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
size={50}
@@ -197,9 +243,22 @@ const Chapter = ({ route, navigation }) => {
/>
</View>
) : (
<Text style={readerStyles}>
{chapter.chapterText.trim()}
</Text>
<View style={{ flex: 1 }}>
{images.length > 0 &&
images.map((image, index) => (
<FitImage source={{ uri: image }} />
))}
<Text
style={readerStyles}
onLayout={scrollToSavedProgress}
selectable={selectText}
>
{chapter.chapterText
.trim()
.replace(/\[https.*?\]/g, "")}
</Text>
</View>
)}
<Portal>
<ReaderSheet
@@ -207,6 +266,8 @@ const Chapter = ({ route, navigation }) => {
reader={reader}
dispatch={dispatch}
bottomSheetRef={readerSheetRef}
selectText={selectText}
setSelectText={setSelectText}
/>
</Portal>
</CollapsibleHeaderScrollView>
@@ -225,7 +286,7 @@ const Chapter = ({ route, navigation }) => {
export default Chapter;
const styles = StyleSheet.create({
container: {
screenContainer: {
flexGrow: 1,
paddingVertical: 10,
},
@@ -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,
});
const navigateToNextChapter = () =>
navigation.replace("Chapter", {
chapterUrl: nextChapter.chapterUrl,
sourceId,
novelUrl,
novelId,
chapterId: nextChapter.chapterId,
chapterName: nextChapter.chapterName,
novelName,
});
return (
<Appbar.Header style={{ backgroundColor: "transparent", elevation: 0 }}>
<Appbar.BackAction
onPress={() => navigation.goBack()}
color="#FFFFFF"
size={26}
style={{ marginRight: 0 }}
/>
<Appbar.Content
title={novelName}
titleStyle={{ color: "#FFFFFF" }}
subtitle={chapterName}
subtitleStyle={{ color: "rgba(255, 255, 255, 0.7)" }}
/>
<Appbar.Action
icon="chevron-left"
size={26}
disabled={!prevChapter}
onPress={navigateToPrevChapter}
color="#FFFFFF"
/>
<Appbar.Action
icon="chevron-right"
size={26}
disabled={!nextChapter}
onPress={navigateToNextChapter}
color="#FFFFFF"
/>
<Appbar.Action
icon="dots-vertical"
size={26}
onPress={() => readerSheetRef.current.show()}
color="#FFFFFF"
/>
</Appbar.Header>
);
};
export default ChapterAppbar;
+288 -151
View File
@@ -1,39 +1,42 @@
import React from "react";
import React, { useState } from "react";
import { StyleSheet, View, Text, Dimensions } from "react-native";
import { ToggleButton, IconButton, Chip } from "react-native-paper";
import { ToggleButton, IconButton, Menu, Switch } from "react-native-paper";
import Slider from "@react-native-community/slider";
import Bottomsheet from "rn-sliding-up-panel";
import { fonts } from "../../../Services/utils/constants";
import {
setReaderFont,
updateReaderPadding,
updateReaderTextAlign,
updateReaderTextSize,
updateReaderTheme,
} from "../../../redux/settings/settings.actions";
import { setReaderSettings } from "../../../redux/settings/settings.actions";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
import { Row } from "../../../Components/Common";
const ReaderSheet = ({
theme,
reader,
dispatch,
bottomSheetRef,
selectText,
setSelectText,
}) => {
const [fontMenu, setFontMenu] = useState(false);
const openFontMenu = () => setFontMenu(true);
const closeFontMenu = () => setFontMenu(false);
const ReaderSettingTitle = ({ title }) => (
<Text style={styles.title}>{title}</Text>
);
const ReaderSheet = ({ theme, reader, dispatch, bottomSheetRef }) => {
return (
<Bottomsheet
ref={bottomSheetRef}
draggableRange={{ top: 400, bottom: 0 }}
snappingPoints={[0, 400]}
draggableRange={{ top: 420, bottom: 0 }}
snappingPoints={[0, 420]}
showBackdrop={true}
backdropOpacity={0}
>
<View style={styles.contentContainer}>
<BottomSheetHandle theme={theme} />
<View style={styles.readerSettingsContainer}>
<Text
style={{
color: "#FFFFFF",
fontWeight: "bold",
}}
>
Text Size
</Text>
<ReaderSettingTitle title="Text Size" />
<Slider
style={{
width: Dimensions.get("window").width,
@@ -47,212 +50,338 @@ const ReaderSheet = ({ theme, reader, dispatch, bottomSheetRef }) => {
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccent}
onValueChange={(value) =>
dispatch(updateReaderTextSize(value))
dispatch(setReaderSettings("textSize", value))
}
/>
<View
<Row
style={{
flexDirection: "row",
justifyContent: "center",
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<Text
style={{
color: "#FFFFFF",
fontWeight: "bold",
}}
>
Reader Theme
</Text>
<ReaderSettingTitle title="Reader Theme" />
<Row>
<ToggleButton.Row
onValueChange={(value) =>
dispatch(updateReaderTheme(value))
}
onValueChange={(value) => {
dispatch(
setReaderSettings(
"theme",
value ?? reader.theme
)
);
}}
value={reader.theme}
style={{ marginTop: 10 }}
>
<ToggleButton
icon="format-text"
icon={reader.theme === 1 && "check"}
color="#FFFFFF"
value={1}
style={{
backgroundColor: "#000000",
marginHorizontal: 10,
borderRadius: 50,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
}}
/>
<ToggleButton
icon="format-text"
icon={reader.theme === 4 && "check"}
color="#FFFFFF"
value={4}
style={{
backgroundColor: "#444444",
marginHorizontal: 10,
borderWidth: 0,
borderRadius: 50,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
}}
/>
<ToggleButton
icon={reader.theme === 2 && "check"}
color="#000000"
value={2}
style={{
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
borderRadius: 50,
}}
/>
<ToggleButton
icon="format-text"
icon={reader.theme === 3 && "check"}
color="#000000"
value={3}
style={{
backgroundColor: "#F4ECD8",
marginHorizontal: 10,
borderRadius: 50,
borderTopLeftRadius: 50,
borderBottomLeftRadius: 50,
}}
/>
</ToggleButton.Row>
</View>
<View
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<Text
style={{
color: "#FFFFFF",
fontWeight: "bold",
}}
>
Text Align
</Text>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Text Align" />
<Row>
<ToggleButton.Row
onValueChange={(value) =>
dispatch(
updateReaderTextAlign(value ?? "left")
setReaderSettings(
"textAlign",
value ?? reader.textAlign
)
)
}
value={reader.textAlign}
style={{ marginTop: 10 }}
>
<ToggleButton
icon="format-align-left"
color="#000000"
color={
reader.textAlign === "left"
? theme.colorAccent
: "#FFFFFF"
}
value="left"
style={{
backgroundColor: "#FFFFFF",
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
}}
/>
<ToggleButton
icon="format-align-justify"
color="#000000"
color={
reader.textAlign === "justify"
? theme.colorAccent
: "#FFFFFF"
}
value="justify"
style={{
backgroundColor: "#FFFFFF",
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderRadius: 4,
}}
/>
<ToggleButton
icon="format-align-right"
color="#000000"
color={
reader.textAlign === "right"
? theme.colorAccent
: "#FFFFFF"
}
value="right"
style={{
backgroundColor: "#FFFFFF",
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
}}
/>
</ToggleButton.Row>
</View>
</View>
<Text
style={{
color: "#FFFFFF",
fontWeight: "bold",
marginTop: 10,
}}
>
Padding
</Text>
<View
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
dispatch(
updateReaderPadding(reader.padding - 1)
)
}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 50,
}}
>
{`${reader.padding}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
dispatch(
updateReaderPadding(reader.padding + 1)
)
}
/>
</View>
<Text
style={{
color: "#FFFFFF",
fontWeight: "bold",
marginTop: 10,
}}
>
Font Style
</Text>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "center",
}}
>
{fonts.map((font, index) => (
<Chip
key={index}
<ReaderSettingTitle title="Padding" />
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
dispatch(setReaderFont(font.fontFamily))
dispatch(
setReaderSettings(
"padding",
reader.padding - 1
)
)
}
textStyle={{ fontFamily: font.fontFamily }}
style={{ marginVertical: 0 }}
/>
<Text
style={{
borderWidth: 0,
marginHorizontal: 5,
marginVertical: 5,
color: "#FFFFFF",
paddingHorizontal: 24,
}}
selected={
reader.fontFamily === font.fontFamily
? true
: false
}
>
{font.name}
</Chip>
))}
</View>
{`${reader.padding}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"padding",
reader.padding + 1
)
)
}
style={{ marginVertical: 0 }}
/>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Line Height" />
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={
reader.lineHeight <= 1.3 ? true : false
}
onPress={() =>
dispatch(
setReaderSettings(
"lineHeight",
reader.lineHeight - 0.1
)
)
}
style={{ marginVertical: 0 }}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 24,
}}
>
{`${Math.round(reader.lineHeight * 10) / 10}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.lineHeight >= 2 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"lineHeight",
reader.lineHeight + 0.1
)
)
}
style={{ marginVertical: 0 }}
/>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Font Style" />
<Menu
visible={fontMenu}
onDismiss={closeFontMenu}
contentStyle={{ backgroundColor: theme.menuColor }}
anchor={
<Text
onPress={openFontMenu}
style={{
color: "white",
paddingHorizontal: 16,
fontSize: 16,
fontFamily: reader.fontFamily,
}}
>
{fonts.find(
(font) =>
font.fontFamily ===
reader.fontFamily
).name || "Default"}
</Text>
}
>
{fonts.map((font) => (
<Menu.Item
key={font.fontFamily}
onPress={() =>
dispatch(
setReaderSettings(
"fontFamily",
font.fontFamily
)
)
}
icon={
font.fontFamily === reader.fontFamily &&
"check"
}
title={font.name}
style={{ backgroundColor: theme.menuColor }}
titleStyle={{
color: theme.textColorPrimary,
}}
/>
))}
</Menu>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<View>
<ReaderSettingTitle title="Select Text " />
<Text
style={{
fontSize: 14,
color: "white",
}}
>
(Justify does not work with selectable text)
</Text>
</View>
<Switch
value={selectText}
onValueChange={setSelectText}
color={theme.colorAccent}
/>
</Row>
</View>
</View>
</Bottomsheet>
);
};
const mapStateToProps = (state) => ({});
export default ReaderSheet;
const styles = StyleSheet.create({
@@ -260,7 +389,15 @@ const styles = StyleSheet.create({
flex: 1,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
backgroundColor: "rgba(0,0,0,0.4)",
backgroundColor: "rgba(0,0,0,0.5)",
},
readerSettingsContainer: {
flex: 1,
alignItems: "center",
paddingTop: 30,
},
title: {
color: "#FFFFFF",
fontWeight: "bold",
},
readerSettingsContainer: { flex: 1, alignItems: "center", paddingTop: 30 },
});
+15 -11
View File
@@ -3,25 +3,29 @@ export const readerBackground = (val) => {
1: "#000000",
2: "#FFFFFF",
3: "#F4ECD8",
4: "#444444",
};
return backgroundColor[val];
return backgroundColor[val] ?? "#FFFFFF";
};
export const readerTextColor = (val) => {
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "#000000";
const textColor =
val === 1 ? "rgba(255,255,255,0.7)" : val === 4 ? "#FFFFFF" : "#000000";
return textColor;
};
export const readerLineHeight = (val) => {
const lineHeight = {
12: 20,
14: 22,
16: 25,
18: 26,
20: 28,
};
export const readerLineHeight = (fontSize, lineHeightMultiplier) => {
let lineHeight = fontSize * lineHeightMultiplier;
return lineHeight[val];
lineHeight = Math.round(lineHeight);
return lineHeight;
};
export const errorTextColor = (val) => {
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.54)";
return textColor;
};
+29 -16
View File
@@ -9,6 +9,7 @@ import { Searchbar } from "../../Components/Searchbar";
import { useTheme, useLibrary } from "../../Hooks/reduxHooks";
import NovelList from "../../Components/NovelList";
import { showToast } from "../../Hooks/showToast";
import ErrorView from "../../Components/ErrorView";
const Extension = ({ navigation, route }) => {
const { sourceId, sourceName, sourceUrl } = route.params;
@@ -18,6 +19,7 @@ const Extension = ({ navigation, route }) => {
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState();
const [error, setError] = useState();
const [searchText, setSearchText] = useState("");
@@ -37,18 +39,21 @@ const Extension = ({ navigation, route }) => {
}
};
const getNovels = () => {
fetch(getSourceUrl())
.then((response) => response.json())
.then((json) => {
setNovels(json);
setLoading(false);
})
.catch((error) => {
setNovels([]);
setLoading(false);
showToast(error.message);
});
const getNovels = async () => {
try {
const url = getSourceUrl();
const res = await fetch(url);
const data = await res.json();
setNovels(data);
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
const clearSearchbar = () => {
@@ -97,10 +102,18 @@ const Extension = ({ navigation, route }) => {
/>
);
const listEmptyComponent = () =>
novels.length === 0 && (
<EmptyView icon="(; ̄Д ̄)" description="No results found" />
);
const listEmptyComponent = () => (
<ErrorView
error={error || "No results found"}
onRetry={() => {
getNovels();
setLoading(true);
setError();
}}
openWebView={() => WebBrowser.openBrowserAsync(sourceUrl)}
theme={theme}
/>
);
return (
<View
+146 -62
View File
@@ -1,87 +1,171 @@
import React, { useEffect } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import React, { useState, useEffect } from "react";
import { StyleSheet, FlatList, ActivityIndicator, Text } from "react-native";
import { Appbar } from "../../Components/Appbar";
import HistoryCard from "./components/HistoryCard";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import EmptyView from "../../Components/EmptyView";
import {
getHistoryAction,
deleteHistoryAction,
clearAllHistoryAction,
} from "../../redux/history/history.actions";
const History = ({
navigation,
theme,
history,
loading,
getHistoryAction,
deleteHistoryAction,
}) => {
import moment from "moment";
import { dateFormat } from "../../Services/utils/constants";
import { ScreenContainer } from "../../Components/Common";
import { Searchbar } from "../../Components/Searchbar";
import { useTheme } from "../../Hooks/reduxHooks";
import { Button, Dialog, Portal } from "react-native-paper";
const History = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { history, loading } = useSelector((state) => state.historyReducer);
const [searchText, setSearchText] = useState("");
const [searchResults, setSearchResults] = useState([]);
/**
* Confirm Clear History Dialog
*/
const [visible, setVisible] = useState(false);
const showDialog = () => setVisible(true);
const hideDialog = () => setVisible(false);
useEffect(() => {
getHistoryAction();
dispatch(getHistoryAction());
}, [getHistoryAction]);
const renderHistoryCard = ({ item }) => (
<HistoryCard
dispatch={dispatch}
item={item}
theme={theme}
deleteHistoryAction={deleteHistoryAction}
navigation={navigation}
/>
);
const clearSearchbar = () => setSearchText("");
const onChangeText = (text) => {
setSearchText(text);
let results = [];
text !== "" &&
history.map((item) => {
const date = item.date;
const novels = item.novels.filter((novel) =>
novel.novelName.toLowerCase().includes(text.toLowerCase())
);
if (novels.length > 0) {
results.push({
date,
novels,
});
}
});
setSearchResults(results);
};
return (
<>
<Appbar title="History" />
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
contentContainerStyle={{ flexGrow: 1, padding: 8 }}
data={history}
keyExtractor={(item) => item.novelId.toString()}
renderItem={renderHistoryCard}
ListFooterComponent={
loading && (
<ActivityIndicator
size="small"
color={theme.colorAccent}
/>
)
}
ListEmptyComponent={
!loading && (
<EmptyView
icon="(˘・_・˘)"
description="Nothing read recently"
/>
)
}
/>
</View>
</>
<ScreenContainer theme={theme}>
<Searchbar
placeholder="Search History"
searchText={searchText}
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
left="magnify"
theme={theme}
right="trash-can-outline"
onPressRight={showDialog}
/>
<FlatList
contentContainerStyle={{
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 16,
}}
data={searchText ? searchResults : history}
keyExtractor={(index) => index.toString()}
renderItem={({ item }) => (
<FlatList
keyExtractor={(item, index) => index.toString()}
data={item.novels}
renderItem={renderHistoryCard}
ListHeaderComponent={
<Text
style={{
textTransform: "uppercase",
paddingVertical: 8,
color: theme.textColorSecondary,
}}
>
{moment(item.date).calendar(null, dateFormat)}
</Text>
}
/>
)}
ListFooterComponent={
loading && (
<ActivityIndicator
size="small"
color={theme.colorAccent}
/>
)
}
ListEmptyComponent={
<EmptyView
icon="(˘・_・˘)"
description="Nothing read recently"
/>
}
/>
<Portal>
<Dialog
visible={visible}
onDismiss={hideDialog}
style={{
borderRadius: 6,
backgroundColor: theme.colorPrimary,
}}
>
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
color: theme.textColorPrimary,
}}
>
Are you sure? All history will be lost.
</Dialog.Title>
<Dialog.Actions>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={hideDialog}
>
Cancel
</Button>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => {
dispatch(clearAllHistoryAction());
hideDialog();
}}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</ScreenContainer>
);
};
const mapStateToProps = (state) => ({
theme: state.settingsReducer.theme,
history: state.historyReducer.history,
loading: state.historyReducer.loading,
});
export default History;
export default connect(mapStateToProps, {
getHistoryAction,
deleteHistoryAction,
})(History);
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
const styles = StyleSheet.create({});
+14 -8
View File
@@ -7,9 +7,10 @@ import moment from "moment";
import { useDispatch } from "react-redux";
import { setNovel } from "../../../redux/novel/novel.actions";
import { parseChapterNumber } from "../../../Services/utils/helpers";
import { deleteHistoryAction } from "../../../redux/history/history.actions";
const HistoryCard = ({ item, deleteHistoryAction, navigation, theme }) => {
const dispatch = useDispatch();
const HistoryCard = ({ item, navigation, theme, dispatch }) => {
return (
<TouchableRipple
style={styles.historyCard}
@@ -44,9 +45,11 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation, theme }) => {
}}
numberOfLines={1}
>
{`${item.chapterName} - ${moment(
item.historyTimeRead
).calendar()}`}
{`Ch. ${parseChapterNumber(
item.chapterName
)} - ${moment(item.historyTimeRead)
.format("h:mm a")
.toUpperCase()}`}
</Text>
</View>
<View
@@ -62,13 +65,15 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation, theme }) => {
style={{
marginRight: 0,
}}
onPress={() => deleteHistoryAction(item.novelId)}
onPress={() =>
dispatch(deleteHistoryAction(item.historyId))
}
/>
<IconButton
icon="play"
size={24}
color={theme.textColorPrimary}
style={{ marginRight: 10 }}
style={{ marginRight: 0 }}
onPress={() =>
navigation.navigate("Chapter", {
chapterId: item.historyChapterId,
@@ -77,6 +82,7 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation, theme }) => {
novelUrl: item.novelUrl,
novelId: item.novelId,
chapterName: item.chapterName,
novelName: item.novelName,
})
}
/>
@@ -91,7 +97,7 @@ export default HistoryCard;
const styles = StyleSheet.create({
historyCard: {
marginBottom: 15,
marginVertical: 8,
borderRadius: 4,
flexDirection: "row",
alignItems: "center",
+29 -12
View File
@@ -5,6 +5,7 @@ import {
View,
RefreshControl,
ActivityIndicator,
TouchableOpacity,
} from "react-native";
import { useFocusEffect } from "@react-navigation/native";
import { useSelector, useDispatch } from "react-redux";
@@ -19,14 +20,15 @@ import {
searchLibraryAction,
} from "../../redux/library/library.actions";
import { updateLibraryAction } from "../../redux/updates/updates.actions";
import { useTheme } from "../../Hooks/reduxHooks";
import { useSettings, useTheme } from "../../Hooks/reduxHooks";
import { setNovel } from "../../redux/novel/novel.actions";
import { Portal } from "react-native-paper";
import { Portal, TouchableRipple } from "react-native-paper";
import LibraryFilterSheet from "./components/LibraryFilterSheet";
const LibraryScreen = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { showNumberOfNovels } = useSettings();
const libraryFilterSheetRef = useRef(null);
@@ -86,11 +88,7 @@ const LibraryScreen = ({ navigation }) => {
// }
const listEmptyComponent = () =>
searchText !== "" ? (
<Text style={[styles.emptySearch, { color: theme.colorAccent }]}>
{`"${searchText}" not in library`}
</Text>
) : (
searchText === "" && (
<EmptyView
icon="Σ(ಠ_ಠ)"
description="Your library is empty. Add series to your library from Browse."
@@ -106,12 +104,15 @@ const LibraryScreen = ({ navigation }) => {
]}
>
<Searchbar
placeholder="Search Library"
placeholder={`Search Library ${
showNumberOfNovels ? "(" + novels.length + ")" : ""
}`}
searchText={searchText}
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
left="magnify"
right="dots-vertical"
right="filter-variant"
filter={filter}
onPressRight={() => libraryFilterSheetRef.current.show()}
theme={theme}
/>
@@ -119,6 +120,22 @@ const LibraryScreen = ({ navigation }) => {
<ActivityIndicator size="small" color={theme.colorAccent} />
) : (
<>
{searchText !== "" && (
<TouchableRipple
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: searchText,
})
}
borderless
rippleColor={theme.colorAccent}
style={styles.emptySearch}
>
<Text style={{ color: theme.colorAccent }}>
{`Search for "${searchText}" globally`}
</Text>
</TouchableRipple>
)}
<NovelList
data={novels}
renderItem={renderItem}
@@ -148,8 +165,8 @@ const styles = StyleSheet.create({
flex: 1,
},
emptySearch: {
marginTop: 8,
textAlign: "center",
paddingHorizontal: 16,
alignItems: "center",
paddingVertical: 8,
borderRadius: 4,
},
});
@@ -1,12 +1,20 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Animated } from "react-native";
import {
StyleSheet,
View,
Text,
Animated,
useWindowDimensions,
} from "react-native";
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
import { List, RadioButton, Checkbox } from "react-native-paper";
import { Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
import { ListItem } from "../../../Components/List";
import { filterLibrary } from "../../../redux/library/library.actions";
import { useSettings } from "../../../Hooks/reduxHooks";
import { setAppSettings } from "../../../redux/settings/settings.actions";
const LibraryFilterSheet = ({
bottomSheetRef,
@@ -17,227 +25,327 @@ const LibraryFilterSheet = ({
}) => {
const [animatedValue] = useState(new Animated.Value(0));
const FirstRoute = () => (
<View style={{ flex: 1 }}>
<Checkbox.Item
label="Downloaded"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersDownloaded > 0"
? "checked"
: "unchecked"
}
onPress={() =>
filter === "chaptersDownloaded > 0"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(sort, "chaptersDownloaded > 0")
)
}
/>
<Checkbox.Item
label="Unread"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={filter === "unread = 1" ? "checked" : "unchecked"}
onPress={() =>
filter === "unread = 1"
? dispatch(filterLibrary(sort, ""))
: dispatch(filterLibrary(sort, "unread = 1"))
}
/>
<Checkbox.Item
label="Completed"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersUnread IS NULL"
? "checked"
: "unchecked"
}
onPress={() =>
filter === "chaptersUnread IS NULL"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(sort, "chaptersUnread IS NULL")
)
}
/>
</View>
);
const SecondRoute = () => (
<View style={{ flex: 1 }}>
<ListItem
style={{ paddingVertical: 10 }}
title="Date Added"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.novelId ASC"
? "arrow-up"
: sort === "novels.novelId DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.novelId ASC"
? dispatch(filterLibrary("novels.novelId DESC", filter))
: dispatch(filterLibrary("novels.novelId ASC", filter))
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Alphabetically"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.novelName ASC"
? "arrow-up"
: sort === "novels.novelName DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.novelName ASC"
? dispatch(
filterLibrary("novels.novelName DESC", filter)
)
: dispatch(
filterLibrary("novels.novelName ASC", filter)
)
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Unread"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.unread ASC"
? "arrow-up"
: sort === "novels.unread DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.unread ASC"
? dispatch(filterLibrary("novels.unread DESC", filter))
: dispatch(filterLibrary("novels.unread ASC", filter))
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Downloaded"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "chaptersDownloaded ASC"
? "arrow-up"
: sort === "chaptersDownloaded DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "chaptersDownloaded ASC"
? dispatch(
filterLibrary("chaptersDownloaded DESC", filter)
)
: dispatch(
filterLibrary("chaptersDownloaded ASC", filter)
)
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Total Chapters"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "chaptersUnread ASC"
? "arrow-up"
: sort === "chaptersUnread DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "chaptersUnread ASC"
? dispatch(filterLibrary("chaptersUnread DESC", filter))
: dispatch(filterLibrary("chaptersUnread ASC", filter))
}
/>
</View>
);
const displayModes = [
{ displayMode: 0, label: "Compact Grid" },
{ displayMode: 1, label: "Comfortable Grid" },
{ displayMode: 2, label: "List" },
];
const {
displayMode,
showDownloadBadges,
showUnreadBadges,
showNumberOfNovels,
} = useSettings();
const renderCheckboxes = () => {
return displayModes.map((mode) => (
<Checkbox.Item
key={mode.displayMode}
label={mode.label}
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={
displayMode === mode.displayMode ? "checked" : "unchecked"
}
// mode="ios"
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(setAppSettings("displayMode", mode.displayMode))
}
/>
));
};
const ThirdRoute = () => (
<View style={{ flex: 1 }}>
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 16,
textTransform: "uppercase",
}}
>
Display Mode
</Text>
{renderCheckboxes()}
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 16,
textTransform: "uppercase",
}}
>
Badges
</Text>
<Checkbox.Item
label="Download Badges"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showDownloadBadges ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(
setAppSettings(
"showDownloadBadges",
!showDownloadBadges
)
)
}
/>
<Checkbox.Item
label="Unread Badges"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showUnreadBadges ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(
setAppSettings("showUnreadBadges", !showUnreadBadges)
)
}
/>
<Checkbox.Item
label="Show number of items"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showNumberOfNovels ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(
setAppSettings(
"showNumberOfNovels",
!showNumberOfNovels
)
)
}
/>
</View>
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
third: ThirdRoute,
});
const layout = useWindowDimensions();
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Filter" },
{ key: "second", title: "Sort" },
{ key: "third", title: "Display" },
]);
const renderTabBar = (props) => (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: theme.colorAccent }}
style={{ backgroundColor: theme.colorPrimary }}
renderLabel={({ route, focused, color }) => (
<Text style={{ color, margin: 8 }}>{route.title}</Text>
)}
inactiveColor={theme.textColorSecondary}
activeColor={theme.colorAccent}
pressColor={theme.rippleColor}
/>
);
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 540, bottom: 0 }}
snappingPoints={[0, 540]}
draggableRange={{ top: 470, bottom: 0 }}
snappingPoints={[0, 470]}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimary },
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<BottomSheetHandle theme={theme} />
<View style={{ marginTop: 24 }}>
<Text
style={{
color: theme.colorAccent,
paddingHorizontal: 16,
paddingBottom: 8,
textTransform: "uppercase",
}}
>
SORT
</Text>
<ListItem
title="Date Added"
theme={theme}
right={
sort === "novels.novelId ASC"
? "arrow-up"
: sort === "novels.novelId DESC"
? "arrow-down"
: null
}
onPress={() =>
sort === "novels.novelId ASC"
? dispatch(
filterLibrary(
"novels.novelId DESC",
filter
)
)
: dispatch(
filterLibrary(
"novels.novelId ASC",
filter
)
)
}
/>
<ListItem
title="Alphabetically"
theme={theme}
right={
sort === "novels.novelName ASC"
? "arrow-up"
: sort === "novels.novelName DESC"
? "arrow-down"
: null
}
onPress={() =>
sort === "novels.novelName ASC"
? dispatch(
filterLibrary(
"novels.novelName DESC",
filter
)
)
: dispatch(
filterLibrary(
"novels.novelName ASC",
filter
)
)
}
/>
<ListItem
title="Unread"
theme={theme}
right={
sort === "novels.unread ASC"
? "arrow-up"
: sort === "novels.unread DESC"
? "arrow-down"
: null
}
onPress={() =>
sort === "novels.unread ASC"
? dispatch(
filterLibrary(
"novels.unread DESC",
filter
)
)
: dispatch(
filterLibrary("novels.unread ASC", filter)
)
}
/>
<ListItem
title="Downloaded"
theme={theme}
right={
sort === "chaptersDownloaded ASC"
? "arrow-up"
: sort === "chaptersDownloaded DESC"
? "arrow-down"
: null
}
onPress={() =>
sort === "chaptersDownloaded ASC"
? dispatch(
filterLibrary(
"chaptersDownloaded DESC",
filter
)
)
: dispatch(
filterLibrary(
"chaptersDownloaded ASC",
filter
)
)
}
/>
<ListItem
title="Total Chapters"
theme={theme}
right={
sort === "chaptersUnread ASC"
? "arrow-up"
: sort === "chaptersUnread DESC"
? "arrow-down"
: null
}
onPress={() =>
sort === "chaptersUnread ASC"
? dispatch(
filterLibrary(
"chaptersUnread DESC",
filter
)
)
: dispatch(
filterLibrary(
"chaptersUnread ASC",
filter
)
)
}
/>
</View>
<View>
<Text
style={{
color: theme.colorAccent,
paddingHorizontal: 16,
paddingVertical: 8,
textTransform: "uppercase",
}}
>
Filter
</Text>
<Checkbox.Item
label="Downloaded"
labelStyle={{ color: theme.textColorPrimary }}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersDownloaded > 0"
? "checked"
: "unchecked"
}
onPress={() =>
filter === "chaptersDownloaded > 0"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(
sort,
"chaptersDownloaded > 0"
)
)
}
/>
<Checkbox.Item
label="Unread"
labelStyle={{ color: theme.textColorPrimary }}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "unread = 1" ? "checked" : "unchecked"
}
onPress={() =>
filter === "unread = 1"
? dispatch(filterLibrary(sort, ""))
: dispatch(filterLibrary(sort, "unread = 1"))
}
/>
<Checkbox.Item
label="Completed"
labelStyle={{ color: theme.textColorPrimary }}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersUnread IS NULL"
? "checked"
: "unchecked"
}
onPress={() =>
filter === "chaptersUnread IS NULL"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(
sort,
"chaptersUnread IS NULL"
)
)
}
/>
</View>
<TabView
navigationState={{ index, routes }}
renderTabBar={renderTabBar}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
style={{ borderTopRightRadius: 8, borderTopLeftRadius: 8 }}
/>
</View>
</Bottomsheet>
);
+5 -5
View File
@@ -17,12 +17,12 @@ const AboutScreen = ({ navigation }) => {
<ListSection>
<ListItem
title="Version"
description="Stable 1.0.17"
description="Stable 1.0.19"
theme={theme}
/>
<ListItem
title="Build Time"
description="06-05-21 21:00 PM"
description="31-05-21 21:30 PM"
theme={theme}
/>
<ListItem
@@ -54,11 +54,11 @@ const AboutScreen = ({ navigation }) => {
theme={theme}
/>
<ListItem
title="Extensions"
description="https://github.com/rajarsheechatterjee/lnreader-extensions"
title="Sources"
description="https://github.com/rajarsheechatterjee/lnreader-sources"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader-extensions"
"https://github.com/rajarsheechatterjee/lnreader-sources"
)
}
theme={theme}
+129
View File
@@ -0,0 +1,129 @@
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import { ScreenContainer } from "../../Components/Common";
import { ListItem, ListSection, ListSubHeader } from "../../Components/List";
import { useTheme } from "../../Hooks/reduxHooks";
import { clearAllHistoryAction } from "../../redux/history/history.actions";
import { deleteNovelCache } from "../../Database/queries/NovelQueries";
import { Button, Dialog, Portal } from "react-native-paper";
const AdvancedSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const [visible, setVisible] = useState(false);
const showDialog = () => setVisible(true);
const hideDialog = () => setVisible(false);
/**
* Confirm Clear Database Dialog
*/
const [clearDatabaseDialog, setClearDatabaseDialog] = useState(false);
const showClearDatabaseDialog = () => setClearDatabaseDialog(true);
const hideClearDatabaseDialog = () => setClearDatabaseDialog(false);
return (
<ScreenContainer theme={theme}>
<Appbar title="Advanced" onBackAction={() => navigation.goBack()} />
<ListSection>
<ListSubHeader theme={theme}>Data</ListSubHeader>
<ListItem
title="Clear database"
description="Delete history for novels not in your library"
onPress={showClearDatabaseDialog}
theme={theme}
/>
<ListItem
title="Clear history"
description="Delete reading history for all novels"
onPress={showDialog}
theme={theme}
/>
</ListSection>
<Portal>
<Dialog
visible={visible}
onDismiss={hideDialog}
style={{
borderRadius: 6,
backgroundColor: theme.colorPrimary,
}}
>
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
color: theme.textColorPrimary,
}}
>
Are you sure? All history will be lost.
</Dialog.Title>
<Dialog.Actions>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={hideDialog}
>
Cancel
</Button>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => {
dispatch(clearAllHistoryAction());
hideDialog();
}}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
<Dialog
visible={clearDatabaseDialog}
onDismiss={hideClearDatabaseDialog}
style={{
borderRadius: 6,
backgroundColor: theme.colorPrimary,
}}
>
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
lineHeight: 16 * 1.5,
color: theme.textColorPrimary,
}}
>
Are you sure? Read chapters and progress of non-library
novels will be lost.
</Dialog.Title>
<Dialog.Actions>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={hideClearDatabaseDialog}
>
Cancel
</Button>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => {
deleteNovelCache();
hideClearDatabaseDialog();
}}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</ScreenContainer>
);
};
export default AdvancedSettings;
+54
View File
@@ -0,0 +1,54 @@
import React from "react";
import { useDispatch } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import {
InfoItem,
ListItem,
ListSection,
ListSubHeader,
} from "../../Components/List";
import { ScreenContainer } from "../../Components/Common";
import { useTheme } from "../../Hooks/reduxHooks";
import { restoreLibraryAction } from "../../redux/library/library.actions";
import { createBackup } from "../../Services/backup";
const BackupSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
return (
<>
<ScreenContainer theme={theme}>
<Appbar
title="Backup"
onBackAction={() => navigation.goBack()}
/>
<ListSection>
<ListSubHeader theme={theme}>Backup</ListSubHeader>
<ListItem
title="Create backup"
description="Can be used to restore current library"
onPress={createBackup}
theme={theme}
/>
<ListItem
title="Restore backup"
description="Restore library from backup file"
onPress={() => dispatch(restoreLibraryAction())}
theme={theme}
/>
<InfoItem
title="Create backup may not work on devices with Android 9 or lower."
icon="information-outline"
description="Restore library from backup file"
theme={theme}
/>
</ListSection>
</ScreenContainer>
</>
);
};
export default BackupSettings;
+129
View File
@@ -0,0 +1,129 @@
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import {
Divider,
ListItem,
ListSection,
ListSubHeader,
} from "../../Components/List";
import { ScreenContainer } from "../../Components/Common";
import { useTheme } from "../../Hooks/reduxHooks";
import DisplayModeModal from "./components/DisplayModeModal";
import GridSizeModal from "./components/GridSizeModal";
import ThemeModal from "./components/ThemeModal";
import AccentColorModal from "./components/AccentColorModal";
const GenralSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { displayMode, novelsPerRow } = useSelector(
(state) => state.settingsReducer
);
const displayModeLabel = (displayMode) => {
const label = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
return label[displayMode] ?? "Compact Grid";
};
/**
* Display Mode Modal
*/
const [displayModalVisible, setDisplayModalVisible] = useState(false);
const showDisplayModal = () => setDisplayModalVisible(true);
const hideDisplayModal = () => setDisplayModalVisible(false);
/**
* Theme Modal
*/
const [themeModalVisible, setthemeModalVisible] = useState(false);
const showthemeModal = () => setthemeModalVisible(true);
const hidethemeModal = () => setthemeModalVisible(false);
/**
* Grid Size Modal
*/
const [gridSizeModalVisible, setGridSizeModalVisible] = useState(false);
const showGridSizeModal = () => setGridSizeModalVisible(true);
const hideGridSizeModal = () => setGridSizeModalVisible(false);
/**
* Accent Color Modal
*/
const [accentColorModal, setAccentColorModal] = useState(false);
const showAccentColorModal = () => setAccentColorModal(true);
const hideAccentColorModal = () => setAccentColorModal(false);
return (
<ScreenContainer theme={theme}>
<Appbar title="General" onBackAction={() => navigation.goBack()} />
<ListSection>
<ListSubHeader theme={theme}>Display</ListSubHeader>
<ListItem
title="Display Mode"
description={displayModeLabel(displayMode)}
onPress={showDisplayModal}
theme={theme}
/>
<ListItem
title="Items per row in library"
description={`${novelsPerRow} items per row`}
onPress={showGridSizeModal}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Theme</ListSubHeader>
<ListItem
title="Theme"
description={theme.name}
onPress={showthemeModal}
theme={theme}
/>
<ListItem
title="Accent Color"
description={theme.colorAccent}
onPress={showAccentColorModal}
theme={theme}
iconColor={theme.colorAccent}
right="circle"
/>
</ListSection>
<DisplayModeModal
displayMode={displayMode}
displayModalVisible={displayModalVisible}
hideDisplayModal={hideDisplayModal}
dispatch={dispatch}
theme={theme}
/>
<GridSizeModal
dispatch={dispatch}
novelsPerRow={novelsPerRow}
gridSizeModalVisible={gridSizeModalVisible}
hideGridSizeModal={hideGridSizeModal}
theme={theme}
/>
<ThemeModal
dispatch={dispatch}
themeModalVisible={themeModalVisible}
hidethemeModal={hidethemeModal}
theme={theme}
/>
<AccentColorModal
dispatch={dispatch}
accentColorModal={accentColorModal}
hideAccentColorModal={hideAccentColorModal}
theme={theme}
/>
</ScreenContainer>
);
};
export default GenralSettings;
+9 -17
View File
@@ -1,12 +1,13 @@
import React from "react";
import { StyleSheet, View, Image } from "react-native";
import { useSelector } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import { ListItem, ListSection } from "../../Components/List";
import { useTheme } from "../../Hooks/reduxHooks";
const MoreScreen = ({ navigation }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
const theme = useTheme();
return (
<>
@@ -15,8 +16,8 @@ const MoreScreen = ({ navigation }) => {
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
<View
style={{
paddingTop: 20,
paddingBottom: 30,
paddingTop: 10,
paddingBottom: 25,
alignItems: "center",
backgroundColor: theme.colorPrimary,
elevation: 4,
@@ -25,8 +26,8 @@ const MoreScreen = ({ navigation }) => {
<Image
source={require("../../../assets/logo.png")}
style={{
height: 60,
width: 60,
height: 80,
width: 80,
tintColor: theme.textColorPrimary,
}}
/>
@@ -38,21 +39,12 @@ const MoreScreen = ({ navigation }) => {
icon="cog-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "Settings",
})
}
theme={theme}
/>
<ListItem
title="Tracking"
icon="sync"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "Trackers",
screen: "SettingsStack",
})
}
theme={theme}
/>
<ListItem
title="About"
icon="information-outline"
+20
View File
@@ -0,0 +1,20 @@
import React from "react";
import { useDispatch } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import { ScreenContainer } from "../../Components/Common";
import { useTheme } from "../../Hooks/reduxHooks";
const ReaderSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
return (
<ScreenContainer theme={theme}>
<Appbar title="Reader" onBackAction={() => navigation.goBack()} />
</ScreenContainer>
);
};
export default ReaderSettings;
+48 -123
View File
@@ -2,9 +2,9 @@ import React, { useState } from "react";
import { View } from "react-native";
import { connect, useDispatch } from "react-redux";
import { switchTheme } from "../../redux/settings/settings.actions";
import { setAppTheme } from "../../redux/settings/settings.actions";
import {
setDisplayMode,
setAppSettings,
setItemsPerRow,
} from "../../redux/settings/settings.actions";
import { deleteNovelCache } from "../../Database/queries/NovelQueries";
@@ -15,53 +15,15 @@ import {
ListItem,
ListSection,
ListSubHeader,
InfoItem,
} from "../../Components/List";
import DisplayModeModal from "./components/DisplayModeModal";
import GridSizeModal from "./components/GridSizeModal";
import ThemeModal from "./components/ThemeModal";
import { createBackup, restoreBackup } from "../../Services/backup";
import { useTheme } from "../../Hooks/reduxHooks";
import { restoreLibraryAction } from "../../redux/library/library.actions";
import { clearAllHistoryAction } from "../../redux/history/history.actions";
const SettingsScreen = ({
navigation,
displayMode,
itemsPerRow,
switchTheme,
setDisplayMode,
setItemsPerRow,
}) => {
const displayModeLabel = (displayMode) => {
const label = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
return label[displayMode];
};
const SettingsScreen = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
// Display Mode Modal
const [displayModalVisible, setDisplayModalVisible] = useState(false);
const showDisplayModal = () => setDisplayModalVisible(true);
const hideDisplayModal = () => setDisplayModalVisible(false);
// Theme Modal
const [themeModalVisible, setthemeModalVisible] = useState(false);
const showthemeModal = () => setthemeModalVisible(true);
const hidethemeModal = () => setthemeModalVisible(false);
// Grid Size Modal
const [gridSizeModalVisible, setGridSizeModalVisible] = useState(false);
const showGridSizeModal = () => setGridSizeModalVisible(true);
const hideGridSizeModal = () => setGridSizeModalVisible(false);
return (
<>
<Appbar title="Settings" onBackAction={() => navigation.goBack()} />
@@ -69,84 +31,56 @@ const SettingsScreen = ({
style={{
flex: 1,
backgroundColor: theme.colorPrimaryDark,
paddingVertical: 8,
}}
>
<ListSection>
<ListSubHeader theme={theme}>Data</ListSubHeader>
<ListItem
title="Clear database"
description="Delete history for novels not in your library"
onPress={() => deleteNovelCache()}
theme={theme}
/>
<ListItem
title="Clear history"
description="Delete reading history for all novels"
onPress={() => dispatch(clearAllHistoryAction())}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Display</ListSubHeader>
<ListItem
title="Display Mode"
description={displayModeLabel(displayMode)}
onPress={showDisplayModal}
theme={theme}
/>
<ListItem
title="Items per row in library"
description={`${itemsPerRow} items per row`}
onPress={showGridSizeModal}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Theme</ListSubHeader>
<ListItem
title="Theme"
description={theme.name}
onPress={showthemeModal}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Backup</ListSubHeader>
<ListItem
title="Create backup"
description="Can be used to restore current library"
onPress={createBackup}
theme={theme}
/>
<ListItem
title="Restore backup"
description="Restore library from backup file"
onPress={() => dispatch(restoreLibraryAction())}
theme={theme}
/>
<InfoItem
title="Create backup may not work on devices with Android 9 or lower."
icon="information-outline"
description="Restore library from backup file"
theme={theme}
/>
</ListSection>
<DisplayModeModal
displayMode={displayMode}
displayModalVisible={displayModalVisible}
hideDisplayModal={hideDisplayModal}
setDisplayMode={setDisplayMode}
<ListItem
title="General"
icon="tune"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "GeneralSettings",
})
}
theme={theme}
/>
<GridSizeModal
itemsPerRow={itemsPerRow}
gridSizeModalVisible={gridSizeModalVisible}
hideGridSizeModal={hideGridSizeModal}
setItemsPerRow={setItemsPerRow}
{/* <ListItem
title="Reader"
icon="book-open-outline"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "ReaderSettings",
})
}
theme={theme}
/> */}
<ListItem
title="Tracking"
icon="sync"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "TrackerSettings",
})
}
theme={theme}
/>
<ThemeModal
themeModalVisible={themeModalVisible}
hidethemeModal={hidethemeModal}
switchTheme={switchTheme}
<ListItem
title="Backup"
icon="cloud-upload-outline"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "BackupSettings",
})
}
theme={theme}
/>
<ListItem
title="Advanced"
icon="code-tags"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "AdvancedSettings",
})
}
theme={theme}
/>
</View>
@@ -154,13 +88,4 @@ const SettingsScreen = ({
);
};
const mapStateToProps = (state) => ({
displayMode: state.settingsReducer.displayMode,
itemsPerRow: state.settingsReducer.itemsPerRow,
});
export default connect(mapStateToProps, {
switchTheme,
setDisplayMode,
setItemsPerRow,
})(SettingsScreen);
export default SettingsScreen;
@@ -0,0 +1,80 @@
import React, { useState } from "react";
import { StyleSheet, Text } from "react-native";
import { Modal, Portal, TextInput } from "react-native-paper";
import { setAccentColor } from "../../../redux/settings/settings.actions";
const AccentColorModal = ({
dispatch,
accentColorModal,
hideAccentColorModal,
theme,
}) => {
const [text, setText] = useState(theme.colorAccent);
const [error, setError] = useState();
return (
<Portal>
<Modal
visible={accentColorModal}
onDismiss={() => {
hideAccentColorModal();
setText();
setError();
}}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={[
styles.dialogTitle,
{ color: theme.textColorPrimary },
]}
>
Hex Color Code
</Text>
<TextInput
value={text}
defaultValue={theme.colorAccent}
onChangeText={(text) => setText(text)}
onSubmitEditing={() => {
if (text.match(/^#[0-9a-f]{3,6}$/i)) {
dispatch(setAccentColor(text));
hideAccentColorModal();
} else {
setError("Enter a valid hex color code");
}
}}
mode="outlined"
theme={{
colors: {
primary: theme.colorAccent,
placeholder: theme.textColorHint,
text: theme.textColorPrimary,
background: "transparent",
},
}}
underlineColor={theme.textColorHint}
dense
error={error}
/>
<Text style={{ color: "#FF0033", paddingTop: 8 }}>{error}</Text>
</Modal>
</Portal>
);
};
export default AccentColorModal;
const styles = StyleSheet.create({
containerStyle: {
margin: 30,
padding: 20,
borderRadius: 8,
},
dialogTitle: {
fontSize: 18,
marginBottom: 16,
},
});
@@ -1,13 +1,14 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Portal, Modal, Checkbox } from "react-native-paper";
import { setAppSettings } from "../../../redux/settings/settings.actions";
const DisplayModeModal = ({
displayMode,
displayModalVisible,
setDisplayMode,
hideDisplayModal,
theme,
dispatch,
displayMode,
hideDisplayModal,
displayModalVisible,
}) => {
const displayModes = [
{ displayMode: 0, label: "Compact Grid" },
@@ -27,7 +28,9 @@ const DisplayModeModal = ({
mode="ios"
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() => setDisplayMode(mode.displayMode)}
onPress={() =>
dispatch(setAppSettings("displayMode", mode.displayMode))
}
/>
));
};
+16 -15
View File
@@ -3,20 +3,21 @@ import { Text, StyleSheet } from "react-native";
import { Portal, Modal } from "react-native-paper";
import Slider from "@react-native-community/slider";
import { RadioButton, RadioButtonGroup } from "../../../Components/RadioButton";
import { setNovelsPerRow } from "../../../redux/settings/settings.actions";
const GridSizeModal = ({
itemsPerRow,
dispatch,
novelsPerRow,
gridSizeModalVisible,
hideGridSizeModal,
setItemsPerRow,
theme,
}) => {
const gridSizes = {
1: "XS",
2: "S",
5: "XS",
4: "S",
3: "M",
4: "L",
5: "XL",
2: "L",
1: "XL",
};
return (
@@ -43,22 +44,22 @@ const GridSizeModal = ({
{ color: theme.textColorSecondary },
]}
>
{`${itemsPerRow} per row`}
{`${novelsPerRow} per row`}
</Text>
<Slider
{/* <Slider
style={styles.slider}
value={itemsPerRow}
value={novelsPerRow}
minimumValue={1}
maximumValue={5}
step={1}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccent}
onValueChange={(value) => setItemsPerRow(value)}
/>
{/* <RadioButtonGroup
onValueChange={(value) => setItemsPerRow(value)}
value={itemsPerRow}
onValueChange={(value) => dispatch(setNovelsPerRow(value))}
/> */}
<RadioButtonGroup
onValueChange={(value) => dispatch(setNovelsPerRow(value))}
value={novelsPerRow}
>
{Object.keys(gridSizes).map((item) => (
<RadioButton
@@ -68,7 +69,7 @@ const GridSizeModal = ({
theme={theme}
/>
))}
</RadioButtonGroup> */}
</RadioButtonGroup>
</Modal>
</Portal>
);
+3 -7
View File
@@ -3,13 +3,9 @@ import { StyleSheet } from "react-native";
import { Portal, Modal, Checkbox } from "react-native-paper";
import { setStatusBarStyle } from "expo-status-bar";
import { setAppTheme } from "../../../redux/settings/settings.actions";
const ThemeModal = ({
themeModalVisible,
hidethemeModal,
switchTheme,
theme,
}) => {
const ThemeModal = ({ themeModalVisible, hidethemeModal, dispatch, theme }) => {
const themes = [
{ id: 0, name: "AMOLED Dark", statusBar: "light" },
{ id: 1, name: "Light", statusBar: "dark" },
@@ -29,7 +25,7 @@ const ThemeModal = ({
mode="ios"
color={theme.colorAccent}
onPress={() => {
switchTheme(item.id);
dispatch(setAppTheme(item.id));
setStatusBarStyle(item.statusBar);
}}
/>
+227 -9
View File
@@ -1,14 +1,26 @@
import React, { useEffect, useRef, useState } from "react";
import { StyleSheet, View, FlatList, RefreshControl } from "react-native";
import {
StyleSheet,
View,
FlatList,
RefreshControl,
StatusBar,
Dimensions,
Share,
} from "react-native";
import { Provider, Portal } from "react-native-paper";
import { Provider, Portal, Appbar, IconButton, Menu } from "react-native-paper";
import { useDispatch } from "react-redux";
import {
bookmarkChapterAction,
deleteAllChaptersAction,
downloadAllChaptersAction,
getNovelAction,
markChapterReadAction,
markChaptersRead,
markChapterUnreadAction,
markPreviousChaptersReadAction,
sortAndFilterChapters,
updateNovelAction,
} from "../../redux/novel/novel.actions";
@@ -25,6 +37,7 @@ import NovelInfoHeader from "./components/NovelHeader";
import ChaptersSettingsSheet from "./components/ChaptersSettingsSheet";
import TrackSheet from "./components/Tracker/TrackSheet";
import ChapterActionsSheet from "./components/ChapterActionsSheet";
import { Row } from "../../Components/Common";
const Novel = ({ route, navigation }) => {
const item = route.params;
@@ -34,15 +47,18 @@ const Novel = ({ route, navigation }) => {
const dispatch = useDispatch();
const { novel, chapters, loading, updating, downloading } = useNovel();
const [selected, setSelected] = useState();
const [selected, setSelected] = useState([]);
const [downloadMenu, showDownloadMenu] = useState(false);
let chaptersSettingsSheetRef = useRef(null);
let trackerSheetRef = useRef(null);
let chapterActionsSheetRef = useRef(null);
const { sort = "ORDER BY chapterId ASC", filter = "" } = usePreferences(
novel.novelId
);
const {
sort = "ORDER BY chapterId ASC",
filter = "",
showChapterTitles = false,
} = usePreferences(novel.novelId);
let { lastReadChapter, position } = useContinueReading(
chapters,
@@ -69,10 +85,11 @@ const Novel = ({ route, navigation }) => {
/>
);
const renderItem = ({ item }) => (
const renderItem = ({ item, index }) => (
<ChapterItem
theme={theme}
chapter={item}
index={index}
novelUrl={novelUrl}
dispatch={dispatch}
sourceId={sourceId}
@@ -81,7 +98,10 @@ const Novel = ({ route, navigation }) => {
position={position}
downloading={downloading}
selected={selected}
novelName={novelName}
selected={selected}
setSelected={setSelected}
showChapterTitles={showChapterTitles}
chapterActionsSheetRef={chapterActionsSheetRef}
/>
);
@@ -94,6 +114,203 @@ const Novel = ({ route, navigation }) => {
{ backgroundColor: theme.colorPrimaryDark },
]}
>
{selected.length > 0 ? (
<View
style={{
backgroundColor: theme.colorPrimary,
position: "absolute",
zIndex: 1,
width: Dimensions.get("window").width,
flexDirection: "row",
alignItems: "center",
paddingTop: StatusBar.currentHeight,
paddingBottom: 8,
}}
>
<Appbar.Action
icon="close"
color={theme.textColorPrimary}
onPress={() => {
setSelected([]);
}}
/>
<Appbar.Content
title={selected.length}
titleStyle={{ color: theme.textColorPrimary }}
/>
<Appbar.Action
icon="bookmark-outline"
color={theme.textColorPrimary}
onPress={() => {
dispatch(bookmarkChapterAction(selected));
setSelected([]);
}}
/>
{selected.some((obj) => obj.read === 0) && (
<Appbar.Action
icon="check"
color={theme.textColorPrimary}
onPress={() => {
dispatch(
markChaptersRead(
selected,
novel.novelId
)
);
setSelected([]);
}}
/>
)}
{selected.some((obj) => obj.read === 1) && (
<Appbar.Action
icon="check-outline"
color={theme.textColorPrimary}
onPress={() => {
dispatch(
markChapterUnreadAction(
selected,
novel.novelId
)
);
setSelected([]);
}}
/>
)}
{selected.length === 1 && (
<Appbar.Action
icon="playlist-check"
color={theme.textColorPrimary}
onPress={() => {
dispatch(
markPreviousChaptersReadAction(
selected[0].chapterId,
selected[0].novelId
)
);
setSelected([]);
}}
/>
)}
{selected.some((obj) => obj.downloaded === 1) && (
<Appbar.Action
icon="trash-can-outline"
color={theme.textColorPrimary}
onPress={() => {
dispatch(deleteAllChaptersAction(selected));
setSelected([]);
}}
/>
)}
{selected.some((obj) => obj.downloaded === 0) && (
<Appbar.Action
icon="download-outline"
color={theme.textColorPrimary}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
selected
)
);
setSelected([]);
}}
/>
)}
<Appbar.Action
icon="select-all"
color={theme.textColorPrimary}
onPress={() => {
setSelected(chapters);
}}
/>
</View>
) : (
<View
style={{
position: "absolute",
zIndex: 1,
height: StatusBar.currentHeight + 54,
width: "100%",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<IconButton
icon="arrow-left"
color="white"
size={24}
style={{ marginTop: StatusBar.currentHeight + 8 }}
onPress={() => navigation.goBack()}
/>
<Row>
<Menu
visible={downloadMenu}
onDismiss={() => showDownloadMenu(false)}
anchor={
<IconButton
icon="download"
color="white"
size={24}
style={{
marginTop:
StatusBar.currentHeight + 8,
}}
onPress={() => showDownloadMenu(true)}
/>
}
contentStyle={{
backgroundColor: theme.menuColor,
}}
>
<Menu.Item
title="Download all"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{
color: theme.textColorPrimary,
}}
onPress={() =>
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
)
)
}
/>
<Menu.Item
title="Delete downloads"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{
color: theme.textColorPrimary,
}}
onPress={() =>
dispatch(
deleteAllChaptersAction(chapters)
)
}
/>
</Menu>
<IconButton
icon="share-variant"
color="white"
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
marginRight: 16,
}}
onPress={() =>
Share.share({
message: novel.sourceUrl,
})
}
/>
</Row>
</View>
)}
<FlatList
data={!loading && chapters}
keyExtractor={(item) => item.chapterId.toString()}
@@ -130,6 +347,7 @@ const Novel = ({ route, navigation }) => {
sort={sort}
theme={theme}
filter={filter}
showChapterTitles={showChapterTitles}
/>
<TrackSheet
bottomSheetRef={trackerSheetRef}
@@ -137,7 +355,7 @@ const Novel = ({ route, navigation }) => {
novelName={novel.novelName}
theme={theme}
/>
<ChapterActionsSheet
{/* <ChapterActionsSheet
theme={theme}
selected={selected}
chapters={chapters}
@@ -147,7 +365,7 @@ const Novel = ({ route, navigation }) => {
markChapterReadAction={markChapterReadAction}
markChapterUnreadAction={markChapterUnreadAction}
bookmarkChapterAction={bookmarkChapterAction}
/>
/> */}
</Portal>
)}
</View>
@@ -43,6 +43,7 @@ const ChapterActionsSheet = ({
paddingBottom: 8,
fontSize: 16,
}}
numberOfLines={1}
>
{selected?.chapterName}
</Text>
+56 -14
View File
@@ -1,22 +1,28 @@
import React, { useState } from "react";
import { View, Text, ActivityIndicator, StyleSheet } from "react-native";
import { TouchableRipple, IconButton, Menu } from "react-native-paper";
import {
deleteChapterAction,
downloadChapterAction,
} from "../../../redux/novel/novel.actions";
import { parseChapterNumber } from "../../../Services/utils/helpers";
const ChapterItem = ({
novelUrl,
novelName,
chapter,
sourceId,
dispatch,
theme,
navigation,
downloading,
index,
position,
selected,
setSelected,
showChapterTitles,
chapterActionsSheetRef,
}) => {
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
@@ -31,11 +37,7 @@ const ChapterItem = ({
chapterId: chapter.chapterId,
chapterUrl: chapter.chapterUrl,
chapterName: chapter.chapterName,
position:
(position &&
position[chapter.chapterId] &&
position[chapter.chapterId].position) ||
0,
novelName: novelName,
});
const displayDownloadButton = () => {
@@ -115,16 +117,44 @@ const ChapterItem = ({
}
};
const isSelected = (chapterId) => {
return selected.some((obj) => obj.chapterId === chapterId);
};
return (
<TouchableRipple
style={[
styles.chapterCardContainer,
{ backgroundColor: theme.colorPrimaryDark },
isSelected(chapter.chapterId) && {
backgroundColor: theme.rippleColor,
},
]}
onPress={navigateToChapter}
onPress={() => {
if (selected.length === 0) {
navigateToChapter();
} else {
if (isSelected(chapter.chapterId)) {
setSelected((selected) =>
selected.filter(
(item) => item.chapterId !== chapter.chapterId
)
);
} else {
setSelected((selected) => [...selected, chapter]);
}
}
}}
onLongPress={() => {
setSelected(chapter);
chapterActionsSheetRef.current.show();
if (isSelected(chapter.chapterId)) {
setSelected((selected) =>
selected.filter(
(item) => item.chapterId !== chapter.chapterId
)
);
} else {
setSelected((selected) => [...selected, chapter]);
}
// chapterActionsSheetRef.current.show();
}}
rippleColor={theme.rippleColor}
>
@@ -147,7 +177,14 @@ const ChapterItem = ({
]}
numberOfLines={1}
>
{chapter.chapterName.substring(0, 50)}
{showChapterTitles
? parseChapterNumber(chapter.chapterName)
? "Chapter " +
parseChapterNumber(
chapter.chapterName
)
: "Chapter " + index
: chapter.chapterName.substring(0, 50)}
</Text>
<View
style={{
@@ -175,11 +212,13 @@ const ChapterItem = ({
!chapter.read &&
position[chapter.chapterId] &&
position[chapter.chapterId].percentage <
95 && (
100 &&
position[chapter.chapterId].percentage >
0 && (
<Text
style={[
{
color: theme.textColorSecondary,
color: theme.textColorHint,
fontSize: 12,
marginLeft: 0,
},
@@ -189,8 +228,11 @@ const ChapterItem = ({
]}
numberOfLines={1}
>
{position[chapter.chapterId]
.percentage + "%"}
{chapter.releaseDate && "• "}
{"Progress " +
position[chapter.chapterId]
.percentage +
"%"}
</Text>
)}
</View>
@@ -1,9 +1,19 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Animated } from "react-native";
import {
StyleSheet,
View,
Text,
Animated,
useWindowDimensions,
} from "react-native";
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
import { List, RadioButton, Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
import { showChapterTitlesAction } from "../../../redux/novel/novel.actions";
import { ListItem } from "../../../Components/List";
import { RadioButton, RadioButtonGroup } from "../../../Components/RadioButton";
import { Checkbox } from "react-native-paper";
const ChaptersSettingsSheet = ({
bottomSheetRef,
@@ -13,6 +23,7 @@ const ChaptersSettingsSheet = ({
sort,
filter,
theme,
showChapterTitles,
}) => {
const sortChapters = (val) =>
dispatch(sortAndFilterChapters(novelId, val, filter));
@@ -20,16 +31,157 @@ const ChaptersSettingsSheet = ({
const filterChapters = (val) =>
dispatch(sortAndFilterChapters(novelId, sort, val));
const checkBoxStyle = { flexDirection: "row", alignItems: "center" };
const [animatedValue] = useState(new Animated.Value(0));
const FirstRoute = () => (
<View style={{ flex: 1 }}>
<Checkbox.Item
label="Downloaded"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter.match("AND downloaded=1") ? "checked" : "unchecked"
}
onPress={() =>
filter.match("AND downloaded=1")
? filterChapters(
filter.replace(" AND downloaded=1", "")
)
: filterChapters(filter + " AND downloaded=1")
}
/>
<Checkbox.Item
label="Unread"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter.match("AND `read`=0")
? "checked"
: filter.match("AND `read`=1")
? "indeterminate"
: "unchecked"
}
onPress={() => {
if (filter.match("AND `read`=0")) {
filterChapters(
filter.replace(" AND `read`=0", " AND `read`=1")
);
} else if (filter.match("AND `read`=1")) {
filterChapters(filter.replace(" AND `read`=1", ""));
} else {
filterChapters(filter + " AND `read`=0");
}
}}
/>
<Checkbox.Item
label="Bookmarked"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter.match("AND bookmark=1") ? "checked" : "unchecked"
}
onPress={() => {
filter.match("AND bookmark=1")
? filterChapters(filter.replace(" AND bookmark=1", ""))
: filterChapters(filter + " AND bookmark=1");
}}
/>
</View>
);
const SecondRoute = () => (
<View style={{ flex: 1 }}>
<ListItem
style={{ paddingVertical: 10 }}
title="By source"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "ORDER BY chapterId ASC"
? "arrow-up"
: "arrow-down"
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "ORDER BY chapterId ASC"
? sortChapters("ORDER BY chapterId DESC")
: sortChapters("ORDER BY chapterId ASC")
}
/>
</View>
);
const ThirdRoute = () => (
<View style={{ flex: 1, padding: 8 }}>
<RadioButtonGroup
onValueChange={(value) =>
dispatch(showChapterTitlesAction(novelId, value))
}
value={showChapterTitles}
>
<RadioButton
value={false}
label="Source title"
theme={theme}
labelStyle={{ fontSize: 14 }}
/>
<RadioButton
value={true}
label="Chapter number"
theme={theme}
labelStyle={{ fontSize: 14 }}
/>
</RadioButtonGroup>
</View>
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
third: ThirdRoute,
});
const layout = useWindowDimensions();
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Filter" },
{ key: "second", title: "Sort" },
{ key: "third", title: "Display" },
]);
const renderTabBar = (props) => (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: theme.colorAccent }}
style={{ backgroundColor: theme.colorPrimary }}
renderLabel={({ route, focused, color }) => (
<Text style={{ color, margin: 8 }}>{route.title}</Text>
)}
inactiveColor={theme.textColorSecondary}
activeColor={theme.colorAccent}
pressColor={theme.rippleColor}
/>
);
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 370, bottom: 0 }}
snappingPoints={[0, 370]}
draggableRange={{ top: 220, bottom: 0 }}
snappingPoints={[0, 220]}
>
<View
style={[
@@ -37,131 +189,14 @@ const ChaptersSettingsSheet = ({
{ backgroundColor: theme.colorPrimary },
]}
>
<BottomSheetHandle theme={theme} />
<List.Subheader
style={{
color: theme.textColorPrimary,
fontSize: 20,
}}
>
Sort
</List.Subheader>
<View>
<RadioButton.Group
onValueChange={(newValue) =>
sortChapters(newValue, filter)
}
value={sort}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
<RadioButton.Item
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
value="ORDER BY chapterId DESC"
/>
<Text
style={{
color: theme.textColorPrimary,
}}
>
Newest to oldest
</Text>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
<RadioButton.Item
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
value="ORDER BY chapterId ASC"
/>
<Text
style={{
color: theme.textColorPrimary,
}}
>
Oldest to newest
</Text>
</View>
</View>
</RadioButton.Group>
</View>
<List.Subheader
style={{
color: theme.textColorPrimary,
fontSize: 20,
paddingVertical: 10,
}}
>
Filter
</List.Subheader>
<View style={checkBoxStyle}>
<Checkbox.Item
status={filter === "" ? "checked" : "unchecked"}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show all
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND `read`=1" ? "checked" : "unchecked"
}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("AND `read`=1")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show read chapters
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND `read`=0" ? "checked" : "unchecked"
}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("AND `read`=0")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show unread chapters
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND downloaded=1"
? "checked"
: "unchecked"
}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("AND downloaded=1")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show downloaded chapters
</Text>
</View>
<TabView
navigationState={{ index, routes }}
renderTabBar={renderTabBar}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
style={{ borderTopRightRadius: 8, borderTopLeftRadius: 8 }}
/>
</View>
</Bottomsheet>
);
@@ -1,49 +0,0 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Chip } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { followNovelAction } from "../../../../redux/novel/novel.actions";
const FollowChip = ({ followed, novel, theme, dispatch }) => {
const getIconName = () => (followed ? "heart" : "heart-outline");
const getChipLabel = () => (followed ? "In Library" : "Add to library");
return (
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={getIconName()}
size={21}
color={theme.colorAccent}
/>
)}
onPress={() => dispatch(followNovelAction(novel))}
style={[
{ backgroundColor: theme.colorPrimaryDark },
styles.toggleFavourite,
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimary,
}}
>
{getChipLabel()}
</Chip>
);
};
export default FollowChip;
const styles = StyleSheet.create({
toggleFavourite: {
height: 32,
justifyContent: "center",
alignItems: "center",
marginLeft: 16,
paddingLeft: 4,
borderWidth: 0,
elevation: 0,
},
});
@@ -1,36 +0,0 @@
import React from "react";
import { ImageBackground, StyleSheet } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import easeGradient from "react-native-easing-gradient";
const NovelCoverImage = ({ children, source, theme }) => {
const { colors, locations } = easeGradient({
colorStops: {
0: {
color: "rgba(0,0,0,0.2)",
},
1: {
color: theme.colorPrimaryDark,
},
},
});
return (
<ImageBackground source={source} style={styles.background}>
<LinearGradient
colors={colors}
locations={locations}
style={styles.linearGradient}
>
{children}
</LinearGradient>
</ImageBackground>
);
};
export default NovelCoverImage;
const styles = StyleSheet.create({
background: { height: 285 },
linearGradient: { height: 286 },
});
@@ -0,0 +1,216 @@
import React from "react";
import {
FlatList,
Image,
ImageBackground,
StyleSheet,
Text,
View,
} from "react-native";
import { Chip, IconButton } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import easeGradient from "react-native-easing-gradient";
const NovelInfoContainer = ({ children }) => (
<View style={styles.novelInfoContainer}>{children}</View>
);
const CoverImage = ({ children, source, theme }) => {
const { colors, locations } = easeGradient({
colorStops: {
0: { color: "rgba(0,0,0,0.2)" },
1: { color: theme.colorPrimaryDark },
},
});
return (
<ImageBackground source={source} style={styles.coverImage}>
<LinearGradient
colors={colors}
locations={locations}
style={styles.linearGradient}
>
{children}
</LinearGradient>
</ImageBackground>
);
};
const NovelThumbnail = ({ source }) => (
<Image source={source} style={styles.novelThumbnail} />
);
const NovelTitle = ({ theme, children, onLongPress, onPress }) => (
<Text
onLongPress={onLongPress}
onPress={onPress}
numberOfLines={2}
style={[styles.novelTitle, { color: theme.textColorPrimary }]}
>
{children}
</Text>
);
const NovelAuthor = ({ theme, children }) => (
<Text
style={[styles.novelAuthor, { color: theme.textColorSecondary }]}
numberOfLines={2}
>
{children}
</Text>
);
const NovelInfo = ({ theme, children }) => (
<Text
style={[styles.novelInfo, { color: theme.textColorSecondary }]}
numberOfLines={1}
>
{children}
</Text>
);
const FollowButton = ({ theme, onPress, followed }) => (
<View style={{ alignItems: "center", flex: 1 }}>
<IconButton
icon={followed ? "heart" : "heart-outline"}
color={followed ? theme.colorAccent : theme.textColorSecondary}
size={24}
style={{ margin: 0 }}
onPress={onPress}
/>
<Text
style={{
fontSize: 12,
color: followed ? theme.colorAccent : theme.textColorSecondary,
}}
>
{followed ? "In Library" : "Add to library"}
</Text>
</View>
);
const TrackerButton = ({ theme, isTracked, onPress }) => (
<View style={{ alignItems: "center", flex: 1 }}>
<IconButton
icon={isTracked ? "check" : "sync"}
color={isTracked ? theme.colorAccent : theme.textColorSecondary}
size={24}
style={{ margin: 0 }}
onPress={onPress}
/>
<Text
style={{
fontSize: 12,
color: isTracked ? theme.colorAccent : theme.textColorSecondary,
}}
>
{isTracked ? "Tracked" : "Tracking"}
</Text>
</View>
);
const GenreChip = ({ children, theme }) => (
<Text
style={[
styles.genreChip,
{
color: theme.colorAccent,
borderColor: theme.colorAccent,
backgroundColor: theme.colorPrimary,
},
]}
>
{children}
</Text>
);
const NovelGenres = ({ theme, genre }) => {
const data = genre.split(",");
const renderItem = ({ item }) => (
<GenreChip theme={theme}>{item}</GenreChip>
);
return (
<FlatList
contentContainerStyle={styles.novelGenres}
horizontal
data={data}
keyExtractor={(item) => item}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
/>
);
};
export {
NovelInfoContainer,
CoverImage,
NovelThumbnail,
NovelTitle,
NovelAuthor,
NovelInfo,
FollowButton,
TrackerButton,
NovelGenres,
};
const styles = StyleSheet.create({
novelInfoContainer: {
flex: 1,
flexDirection: "row",
margin: 16,
paddingTop: 90,
},
coverImage: {
height: 285,
},
linearGradient: {
height: 286,
},
novelThumbnail: {
height: 160,
width: 110,
margin: 3.2,
borderRadius: 6,
},
novelTitle: {
fontWeight: "bold",
fontSize: 18,
},
novelAuthor: {
marginVertical: 3,
fontSize: 14,
fontWeight: "bold",
},
novelInfo: {
marginVertical: 3,
fontSize: 14,
},
followButton: {
height: 32,
justifyContent: "center",
alignItems: "center",
marginLeft: 16,
paddingLeft: 4,
borderWidth: 0,
elevation: 0,
},
novelGenres: {
// marginTop: 12,
paddingHorizontal: 16,
paddingBottom: 16,
},
genreChip: {
flex: 1,
justifyContent: "center",
paddingVertical: 2,
paddingHorizontal: 10,
marginHorizontal: 2,
fontSize: 13,
borderWidth: 1,
borderRadius: 50,
},
});
@@ -5,16 +5,8 @@ const NovelSummary = ({ summary, followed, theme }) => {
const [more, showMore] = useState(!followed);
return (
summary !== "" && (
summary && (
<View style={styles.summaryContainer}>
<Text
style={[
{ color: theme.textColorPrimary },
styles.summaryHeader,
]}
>
About
</Text>
<Text
style={{
color: theme.textColorSecondary,
@@ -48,14 +40,14 @@ export default NovelSummary;
const styles = StyleSheet.create({
summaryContainer: {
paddingHorizontal: 16,
marginBottom: 8,
},
summaryHeader: {
marginTop: 5,
paddingVertical: 5,
fontSize: 15,
fontWeight: "bold",
marginVertical: 16,
},
// summaryHeader: {
// marginTop: 5,
// paddingVertical: 5,
// fontSize: 15,
// fontWeight: "bold",
// },
showMoreButton: {
fontWeight: "bold",
position: "absolute",
@@ -1,7 +1,7 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Button } from "react-native-paper";
import { useContinueReading } from "../../../../Hooks/reduxHooks";
const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
return (
@@ -23,6 +23,7 @@ const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
novelId: lastRead.novelId,
sourceId: novel.sourceId,
chapterName: lastRead.chapterName,
novelName: novel.novelName,
});
}}
>
@@ -1,47 +0,0 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Chip } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
const TrackerChip = ({ isTracked, trackerSheetRef, theme }) => {
const getIconName = () => (isTracked ? "check" : "sync");
const getChipLabel = () => (isTracked ? "Tracked" : "Tracking");
return (
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={getIconName(isTracked)}
size={21}
color={theme.colorAccent}
/>
)}
onPress={() => trackerSheetRef.current.show()}
style={[
styles.toggleFavourite,
{ backgroundColor: theme.colorPrimaryDark },
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimary,
}}
>
{getChipLabel()}
</Chip>
);
};
export default TrackerChip;
const styles = StyleSheet.create({
toggleFavourite: {
height: 32,
justifyContent: "center",
alignItems: "center",
paddingLeft: 4,
marginLeft: 0,
borderWidth: 0,
elevation: 0,
},
});
+117 -191
View File
@@ -1,21 +1,27 @@
import React, { useState } from "react";
import { View, Text, StyleSheet, Image, FlatList, Share } from "react-native";
import { TouchableRipple, IconButton, Button, Menu } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import React from "react";
import { View, Text, StyleSheet, Clipboard } from "react-native";
import * as WebBrowser from "expo-web-browser";
import { useSelector, useDispatch } from "react-redux";
import { TouchableRipple, IconButton } from "react-native-paper";
import { followNovelAction } from "../../../redux/novel/novel.actions";
import { useTrackingStatus } from "../../../Hooks/reduxHooks";
import {
downloadAllChaptersAction,
deleteAllChaptersAction,
} from "../../../redux/novel/novel.actions";
import { GenreChip } from "./Info/GenreChip";
import NovelCoverImage from "./Info/NovelCoverImage";
import FollowChip from "./Info/FollowChip";
import TrackerChip from "./Info/TrackerChip";
CoverImage,
NovelAuthor,
NovelInfo,
NovelInfoContainer,
NovelThumbnail,
NovelTitle,
FollowButton,
TrackerButton,
NovelGenres,
} from "./Info/NovelDetailsComponents";
import { Row } from "../../../Components/Common";
import NovelSummary from "./Info/NovelSummary";
import ReadButton from "./Info/ReadButton";
import { useTrackingStatus } from "../../../Hooks/reduxHooks";
import { showToast } from "../../../Hooks/showToast";
const NovelInfoHeader = ({
item,
@@ -30,8 +36,6 @@ const NovelInfoHeader = ({
trackerSheetRef,
chaptersSettingsSheetRef,
}) => {
const [downloadMenu, showDownloadMenu] = useState(false);
const { tracker, trackedNovels } = useTrackingStatus();
let isTracked = false;
@@ -40,165 +44,113 @@ const NovelInfoHeader = ({
isTracked = trackedNovels.find((obj) => obj.novelId === novel.novelId);
}
const getGenres = ({ item }) => <GenreChip theme={theme}>{item}</GenreChip>;
return (
<View style={{ flexGrow: 1 }}>
<NovelCoverImage source={{ uri: item.novelCover }} theme={theme}>
<View style={styles.detailsContainer}>
<View>
<Image
source={{ uri: item.novelCover }}
style={styles.logo}
/>
</View>
<View style={styles.nameContainer}>
<Text
numberOfLines={2}
style={[
styles.name,
{ color: theme.textColorPrimary },
]}
>
{item.novelName}
</Text>
<>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
fontWeight: "bold",
}}
numberOfLines={2}
>
{(item.author &&
item.author.replace(",", ", ")) ||
(!loading &&
novel.author.replace(",", ", "))}
</Text>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
}}
numberOfLines={1}
>
{item.status || (!loading && novel.status)}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 14,
}}
numberOfLines={1}
>
{item.source || (!loading && novel.source)}
</Text>
</>
</View>
</View>
</NovelCoverImage>
{!loading && (
<>
<View style={styles.buttonsContainer}>
<FollowChip
<View>
<CoverImage source={{ uri: item.novelCover }} theme={theme}>
<NovelInfoContainer>
<NovelThumbnail source={{ uri: item.novelCover }} />
<View style={styles.novelDetails}>
<NovelTitle
theme={theme}
followed={novel.followed}
novel={novel}
dispatch={dispatch}
/>
{tracker && (
<TrackerChip
theme={theme}
isTracked={isTracked}
trackerSheetRef={trackerSheetRef}
/>
)}
<IconButton
onPress={() =>
WebBrowser.openBrowserAsync(novel.sourceUrl)
}
icon="earth"
color={theme.colorAccent}
size={21}
/>
<IconButton
onPress={() =>
Share.share({ message: novel.sourceUrl })
}
icon="share-variant"
color={theme.colorAccent}
size={21}
/>
<Menu
visible={downloadMenu}
onDismiss={() => showDownloadMenu(false)}
anchor={
<IconButton
icon="download"
color={theme.colorAccent}
size={21}
onPress={() => showDownloadMenu(true)}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
>
<Menu.Item
title="Download all"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{ color: theme.textColorPrimary }}
onPress={() =>
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
)
)
}
/>
<Menu.Item
title="Delete downloads"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{ color: theme.textColorPrimary }}
onPress={() =>
dispatch(deleteAllChaptersAction(chapters))
}
/>
</Menu>
{/* <IconButton
onPress={() =>
navigation.navigate("MigrateNovel", {
sourceId: novel.sourceId,
novelName: novel.novelName,
navigation.navigate("GlobalSearch", {
novelName: item.novelName,
})
}
icon="weather-cloudy-arrow-right"
color={theme.colorAccent}
size={21}
/> */}
onLongPress={() => {
Clipboard.setString(item.novelName);
showToast(
"Copied to clipboard: " + item.novelName
);
}}
>
{item.novelName}
</NovelTitle>
{!loading && (
<>
<NovelAuthor theme={theme}>
{novel.author}
</NovelAuthor>
<NovelInfo theme={theme}>
{novel.status}
</NovelInfo>
<NovelInfo theme={theme}>
{novel.source}
</NovelInfo>
</>
)}
</View>
</NovelInfoContainer>
</CoverImage>
{!loading && (
<>
<Row
style={{
justifyContent: "space-around",
paddingHorizontal: 16,
flex: 1,
}}
>
<FollowButton
theme={theme}
followed={novel.followed}
onPress={() => dispatch(followNovelAction(novel))}
/>
{tracker && (
<TrackerButton
theme={theme}
isTracked={isTracked}
onPress={() => trackerSheetRef.current.show()}
/>
)}
<View style={{ alignItems: "center", flex: 1 }}>
<IconButton
icon="swap-vertical-variant"
color={theme.textColorSecondary}
size={24}
style={{ margin: 0 }}
onPress={() =>
navigation.navigate("MigrateNovel", {
sourceId: novel.sourceId,
novelName: novel.novelName,
})
}
/>
<Text
style={{
fontSize: 12,
color: theme.textColorSecondary,
}}
>
Migrate
</Text>
</View>
<View style={{ alignItems: "center", flex: 1 }}>
<IconButton
icon="earth"
color={theme.textColorSecondary}
size={24}
style={{ margin: 0 }}
onPress={() =>
WebBrowser.openBrowserAsync(novel.sourceUrl)
}
/>
<Text
style={{
fontSize: 12,
color: theme.textColorSecondary,
}}
>
WebView
</Text>
</View>
</Row>
<NovelSummary
summary={novel.novelSummary}
followed={novel.followed}
theme={theme}
/>
<FlatList
style={
novel.novelSummary === ""
? { marginTop: 20 }
: { marginTop: 12 }
}
contentContainerStyle={styles.genreContainer}
horizontal
data={novel.genre.split(",")}
keyExtractor={(item) => item}
renderItem={getGenres}
showsHorizontalScrollIndicator={false}
/>
<NovelGenres theme={theme} genre={novel.genre} />
<ReadButton
novel={novel}
chapters={chapters}
@@ -240,32 +192,11 @@ const NovelInfoHeader = ({
export default NovelInfoHeader;
const styles = StyleSheet.create({
nameContainer: {
novelDetails: {
flex: 1,
marginHorizontal: 16,
paddingHorizontal: 16,
paddingTop: 8,
},
detailsContainer: {
flex: 1,
flexDirection: "row",
margin: 16,
paddingTop: 90,
},
logo: {
height: 160,
width: 110,
margin: 3.2,
borderRadius: 6,
},
name: {
fontWeight: "bold",
fontSize: 18,
},
buttonsContainer: {
flexDirection: "row",
alignItems: "center",
},
chapters: {
paddingHorizontal: 16,
paddingVertical: 4,
@@ -278,9 +209,4 @@ const styles = StyleSheet.create({
alignItems: "center",
paddingRight: 12,
},
genreContainer: {
paddingHorizontal: 15,
paddingBottom: 15,
},
});
+43 -5
View File
@@ -5,6 +5,8 @@ import {
FlatList,
ActivityIndicator,
RefreshControl,
Button,
Text,
} from "react-native";
import { useFocusEffect } from "@react-navigation/native";
import { useDispatch, useSelector } from "react-redux";
@@ -18,6 +20,9 @@ import UpdatesItem from "./components/UpdatesItem";
import { useTheme } from "../../Hooks/reduxHooks";
import { Searchbar } from "../../Components/Searchbar";
import moment from "moment";
import { dateFormat } from "../../Services/utils/constants";
const Updates = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
@@ -47,6 +52,7 @@ const Updates = ({ navigation }) => {
novelUrl: item.novelUrl,
chapterName: item.chapterName,
novelId: item.novelId,
novelName: item.novelName,
});
const renderItem = ({ item }) => (
@@ -76,9 +82,23 @@ const Updates = ({ navigation }) => {
const onChangeText = (text) => {
setSearchText(text);
let results = updates.filter((update) =>
update.novelName.toLowerCase().includes(text.toLowerCase())
);
let results = [];
text !== "" &&
updates.map((item) => {
const date = item.date;
const chapters = item.chapters.filter((chapter) =>
chapter.novelName.toLowerCase().includes(text.toLowerCase())
);
if (chapters.length > 0) {
results.push({
date,
chapters,
});
}
});
setSearchResults(results);
};
@@ -102,8 +122,26 @@ const Updates = ({ navigation }) => {
<FlatList
contentContainerStyle={styles.flatList}
data={searchText ? searchResults : updates}
keyExtractor={(item) => item.updateId.toString()}
renderItem={renderItem}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<FlatList
ListHeaderComponent={
<Text
style={{
paddingHorizontal: 16,
textTransform: "uppercase",
paddingVertical: 8,
color: theme.textColorSecondary,
}}
>
{moment(item.date).calendar(null, dateFormat)}
</Text>
}
data={item.chapters}
keyExtractor={(item) => item.updateId.toString()}
renderItem={renderItem}
/>
)}
ListFooterComponent={ListFooterComponent()}
ListEmptyComponent={ListEmptyComponent()}
refreshControl={refreshControl()}
+18 -19
View File
@@ -1,7 +1,6 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple } from "react-native-paper";
import moment from "moment";
const UpdateCard = ({ item, theme, onPress }) => {
return (
@@ -18,27 +17,27 @@ const UpdateCard = ({ item, theme, onPress }) => {
/>
<View style={styles.chapterDetails}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
style={[
{ color: theme.textColorPrimary, fontSize: 14 },
item.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={{ flexDirection: "row" }}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 12,
}}
numberOfLines={1}
>
{`${item.chapterName} · ${moment(
item.updateTime
).calendar()}`}
</Text>
</View>
<Text
style={[
{ color: theme.textColorPrimary, fontSize: 12 },
item.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{item.chapterName}
</Text>
</View>
</>
</TouchableRipple>
@@ -52,7 +51,7 @@ const styles = StyleSheet.create({
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 12,
paddingHorizontal: 16,
},
updateIcon: {
width: 42,
+1 -1
View File
@@ -62,7 +62,7 @@ export const restoreBackup = async () => {
if (index + 1 === novels.length) {
Notifications.scheduleNotificationAsync({
content: {
title: "Library Updated",
title: "Library Restored",
body: novels.length + " novels restored",
},
trigger: null,
+9 -2
View File
@@ -1,4 +1,4 @@
const fonts = [
export const fonts = [
{ fontFamily: "", name: "Original" },
{ fontFamily: "arbutus-slab", name: "Arbutus Slab" },
{ fontFamily: "domine", name: "Domine" },
@@ -8,4 +8,11 @@ const fonts = [
{ fontFamily: "pt-serif", name: "PT Serif" },
];
export { fonts };
export const dateFormat = {
sameDay: "[Today]",
nextDay: "[Tomorrow]",
nextWeek: "dddd",
lastDay: "[Yesterday]",
lastWeek: "[Last] dddd",
sameElse: "DD/MM/YYYY",
};
+1 -1
View File
@@ -2,7 +2,7 @@ export const parseChapterNumber = (chapterName) => {
chapterName = chapterName.toLowerCase();
chapterName = chapterName.replace(/volume (\d+)/, "");
const basic = chapterName.match(/ch (\d+)/);
const basic = chapterName.match(/[ch]i (\d+)/);
const occurrence = chapterName.match(/\d+/);
if (basic) {
+8 -8
View File
@@ -8,7 +8,7 @@ export const theme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.06)",
};
/**
@@ -23,13 +23,13 @@ export const lightTheme = {
textColorPrimary: "#000000",
textColorSecondary: "rgba(0,0,0,0.54)",
textColorHint: "rgba(0,0,0,0.38)",
rippleColor: "rgba(0,0,0,0.12)",
rippleColor: "rgba(0,0,0,0.06)",
searchBarColor: "#FFFFFF",
menuColor: "#FFFFFF",
dividerColor: "rgba(0,0,0,0.1)",
filterColor: "#FFC107",
colorDisabled: "rgba(220,220,220,255)",
colorButtonText: "#000000",
colorButtonText: "#FFFFFF",
statusBar: "dark",
};
@@ -45,7 +45,7 @@ export const darkTheme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#303135",
menuColor: "#242529",
filterColor: "#FFEB3B",
@@ -67,7 +67,7 @@ export const amoledDarkTheme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.12)",
searchBarColor: "#1F1F1F",
menuColor: "#242529",
filterColor: "#FFEB3B",
@@ -89,7 +89,7 @@ export const midnightDuskTheme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#201F27",
menuColor: "#201F27",
filterColor: "#FFEB3B",
@@ -112,7 +112,7 @@ export const limeTheme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#201F27",
menuColor: "#201F27",
filterColor: "#FFEB3B",
@@ -131,7 +131,7 @@ export const irisBlueTheme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#393e46",
menuColor: "#393e46",
filterColor: "#FFEB3B",
-31
View File
@@ -1,31 +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 },
});
};
-21
View File
@@ -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;
-2
View File
@@ -1,2 +0,0 @@
export const GET_CHAPTER = "GET_CHAPTER";
export const CHAPTER_LOADING = "CHAPTER_LOADING";
+39 -16
View File
@@ -1,5 +1,4 @@
import {
UPDATE_NOVEL_HISTORY,
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
@@ -12,41 +11,65 @@ import {
deleteAllHistory,
} from "../../Database/queries/HistoryQueries";
import { SET_LAST_READ } from "../preferences/preference.types";
import { showToast } from "../../Hooks/showToast";
export const getHistoryAction = () => async (dispatch) => {
dispatch({ type: LOAD_HISTORY });
const history = await getHistory();
dispatch({ type: GET_HISTORY, payload: history });
const groups = history.reduce((groups, update) => {
var dateParts = update.historyTimeRead.split("-");
var jsDate = new Date(
dateParts[0],
dateParts[1] - 1,
dateParts[2].substr(0, 2)
);
const date = jsDate.toISOString();
if (!groups[date]) {
groups[date] = [];
}
groups[date].push(update);
return groups;
}, {});
const groupedHistory = Object.keys(groups).map((date) => {
return {
date,
novels: groups[date],
};
});
// console.log(groupedHistory);
dispatch({ type: GET_HISTORY, payload: groupedHistory });
};
export const insertHistoryAction = (novelId, chapterId) => async (dispatch) => {
await insertHistory(novelId, chapterId);
// dispatch({ type: UPDATE_NOVEL_HISTORY });
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId },
});
const history = await getHistory();
dispatch({ type: GET_HISTORY, payload: history });
dispatch(getHistoryAction());
};
export const deleteHistoryAction = (novelId) => async (dispatch) => {
await deleteHistory(novelId);
dispatch({
type: CLEAR_NOVEL_HISTORY,
payload: { novelId },
});
export const deleteHistoryAction = (historyId) => async (dispatch) => {
await deleteHistory(historyId);
console.log("Delete History Action Called");
dispatch(getHistoryAction());
};
export const clearAllHistoryAction = () => async (dispatch) => {
deleteAllHistory();
try {
await deleteAllHistory();
dispatch({ type: CLEAR_HISTORY });
showToast("History deleted");
dispatch({ type: CLEAR_HISTORY });
} catch (error) {
showToast(error.message);
}
};
+8 -14
View File
@@ -3,7 +3,6 @@ import {
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
UPDATE_NOVEL_HISTORY,
} from "./history.types";
const initialState = {
@@ -19,20 +18,15 @@ const historyReducer = (state = initialState, action) => {
return { ...state, loading: true };
case GET_HISTORY:
return { ...state, history: payload, loading: false };
// case UPDATE_NOVEL_HISTORY:
// return state;
case CLEAR_NOVEL_HISTORY:
return {
...state,
history: state.history.filter(
(item) => item.novelId !== payload.novelId
),
};
// case CLEAR_NOVEL_HISTORY:
// return {
// ...state,
// history: state.history.filter(
// (item) => item.novelId !== payload.novelId
// ),
// };
case CLEAR_HISTORY:
return {
...state,
history: [],
};
return { ...state, history: [] };
default:
return state;
}
+1 -1
View File
@@ -23,7 +23,7 @@ const libraryReducer = (state = initialState, action) => {
case GET_LIBRARY_SEARCH_RESULTS:
return { ...state, novels: payload, loading: false };
case SORT_FILTER_LIBRARY:
console.log("2.", payload.sort, payload.filter);
// console.log("2.", payload.sort, payload.filter);
return {
...state,
filters: { sort: payload.sort, filter: payload.filter },
+162 -75
View File
@@ -4,6 +4,7 @@ import {
LOADING_NOVEL,
GET_NOVEL,
GET_CHAPTERS,
NOVEL_ERROR,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
@@ -36,15 +37,18 @@ import {
bookmarkChapter,
markPreviuschaptersRead,
markPreviousChaptersUnread,
getNextChapterFromDB,
} from "../../Database/queries/ChapterQueries";
import { deleteNovelUpdates } from "../../Database/queries/UpdateQueries";
import {
SAVE_SCROLL_POSITION,
SET_CHAPTER_LIST_PREF,
SET_LAST_READ,
} from "../preferences/preference.types";
import { GET_LIBRARY_NOVELS } from "../library/library.types";
import { getLibrary } from "../../Database/queries/LibraryQueries";
import { showToast } from "../../Hooks/showToast";
import { store } from "../store";
export const setNovel = (novel) => async (dispatch) => {
dispatch({ type: SET_NOVEL, payload: novel });
@@ -53,63 +57,74 @@ export const setNovel = (novel) => async (dispatch) => {
export const getNovelAction =
(followed, sourceId, novelUrl, novelId, sort, filter) =>
async (dispatch) => {
dispatch({ type: LOADING_NOVEL });
try {
dispatch({ type: LOADING_NOVEL });
if (followed === 1) {
/**
* If novel is followed directly get the chapters from db.
*/
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({ type: FETCHING_NOVEL });
/**
* Check if novel is cached.
*/
const novel = await getNovel(sourceId, novelUrl);
if (novel) {
if (followed === 1) {
/**
* Get chapters from db.
* If novel is followed directly get the chapters from db.
*/
novel.chapters = await getChapters(novel.novelId, sort, filter);
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_NOVEL,
payload: novel,
type: GET_CHAPTERS,
payload: chapters,
});
} else {
/**
* Fetch novel from source.
*/
const fetchedNovel = await fetchNovel(sourceId, novelUrl);
dispatch({ type: FETCHING_NOVEL });
/**
* Insert novel in db.
*/
const fetchedNovelId = await insertNovel(fetchedNovel);
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
/**
* Get novel from db.
* Check if novel is cached.
*/
const novel = await getNovel(sourceId, novelUrl);
novel.chapters = await getChapters(novel.novelId);
dispatch({
type: GET_NOVEL,
payload: novel,
});
if (novel) {
/**
* Get chapters from db.
*/
novel.chapters = await getChapters(
novel.novelId,
sort,
filter
);
dispatch({
type: GET_NOVEL,
payload: novel,
});
} else {
/**
* Fetch novel from source.
*/
const fetchedNovel = await fetchNovel(sourceId, novelUrl);
/**
* Insert novel in db.
*/
const fetchedNovelId = await insertNovel(fetchedNovel);
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
/**
* Get novel from db.
*/
const novel = await getNovel(sourceId, novelUrl);
novel.chapters = await getChapters(novel.novelId);
dispatch({
type: GET_NOVEL,
payload: novel,
});
}
}
} catch (error) {
showToast(error.message);
// dispatch({ type: NOVEL_ERROR });
}
};
export const sortAndFilterChapters =
(novelId, sort, filter) => async (dispatch) => {
dispatch({ type: FETCHING_NOVEL });
const chapters = await getChapters(novelId, sort, filter);
dispatch({
@@ -123,6 +138,13 @@ export const sortAndFilterChapters =
});
};
export const showChapterTitlesAction = (novelId, value) => async (dispatch) => {
dispatch({
type: SET_NOVEL_SETTINGS,
payload: { novelId, value: value },
});
};
export const followNovelAction = (novel) => async (dispatch) => {
await followNovel(novel.followed, novel.novelId);
@@ -145,23 +167,47 @@ export const followNovelAction = (novel) => async (dispatch) => {
showToast(!novel.followed ? "Added to library" : "Removed from library");
};
export const bookmarkChapterAction =
(bookmark, chapterId) => async (dispatch) => {
await bookmarkChapter(bookmark, chapterId);
export const bookmarkChapterAction = (chapters) => async (dispatch) => {
chapters.map((chapter) => {
bookmarkChapter(chapter.bookmark, chapter.chapterId);
dispatch({
type: BOOKMARK_CHAPTER,
payload: { bookmark, chapterId },
payload: {
bookmark: chapter.bookmark,
chapterId: chapter.chapterId,
},
});
};
});
};
export const markChapterReadAction =
(chapterId, novelId) => async (dispatch) => {
await markChapterRead(chapterId);
dispatch({ type: CHAPTER_READ, payload: { chapterId } });
dispatch({
type: CHAPTER_READ,
payload: { chapterId },
});
const nextChapter = await getNextChapterFromDB(novelId, chapterId);
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId: chapterId + 1 },
payload: { novelId, chapterId: nextChapter.chapterId },
});
/**
* Reset progress on marked read
*/
dispatch({
type: SAVE_SCROLL_POSITION,
payload: {
position: 0,
percentage: 0,
chapterId,
novelId,
},
});
};
@@ -169,21 +215,56 @@ export const markPreviousChaptersReadAction =
(chapterId, novelId) => async (dispatch) => {
await markPreviuschaptersRead(chapterId, novelId);
dispatch({ type: MARK_PREVIOUS_CHAPTERS_READ, payload: chapterId });
dispatch({
type: MARK_PREVIOUS_CHAPTERS_READ,
payload: chapterId,
});
showToast("Marked previous chapters read");
};
export const markChaptersRead = (chapters, novelId) => async (dispatch) => {
try {
const { sort = "ORDER BY chapterId ASC", filter = "" } =
store.getState().preferenceReducer.novelSettings[novelId];
chapters.map((chapter) => markChapterRead(chapter.chapterId));
const chaps = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chaps,
});
} catch (error) {
showToast(error.message);
}
};
export const markPreviousChaptersUnreadAction =
(chapterId, novelId) => async (dispatch) => {
await markPreviousChaptersUnread(chapterId, novelId);
dispatch({ type: MARK_PREVIOUS_CHAPTERS_UNREAD, payload: chapterId });
dispatch({
type: MARK_PREVIOUS_CHAPTERS_UNREAD,
payload: chapterId,
});
};
export const markChapterUnreadAction = (chapterId) => async (dispatch) => {
await markChapterUnread(chapterId);
export const markChapterUnreadAction =
(chapters, novelId) => async (dispatch) => {
const { sort = "ORDER BY chapterId ASC", filter = "" } =
store.getState().preferenceReducer.novelSettings[novelId];
dispatch({ type: CHAPTER_UNREAD, payload: { chapterId } });
};
chapters.map((chapter) => markChapterUnread(chapter.chapterId));
const chaps = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chaps,
});
};
export const downloadChapterAction =
(extensionId, novelUrl, chapterUrl, chapterName, chapterId) =>
@@ -205,30 +286,36 @@ export const downloadChapterAction =
export const downloadAllChaptersAction =
(extensionId, novelUrl, chapters) => async (dispatch) => {
await chapters.map((chapter, index) => {
setTimeout(async () => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapter.chapterId,
});
try {
await chapters.map((chapter, index) => {
setTimeout(async () => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapter.chapterId,
});
if (!chapter.downloaded) {
await downloadChapter(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterId
);
}
if (!chapter.downloaded) {
await downloadChapter(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterId
);
}
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapter.chapterId,
});
}, 1000 * index);
});
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapter.chapterId,
});
showToast(`All chapters downloaded`);
if (index + 1 === chapters.length) {
showToast(`Download completed `);
}
}, 1000 * index);
});
} catch (error) {
showToast(error.message);
}
};
export const deleteChapterAction =
@@ -253,7 +340,7 @@ export const deleteAllChaptersAction = (chapters) => async (dispatch) => {
});
});
showToast("Deleted all chapters");
showToast("Chapters deleted");
};
export const updateNovelAction =
+9
View File
@@ -8,6 +8,7 @@ import {
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
NOVEL_ERROR,
CHAPTER_READ,
UPDATE_NOVEL,
UPDATE_LAST_READ,
@@ -140,6 +141,14 @@ const novelReducer = (state = initialState, action) => {
: chapter
),
};
case NOVEL_ERROR:
return {
...state,
novel: {},
chapters: [],
loading: false,
updating: false,
};
default:
return state;
}
+1 -1
View File
@@ -14,7 +14,7 @@ export const CHAPTER_DOWNLOADING = "CHAPTER_DOWNLOADING";
export const CHAPTER_READ = "CHAPTER_READ";
export const CHAPTER_UNREAD = "CHAPTER_UNREAD";
export const GET_CHAPTER = "GET_CHAPTER";
export const NOVEL_ERROR = "NOVEL_ERROR";
export const BOOKMARK_CHAPTER = "BOOKMARK_CHAPTER";
export const SET_NOVEL_SETTINGS = "SET_NOVEL_SETTINGS";
@@ -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,
},
});
};
+12 -1
View File
@@ -1,3 +1,4 @@
import { SET_NOVEL_SETTINGS } from "../novel/novel.types";
import {
SAVE_SCROLL_POSITION,
SET_CHAPTER_LIST_PREF,
@@ -35,7 +36,17 @@ const novelReducer = (state = initialState, action) => {
},
},
};
case SET_NOVEL_SETTINGS:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
showChapterTitles: payload.value,
},
},
};
case SAVE_SCROLL_POSITION:
return {
...state,
+17 -41
View File
@@ -1,66 +1,42 @@
import {
SWITCH_THEME,
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
UPDATE_READER_TEXT_ALIGN,
UPDATE_READER_PADDING,
SET_READER_FONT,
SET_APP_THEME,
SET_NOVELS_PER_ROW,
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
} from "./settings.types";
export const switchTheme = (val) => (dispatch) => {
export const setAppTheme = (val) => (dispatch) => {
dispatch({
type: SWITCH_THEME,
type: SET_APP_THEME,
payload: val,
});
};
export const setDisplayMode = (val) => (dispatch) => {
export const setNovelsPerRow = (val) => (dispatch) => {
dispatch({
type: SET_DISPLAY_MODE,
type: SET_NOVELS_PER_ROW,
payload: val,
});
};
export const setItemsPerRow = (val) => (dispatch) => {
export const setAppSettings = (key, val) => (dispatch) => {
dispatch({
type: SET_ITEMS_PER_ROW,
payload: val,
type: SET_APP_SETTINGS,
payload: { key, val },
});
};
export const updateReaderTheme = (val) => (dispatch) => {
export const setReaderSettings = (key, val) => (dispatch) => {
dispatch({
type: UPDATE_READER_THEME,
payload: val,
type: SET_READER_SETTINGS,
payload: { key, val },
});
};
export const updateReaderTextSize = (val) => (dispatch) => {
export const setAccentColor = (val) => (dispatch) => {
dispatch({
type: UPDATE_READER_TEXT_SIZE,
payload: val,
});
};
export const updateReaderTextAlign = (val) => (dispatch) => {
dispatch({
type: UPDATE_READER_TEXT_ALIGN,
payload: val,
});
};
export const updateReaderPadding = (val) => (dispatch) => {
dispatch({
type: UPDATE_READER_PADDING,
payload: val,
});
};
export const setReaderFont = (val) => (dispatch) => {
dispatch({
type: SET_READER_FONT,
type: SET_ACCENT_COLOR,
payload: val,
});
};
+40 -44
View File
@@ -1,13 +1,9 @@
import { SET_NOVEL_SETTINGS } from "../novel/novel.types";
import {
SWITCH_THEME,
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
SET_READER_FONT,
UPDATE_READER_PADDING,
UPDATE_READER_TEXT_ALIGN,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
SET_APP_THEME,
SET_NOVELS_PER_ROW,
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
} from "./settings.types";
import {
amoledDarkTheme,
@@ -27,71 +23,71 @@ const themes = {
5: irisBlueTheme,
};
const initialState = {
displayMode: 0,
itemsPerRow: 3,
reader: {
theme: 1,
textSize: 16,
textAlign: "left",
padding: 5,
fontFamily: null,
},
theme: themes[5],
};
/**
* Display Mode
*
* 0 -> Compact
* 1 -> Comfortable
* 2 -> List
*/
/**
* Reader Settings
* theme
* Reader Theme
*
* 1 -> Dark
* 2 -> White
* 3 -> Sepia
*/
const initialState = {
theme: darkTheme,
displayMode: 0,
novelsPerRow: 3,
reader: {
theme: 1,
textSize: 16,
textAlign: "left",
padding: 5,
fontFamily: "",
lineHeight: 1.5,
},
showDownloadBadges: true,
showUnreadBadges: true,
showNumberOfNovels: false,
};
const settingsReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case SWITCH_THEME:
case SET_APP_THEME:
return { ...state, theme: themes[payload] };
case SET_DISPLAY_MODE:
case SET_NOVELS_PER_ROW:
return {
...state,
displayMode: payload,
novelsPerRow: state.displayMode !== 2 ? payload : 1,
};
case SET_ITEMS_PER_ROW:
case SET_ACCENT_COLOR:
return {
...state,
itemsPerRow: state.displayMode !== 2 ? payload : 1,
theme: {
...state.theme,
colorAccent: payload,
},
};
case UPDATE_READER_THEME:
return { ...state, reader: { ...state.reader, theme: payload } };
case UPDATE_READER_TEXT_SIZE:
return { ...state, reader: { ...state.reader, textSize: payload } };
case UPDATE_READER_TEXT_ALIGN:
case SET_APP_SETTINGS:
return {
...state,
reader: { ...state.reader, textAlign: payload },
[payload.key]: payload.val,
};
case UPDATE_READER_PADDING:
case SET_READER_SETTINGS:
return {
...state,
reader: { ...state.reader, padding: payload },
reader: {
...state.reader,
[payload.key]: payload.val,
},
};
case SET_READER_FONT:
return {
...state,
reader: { ...state.reader, fontFamily: payload },
};
case SET_NOVEL_SETTINGS:
console.log(state.novelSettings);
default:
return state;
}
+5 -8
View File
@@ -1,8 +1,5 @@
export const SWITCH_THEME = "SWITCH_THEME";
export const SET_DISPLAY_MODE = "SET_DISPLAY_MODE";
export const SET_ITEMS_PER_ROW = "SET_ITEMS_PER_ROW";
export const UPDATE_READER_THEME = "UPDATE_READER_THEME";
export const UPDATE_READER_TEXT_SIZE = "UPDATE_READER_TEXT_SIZE";
export const UPDATE_READER_TEXT_ALIGN = "UPDATE_READER_TEXT_ALIGN";
export const UPDATE_READER_PADDING = "UPDATE_READER_PADDING";
export const SET_READER_FONT = "SET_READER_FONT";
export const SET_APP_SETTINGS = "SET_APP_SETTINGS";
export const SET_APP_THEME = "SET_APP_THEME";
export const SET_NOVELS_PER_ROW = "SET_NOVELS_PER_ROW";
export const SET_READER_SETTINGS = "SET_READER_SETTINGS";
export const SET_ACCENT_COLOR = "SET_ACCENT_COLOR";
+13 -6
View File
@@ -1,19 +1,26 @@
import { GET_SOURCES, PIN_SOURCES, SEARCH_SOURCES } from "./source.types";
import { fetchSources } from "../../Services/Source/source";
import { showToast } from "../../Hooks/showToast";
export const getSourcesAction = () => async (dispatch) => {
const res = await fetchSources();
try {
const res = await fetchSources();
dispatch({
type: GET_SOURCES,
payload: res,
});
dispatch({
type: GET_SOURCES,
payload: res,
});
} catch (error) {
showToast(error.message);
}
};
export const searchSourcesAction = (searchText) => async (dispatch) => {
const res = searchText.toLowerCase();
dispatch({
type: SEARCH_SOURCES,
payload: searchText,
payload: res,
});
};
+1 -1
View File
@@ -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:
+1 -3
View File
@@ -6,7 +6,6 @@ import thunk from "redux-thunk";
import settingsReducer from "./settings/settings.reducer";
import libraryReducer from "./library/library.reducer";
import sourceReducer from "./source/source.reducers";
import chapterReducer from "./chapter/chapter.reducer";
import novelReducer from "./novel/novel.reducer";
import historyReducer from "./history/history.reducer";
import updatesReducer from "./updates/updates.reducer";
@@ -16,7 +15,7 @@ import preferenceReducer from "./preferences/preference.reducer";
const persistConfig = {
key: "root",
storage: AsyncStorage,
blacklist: ["novelReducer", "chapterReducer"],
blacklist: ["novelReducer"],
};
const persistedReducer = persistReducer(
@@ -27,7 +26,6 @@ const persistedReducer = persistReducer(
sourceReducer,
novelReducer,
historyReducer,
chapterReducer,
updatesReducer,
trackerReducer,
preferenceReducer,
+24 -3
View File
@@ -8,14 +8,35 @@ import { showToast } from "../../Hooks/showToast";
export const getUpdatesAction = () => async (dispatch) => {
const updates = await getUpdates();
dispatch({ type: GET_UPDATES, payload: updates });
const groups = updates.reduce((groups, update) => {
var dateParts = update.updateTime.split("-");
var jsDate = new Date(
dateParts[0],
dateParts[1] - 1,
dateParts[2].substr(0, 2)
);
const date = jsDate.toISOString();
if (!groups[date]) {
groups[date] = [];
}
groups[date].push(update);
return groups;
}, {});
const groupedUpdates = Object.keys(groups).map((date) => {
return {
date,
chapters: groups[date],
};
});
dispatch({ type: GET_UPDATES, payload: groupedUpdates });
};
export const updateLibraryAction = () => async (dispatch) => {
showToast("Updating library");
await updateAllNovels();
const updates = await getUpdates();
dispatch({ type: GET_UPDATES, payload: updates });
dispatch(getUpdatesAction());
};