Compare commits

...

26 Commits

Author SHA1 Message Date
Rajarshee Chatterjee daf9444e87 Merge branch 'main' of https://github.com/rajarsheechatterjee/LNReader into main 2021-05-06 21:00:17 +05:30
Rajarshee Chatterjee e4b1b757e6 Fix navigating to novel from history screen 2021-05-06 20:59:22 +05:30
Rajarshee Chatterjee b01e63791e Release v1.0.17 2021-05-06 20:55:23 +05:30
Rajarshee Chatterjee ffe4950966 Update screenshots 2021-05-06 17:07:25 +05:30
Rajarshee Chatterjee caac2c3227 Refactor 2021-05-06 16:10:22 +05:30
Rajarshee Chatterjee 489d5ba714 Add global search (#12)
* Add global search

* Split GlobalSearch into components
2021-05-05 20:17:17 +05:30
Rajarshee Chatterjee 4251ec7f20 Pin sources and refactor 2021-05-05 14:19:08 +05:30
Rajarshee Chatterjee c1e0b01df8 Refactor and some fixes 2021-05-04 21:44:17 +05:30
Rajarshee Chatterjee 66dc1c4bdb fix continue reading button 2021-05-04 15:13:12 +05:30
Rajarshee Chatterjee 1ba88a16ff Save chapter sort and filter settings 2021-05-04 12:15:02 +05:30
Rajarshee Chatterjee 381c779c36 Release v1.0.16 2021-05-03 22:06:08 +05:30
Rajarshee Chatterjee 90df6b5aca Refactor 2021-05-03 20:49:06 +05:30
Rajarshee Chatterjee 87ff17d5e5 Use Expo StorageAccessFramework for backups 2021-05-01 17:51:50 +05:30
Rajarshee Chatterjee 913d319222 Check github for new release (closes #9) 2021-05-01 17:24:33 +05:30
Rajarshee Chatterjee cca143383e Upgrade Expo SDK to 41 (closes #7) 2021-05-01 08:38:06 +05:30
Rajarshee Chatterjee 4a874b30be Move useTheme hook to Hooks folder 2021-05-01 08:11:50 +05:30
Rajarshee Chatterjee e958b0fa09 Refactor 2021-05-01 07:57:16 +05:30
Rajarshee Chatterjee a484d550a8 Fix navigation to novel from history item 2021-04-30 23:35:48 +05:30
Rajarshee Chatterjee 4acafdb741 Add backup/restore (closes #6) 2021-04-30 23:18:09 +05:30
Rajarshee Chatterjee 1e6c4758ff Check github for new release on launch 2021-04-30 20:52:05 +05:30
Rajarshee Chatterjee 1ed0e8b4c4 Fix tracker crashes and update app version 2021-04-30 12:01:34 +05:30
Rajarshee Chatterjee 458cdb2c2a Add update chapter progress after reading 2021-04-30 11:16:14 +05:30
Rajarshee Chatterjee 0a476ba6b4 Update api url 2021-04-30 11:15:58 +05:30
Rajarshee Chatterjee 6e85909f6f Finish mal tracking and refactor 2021-04-29 20:36:32 +05:30
Rajarshee Chatterjee 5642122a5e Add updating mal list logic 2021-04-28 22:33:41 +05:30
Rajarshee Chatterjee 77481dfc06 Add MAL tracking logic 2021-04-27 20:33:22 +05:30
117 changed files with 7123 additions and 3776 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+20 -43
View File
@@ -1,55 +1,32 @@
import React, { useState, useEffect } from "react";
import { StatusBar } from "expo-status-bar";
import { NavigationContainer } from "@react-navigation/native";
import * as Font from "expo-font";
import AppLoading from "expo-app-loading";
import React, { useEffect } from "react";
import { useFonts } from "expo-font";
import { Provider as PaperProvider } from "react-native-paper";
import Main from "./src/Navigators/Main";
import Router from "./src/navigation/Router";
// Redux
import { Provider } from "react-redux";
import { persistor, store } from "./src/redux/store";
import { PersistGate } from "redux-persist/lib/integration/react";
import { createDB, deleteDb } from "./src/database/DBHelper";
const getFonts = () =>
Font.loadAsync({
"pt-sans-bold": require("./assets/fonts/PTSansNarrow-Bold.ttf"),
"arbutus-slab": require("./assets/fonts/ArbutusSlab-Regular.ttf"),
domine: require("./assets/fonts/Domine-VariableFont_wght.ttf"),
lato: require("./assets/fonts/Lato-Regular.ttf"),
"noto-sans": require("./assets/fonts/NotoSans-Regular.ttf"),
"open-sans": require("./assets/fonts/OpenSans-Regular.ttf"),
"pt-serif": require("./assets/fonts/PTSerif-Regular.ttf"),
});
import { createDB, deleteDb } from "./src/Database/DBHelper";
import { fonts } from "./src/Theme/fonts";
const App = () => {
const [fontsLoaded, setFontsLoaded] = useState(false);
const [loaded] = useFonts(fonts);
useEffect(() => {
createDB();
}, []);
useEffect(() => createDB(), []);
if (fontsLoaded) {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<StatusBar />
<NavigationContainer>
<Router />
</NavigationContainer>
</PersistGate>
</Provider>
);
} else {
return (
<AppLoading
startAsync={getFonts}
onError={() => warn(error)}
onFinish={() => setFontsLoaded(true)}
/>
);
if (!loaded) {
return null;
}
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<PaperProvider>
<Main />
</PaperProvider>
</PersistGate>
</Provider>
);
};
export default App;
+2 -1
View File
@@ -2,7 +2,8 @@
"expo": {
"name": "LNReader",
"slug": "LNReader",
"version": "1.0.0",
"scheme": "lnreader",
"version": "1.0.16",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+2491 -1766
View File
File diff suppressed because it is too large Load Diff
+29 -20
View File
@@ -8,46 +8,55 @@
"eject": "expo eject"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.13.2",
"@react-native-async-storage/async-storage": "^1.13.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "^5.9.7",
"@react-native-community/slider": "^3.0.3",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/slider": "3.0.3",
"@react-navigation/material-bottom-tabs": "^5.3.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "~40.0.0",
"expo": "^41.0.1",
"expo-app-loading": "^1.0.1",
"expo-font": "^8.4.0",
"expo-linear-gradient": "^8.4.0",
"expo-sharing": "~8.5.0",
"expo-sqlite": "~8.5.0",
"expo-status-bar": "~1.0.3",
"expo-web-browser": "~8.6.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-permissions": "^12.0.1",
"expo-sharing": "~9.1.2",
"expo-sqlite": "~9.1.0",
"expo-status-bar": "~1.0.4",
"expo-web-browser": "~9.1.0",
"moment": "^2.29.1",
"qs": "^6.10.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
"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-gesture-handler": "^1.8.0",
"react-native-easing-gradient": "^0.1.2",
"react-native-gesture-handler": "~1.10.2",
"react-native-offline": "^5.7.0",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "^1.13.2",
"react-native-safe-area-context": "3.1.9",
"react-native-screens": "~2.15.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-vector-icons": "^7.1.0",
"react-native-web": "~0.13.12",
"react-native-webview": "11.0.0",
"react-native-webview": "11.2.3",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"reflect-metadata": "^0.1.13",
"rn-sliding-up-panel": "^2.4.4"
"rn-sliding-up-panel": "^2.4.4",
"expo-notifications": "~0.11.6",
"expo-background-fetch": "~9.1.0",
"expo-task-manager": "~9.1.0"
},
"devDependencies": {
"@babel/core": "~7.9.0",
"@types/node": "^14.14.35"
"@babel/core": "~7.9.0"
},
"private": true
}
@@ -11,7 +11,7 @@ import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
export const Appbar = ({ title, onBackAction, style }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<MaterialAppbar.Header
@@ -42,7 +42,7 @@ export const SearchAppbar = ({
const navigation = useNavigation();
const theme = useSelector((state) => state.themeReducer.theme);
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<TouchableRipple
@@ -3,12 +3,12 @@ import { View } from "react-native";
import { useSelector } from "react-redux";
const BottomSheetHandle = () => {
const theme = useSelector((state) => state.themeReducer.theme);
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<>
<View
style={{
backgroundColor: theme.textColorHintDark,
backgroundColor: theme.textColorHint,
height: 5,
width: 30,
borderRadius: 50,
@@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from "react-native";
import { useSelector } from "react-redux";
const EmptyView = ({ icon, description }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<View style={styles.emptyViewContainer}>
@@ -8,13 +8,13 @@ const ListSection = ({ children }) => (
const ListSubHeader = ({ children, theme }) => (
<List.Subheader
style={[styles.listSubHeader, { color: theme.colorAccentDark }]}
style={[styles.listSubHeader, { color: theme.colorAccent }]}
>
{children}
</List.Subheader>
);
const ListItem = ({ title, description, icon, onPress, theme }) => (
const ListItem = ({ title, description, icon, onPress, theme, right }) => (
<List.Item
title={title}
titleStyle={{ color: theme.textColorPrimary }}
@@ -24,27 +24,48 @@ const ListItem = ({ title, description, icon, onPress, theme }) => (
left={() =>
icon && (
<List.Icon
color={theme.colorAccentDark}
color={theme.colorAccent}
icon={icon}
style={{ marginVertical: 0 }}
/>
)
}
right={() =>
right && (
<List.Icon
color="#47a84a"
icon={right}
style={{ marginVertical: 0 }}
/>
)
}
onPress={onPress}
rippleColor={theme.rippleColor}
/>
);
const Divider = ({ theme }) => (
<PaperDivider
style={{
backgroundColor: theme.dividerColor,
height: 1,
}}
<PaperDivider style={{ backgroundColor: theme.dividerColor, height: 1 }} />
);
const InfoItem = ({ title, icon, theme }) => (
<List.Item
title={title}
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
titleNumberOfLines={5}
left={() =>
icon && (
<List.Icon
color={theme.textColorSecondary}
icon={icon}
style={{ marginVertical: 0 }}
/>
)
}
/>
);
export { ListSection, ListSubHeader, ListItem, Divider };
export { ListSection, ListSubHeader, ListItem, Divider, InfoItem };
const styles = StyleSheet.create({
listSection: {
+64
View File
@@ -0,0 +1,64 @@
import React from "react";
import { StyleSheet, View, Text, Image } from "react-native";
import { TouchableRipple } from "react-native-paper";
const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
return (
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.listView}
onPress={onPress}
>
<>
<Image
source={{ uri: item.novelCover }}
style={styles.extensionIcon}
/>
<Text
style={[
{ color: theme.textColorPrimary },
styles.novelName,
]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={styles.badgeContainer}>
{downloadBadge}
{unreadBadge}
</View>
</>
</TouchableRipple>
);
};
export default ListView;
const styles = StyleSheet.create({
listView: {
flex: 1,
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 12,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 4,
},
novelName: {
flex: 1,
marginLeft: 16,
fontSize: 15,
paddingRight: 8,
flexWrap: "wrap",
},
badgeContainer: {
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "flex-end",
},
});
+80
View File
@@ -0,0 +1,80 @@
import React, { useState } from "react";
import { StyleSheet, View, Text } from "react-native";
import { Portal, Modal, Button } from "react-native-paper";
import * as Linking from "expo-linking";
import { useTheme } from "../Hooks/reduxHooks";
const NewUpdateDialog = ({ newVersion }) => {
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
const theme = useTheme();
return (
<Portal>
<Modal
visible={newUpdateDialog}
onDismiss={() => showNewUpdateDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Text
style={[
styles.modalHeader,
{ color: theme.textColorPrimary },
]}
>
{`New update available ${newVersion.tag_name}`}
</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 16 }}>
{newVersion.body}
</Text>
<View
style={{
flexDirection: "row",
marginTop: 30,
justifyContent: "flex-end",
}}
>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => showNewUpdateDialog(false)}
>
Cancel
</Button>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => Linking.openURL(newVersion.downloadUrl)}
>
Install
</Button>
</View>
</Modal>
</Portal>
);
};
export default NewUpdateDialog;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
modalHeader: {
fontSize: 20,
marginBottom: 10,
},
});
+179
View File
@@ -0,0 +1,179 @@
import React from "react";
import { StyleSheet, View, Text, ImageBackground, Image } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { TouchableRipple } from "react-native-paper";
import { useSelector } from "react-redux";
import { useSettings, useTheme } from "../Hooks/reduxHooks";
import ListView from "./ListView";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const theme = useTheme();
const { displayMode, itemsPerRow } = useSettings();
const height = {
1: 550,
2: 280,
3: 180,
4: 140,
5: 100,
};
const comfortableTitle = () =>
displayMode === 1 && (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{item.novelName}
</Text>
);
const compactTitle = () =>
displayMode === 0 && (
<View style={styles.titleContainer}>
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.6)"]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
>
{item.novelName}
</Text>
</LinearGradient>
</View>
);
const unreadBadge = () =>
item.chaptersUnread && (
<Text
style={[
styles.unreadBadge,
!item.chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
]}
>
{item.chaptersUnread}
</Text>
);
const downloadBadge = () =>
item.chaptersDownloaded && (
<Text
style={[
styles.downloadBadge,
!item.chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
]}
>
{item.chaptersDownloaded}
</Text>
);
return displayMode !== 2 ? (
<View style={{ flex: 1 / itemsPerRow }}>
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.opac}
onPress={onPress}
>
<>
<ImageBackground
source={{ uri: item.novelCover }}
style={{ height: height[itemsPerRow] }}
imageStyle={[
{ borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
]}
progressiveRenderingEnabled={true}
>
<View style={{ flexDirection: "row", margin: 4 }}>
{downloadBadge()}
{unreadBadge()}
</View>
{compactTitle()}
</ImageBackground>
{comfortableTitle()}
</>
</TouchableRipple>
</View>
) : (
<ListView
item={item}
downloadBadge={downloadBadge()}
unreadBadge={unreadBadge()}
theme={theme}
onPress={onPress}
/>
);
};
export default NovelCover;
const styles = StyleSheet.create({
titleContainer: {
flex: 1,
justifyContent: "flex-end",
borderRadius: 4,
},
title: {
fontFamily: "pt-sans-bold",
fontSize: 14,
padding: 8,
},
linearGradient: {
bottom: -1,
borderRadius: 4,
},
opac: {
paddingHorizontal: 4.5,
paddingVertical: 4,
borderRadius: 4,
},
extensionIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
listView: {
flex: 1,
paddingVertical: 8,
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
borderRadius: 4,
},
downloadBadge: {
backgroundColor: "#47a84a",
color: "#FFFFFF",
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
unreadBadge: {
backgroundColor: "#2979FF",
color: "#FFFFFF",
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
});
+42
View File
@@ -0,0 +1,42 @@
import React from "react";
import { RefreshControl, StyleSheet, FlatList } from "react-native";
import { useSettings } from "../Hooks/reduxHooks";
const NovelList = ({
data,
renderItem,
refreshControl,
ListEmptyComponent,
}) => {
const { displayMode, itemsPerRow } = useSettings();
const getItemsPerRow = () => {
if (displayMode === 2) {
return 1;
} else {
return itemsPerRow;
}
};
return (
<FlatList
contentContainerStyle={styles.flatListCont}
numColumns={getItemsPerRow()}
key={getItemsPerRow()}
data={data}
keyExtractor={(item) => item.novelUrl}
renderItem={renderItem}
refreshControl={refreshControl}
ListEmptyComponent={ListEmptyComponent}
/>
);
};
export default NovelList;
const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
+36
View File
@@ -0,0 +1,36 @@
import React from "react";
import { StyleSheet, View, Text } from "react-native";
import { RadioButton as MaterialRadioButton } from "react-native-paper";
export const RadioButtonGroup = ({ children, value, onValueChange }) => (
<MaterialRadioButton.Group onValueChange={onValueChange} value={value}>
{children}
</MaterialRadioButton.Group>
);
export const RadioButton = ({ value, label, theme }) => (
<View style={styles.radioButtonContainer}>
<MaterialRadioButton
value={value}
color={theme.colorAccent}
uncheckedColor={theme.textColorSecondary}
/>
<Text
style={[styles.radioButtonLabel, { color: theme.textColorPrimary }]}
>
{label}
</Text>
</View>
);
const styles = StyleSheet.create({
radioButtonContainer: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
},
radioButtonLabel: {
marginLeft: 16,
fontSize: 16,
},
});
+124
View File
@@ -0,0 +1,124 @@
import React, { useRef } from "react";
import { View, TextInput, StyleSheet } from "react-native";
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";
export const Searchbar = ({
theme,
placeholder,
searchText,
left,
onPressLeft,
right,
displayMenu,
onPressRight,
clearSearchbar,
onChangeText,
onSubmitEditing,
}) => {
const searchRef = useRef(null);
const dispatch = useDispatch();
const { displayMode } = useSettings();
const displayMenuIcon = () => {
const icons = {
0: "view-module",
1: "view-list",
2: "view-module",
};
return icons[displayMode];
};
const getDisplayMode = () =>
displayMode === 0 ? 1 : displayMode === 1 ? 2 : displayMode === 2 && 0;
return (
<TouchableRipple
borderless
onPress={() => searchRef.current.focus()}
style={[
styles.searchAppbarContainer,
{ backgroundColor: theme.searchBarColor },
]}
rippleColor={theme.rippleColor}
>
<View style={styles.container}>
<View style={{ flex: 1, flexDirection: "row" }}>
{left && (
<IconButton
icon={left}
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
onPress={onPressLeft}
/>
)}
<TextInput
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorSecondary,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
defaultValue={searchText}
/>
</View>
{searchText !== "" && (
<IconButton
icon="close"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={clearSearchbar}
/>
)}
{displayMenu && (
<IconButton
icon={displayMenuIcon()}
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={() =>
dispatch(setDisplayMode(getDisplayMode()))
}
/>
)}
{right && (
<IconButton
icon={right}
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={onPressRight}
/>
)}
</View>
</TouchableRipple>
);
};
const styles = StyleSheet.create({
searchAppbarContainer: {
marginTop: Constants.statusBarHeight + 4,
height: 48,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
marginBottom: 12,
borderRadius: 8,
marginHorizontal: 12,
elevation: 2,
},
container: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
},
});
+8
View File
@@ -0,0 +1,8 @@
import React from "react";
import { View, Text as RNText, Image, StyleSheet } from "react-native";
const Text = () => {};
export default UpdateCard;
const styles = StyleSheet.create({});
@@ -35,7 +35,9 @@ export const insertChapters = async (novelId, chapters) => {
};
const getChaptersQuery = (sort, filter) =>
`SELECT * FROM chapters WHERE novelId = ? ${filter ?? ""} ${sort ?? ""}`;
`SELECT * FROM chapters WHERE novelId = ? ${filter || ""} ${
sort || "ORDER BY chapterId ASC"
}`;
export const getChapters = (novelId, sort, filter) => {
return new Promise((resolve, reject) =>
@@ -111,9 +113,7 @@ export const downloadChapter = async (
chapterUrl,
chapterId
) => {
const downloadUrl = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
// console.log(downloadUrl);
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
const response = await fetch(downloadUrl);
const chapter = await response.json();
@@ -159,26 +159,6 @@ export const deleteChapter = async (chapterId) => {
});
};
// export const deleteAllChapters = async (novelId) => {
// const updateIsDownloadedQuery = `UPDATE chapters SET downloaded = 0 WHERE novelId=?`;
// const deleteChapterQuery = `DELETE FROM downloads WHERE downloadChapterId=?`;
// db.transaction((tx) => {
// tx.executeSql(
// updateIsDownloadedQuery,
// [chapterId],
// (tx, res) => console.log(`Chaptermarked not downapdeod deleted`),
// (txObj, error) => console.log("Error ", error)
// );
// tx.executeSql(
// deleteChapterQuery,
// [chapterId],
// (tx, res) => console.log(`Chapter deleted`),
// (txObj, error) => console.log("Error ", error)
// );
// });
// };
const getLastReadChapterQuery = `
SELECT chapters.*
FROM history
@@ -1,6 +1,9 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import { fetchChapters } from "../../Services/Source/source";
import { insertChapters } from "./ChapterQueries";
const insertNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
export const insertNovel = async (novel) => {
@@ -60,12 +63,12 @@ export const checkNovelInCache = (novelUrl) => {
);
};
export const getNovel = async (novelUrl) => {
export const getNovel = async (sourceId, novelUrl) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
`SELECT * FROM novels WHERE novelUrl = ?`,
[novelUrl],
`SELECT * FROM novels WHERE novelUrl = ? AND sourceId = ?`,
[novelUrl, sourceId],
(txObj, { rows }) => resolve(rows.item(0)),
(txObj, error) => console.log("Error ", error)
);
@@ -85,3 +88,38 @@ export const deleteNovelCache = () => {
);
});
};
const restoreFromBackupQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed, unread) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
export const restoreLibrary = async (novel) => {
return new Promise((resolve, reject) =>
db.transaction((tx) =>
tx.executeSql(
restoreFromBackupQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
novel.followed,
novel.unread,
],
async (txObj, { insertId }) => {
const chapters = await fetchChapters(
novel.sourceId,
novel.novelUrl
);
await insertChapters(insertId, chapters);
},
(txObj, error) => console.log("Error ", error)
)
)
);
};
@@ -1,7 +1,7 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const getHistoryQuery = `
const getUpdatesQuery = `
SELECT chapters.chapterId, chapters.chapterUrl, chapters.chapterName, novels.novelUrl, novels.novelId, novels.novelCover, novels.novelName, novels.sourceId, updates.updateTime, updates.updateId
FROM updates
JOIN chapters
@@ -14,7 +14,7 @@ export const getUpdates = async () => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
getHistoryQuery,
getUpdatesQuery,
null,
(txObj, { rows: { _array } }) => {
resolve(_array);
@@ -24,3 +24,14 @@ export const getUpdates = async () => {
});
});
};
export const deleteNovelUpdates = (novelId) => {
db.transaction((tx) => {
tx.executeSql(
"DELETE FROM updates WHERE novelId = ?",
[novelId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
+46
View File
@@ -0,0 +1,46 @@
import { useState, useEffect } from "react";
export const githubUpdateChecker = () => {
const latestReleaseUrl =
"https://api.github.com/repos/rajarsheechatterjee/lnreader/releases/latest";
const [checking, setChecking] = useState(true);
const [latestRelease, setLatestRelease] = useState();
const checkForRelease = async () => {
let res = await fetch(latestReleaseUrl);
res = await res.json();
const release = {
tag_name: res.tag_name,
body: res.body,
downloadUrl: res.assets[0].browser_download_url,
};
setLatestRelease(release);
setChecking(false);
};
const isNewVersion = (versionTag) => {
let currentVersion = "v1.0.17";
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, "");
currentVersion = currentVersion.replace(regex, "");
return newVersion !== currentVersion;
};
useEffect(() => {
checkForRelease();
}, []);
if (!checking) {
const data = {
latestRelease,
isNewVersion: isNewVersion(latestRelease.tag_name),
};
return data;
}
};
+84
View File
@@ -0,0 +1,84 @@
import { useEffect } from "react";
import { useSelector } from "react-redux";
const useTheme = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
return theme;
};
const useReaderSettings = () => {
const readerSettings = useSelector((state) => state.settingsReducer.reader);
return readerSettings;
};
const useSettings = () => {
const { displayMode, itemsPerRow } = useSelector(
(state) => state.settingsReducer
);
return { displayMode, itemsPerRow };
};
const useLibrary = () => {
const library = useSelector((state) => state.libraryReducer.novels);
return library;
};
const useNovel = () => {
const novel = useSelector((state) => state.novelReducer);
return novel;
};
const findNovel = (novelId) => {
let novel = useSelector((state) => state.preferenceReducer.novelSettings);
novel = novel.find((novel) => novel.novelId === novelId);
return novel;
};
const usePreferences = (novelId) => {
let sort, filter;
const novel = findNovel(novelId);
if (novel) {
sort = novel.sort ?? "";
filter = novel.filter ?? "";
}
return { sort, filter };
};
const useContinueReading = (chapters, novelId) => {
let chapter, chapterId, novel;
novel = findNovel(novelId);
if (novel) {
chapterId = novel.lastRead;
}
if (chapterId) {
chapter = chapters.find((obj) => obj.chapterId === chapterId);
}
if (!chapter) {
chapter = chapters.find((obj) => obj.read === 0);
}
return chapter;
};
export {
useTheme,
useReaderSettings,
useLibrary,
useSettings,
usePreferences,
useContinueReading,
useNovel,
};
+7
View File
@@ -0,0 +1,7 @@
import { setStatusBarStyle as setExpoStatusBarStyle } from "expo-status-bar";
import { useTheme } from "../Hooks/reduxHooks";
export const setStatusBarStyle = () => {
const theme = useTheme();
setExpoStatusBarStyle(theme.id === 1 ? "dark" : "light");
};
+5
View File
@@ -0,0 +1,5 @@
import { ToastAndroid } from "react-native";
export const showToast = (message) => {
ToastAndroid.show(message, ToastAndroid.SHORT);
};
+92
View File
@@ -0,0 +1,92 @@
import React from "react";
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";
import History from "../Screens/History/History";
import Browse from "../Screens/Browse/Browse";
import More from "../Screens/More/More";
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>
);
};
export default BottomNavigator;
+69
View File
@@ -0,0 +1,69 @@
import React from "react";
import {
createStackNavigator,
TransitionPresets,
} from "@react-navigation/stack";
import BottomNavigator from "./BottomNavigator";
import Novel from "../Screens/Novel/Novel";
import Chapter from "../Screens/Chapter/Chapter";
import Extension from "../Screens/Extension/Extension";
import GlobalSearch from "../Screens/Browse/GlobalSearch";
import MoreStack from "./More";
import { setStatusBarStyle } from "../Hooks/setStatusBarStyle";
import { View } from "react-native";
import { useTheme } from "../Hooks/reduxHooks";
import { githubUpdateChecker } from "../Hooks/githubUpdateChecker";
import NewUpdateDialog from "../Components/NewUpdateDialog";
import { NavigationContainer } from "@react-navigation/native";
const Stack = createStackNavigator();
const stackNavigatorConfig = {
headerShown: false,
};
const MainNavigator = () => {
const theme = useTheme();
setStatusBarStyle();
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
return (
<NavigationContainer>
<View style={{ flex: 1, backgroundColor: theme.colorPrimary }}>
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen
name="BottomNavigator"
component={BottomNavigator}
/>
<Stack.Screen
name="Novel"
component={Novel}
options={{
headerTitle: "",
headerShown: true,
headerTransparent: true,
headerTintColor: "white",
...TransitionPresets.RevealFromBottomAndroid,
}}
/>
<Stack.Screen
name="Chapter"
component={Chapter}
options={{ ...TransitionPresets.SlideFromRightIOS }}
/>
<Stack.Screen name="MoreStack" component={MoreStack} />
<Stack.Screen name="Extension" component={Extension} />
<Stack.Screen
name="GlobalSearch"
component={GlobalSearch}
/>
</Stack.Navigator>
</View>
</NavigationContainer>
);
};
export default MainNavigator;
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
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";
const Stack = createStackNavigator();
const stackNavigatorConfig = { headerShown: false };
const MoreStack = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimary }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen name="Trackers" component={Trackers} />
<Stack.Screen name="About" component={About} />
</Stack.Navigator>
</View>
);
};
export default MoreStack;
+155
View File
@@ -0,0 +1,155 @@
import React, { useEffect, useState } from "react";
import {
StyleSheet,
View,
ScrollView,
Text,
FlatList,
ActivityIndicator,
RefreshControl,
} from "react-native";
import { useTheme } from "../../Hooks/reduxHooks";
import { useDispatch, useSelector } from "react-redux";
import {
getSourcesAction,
searchSourcesAction,
} from "../../redux/source/source.actions";
import SourceCard from "./components/SourceCard";
import { showToast } from "../../Hooks/showToast";
import { Searchbar } from "../../Components/Searchbar";
const Browse = ({ navigation }) => {
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
// const [globalSearch, setGlobalSearch] = useState(false);
// const [searchResult, setSearchResults] = useState();
const { sources, search, loading, pinned } = useSelector(
(state) => state.sourceReducer
);
const theme = useTheme();
const dispatch = useDispatch();
const isPinned = (sourceId) =>
pinned.indexOf(sourceId) === -1 ? false : true;
useEffect(() => {
dispatch(getSourcesAction());
}, [getSourcesAction]);
const onRefresh = () => {
showToast("Updating extension list");
setRefreshing(true);
getSourcesAction();
setRefreshing(false);
};
const clearSearchbar = () => {
setSearchText("");
};
const onChangeText = (text) => {
setSearchText(text);
dispatch(searchSourcesAction(text));
};
// const onSubmitEditing = () => {};
const renderItem = ({ item }) => (
<SourceCard
item={item}
isPinned={isPinned(item.sourceId)}
theme={theme}
/>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
theme={theme}
placeholder="Search Source"
left={"magnify"}
onPressLeft={clearSearchbar}
searchText={searchText}
onChangeText={onChangeText}
clearSearchbar={clearSearchbar}
right="book-search"
onPressRight={() => navigation.navigate("GlobalSearch")}
/>
<FlatList
contentContainerStyle={{ flexGrow: 1 }}
data={!searchText ? sources : search}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListHeaderComponent={
<View>
{pinned.length > 0 && (
<FlatList
contentContainerStyle={{ paddingBottom: 16 }}
data={sources.filter((source) =>
isPinned(source.sourceId)
)}
keyExtractor={(item) =>
item.sourceId.toString()
}
renderItem={renderItem}
extraData={pinned}
ListHeaderComponent={
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Pinned
</Text>
}
/>
)}
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Sources
</Text>
</View>
}
ListEmptyComponent={
loading && (
<ActivityIndicator
size="small"
color={theme.colorAccent}
style={{ marginTop: 16 }}
/>
)
}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["white"]}
progressBackgroundColor={theme.colorAccent}
/>
}
/>
</View>
);
};
export default Browse;
const styles = StyleSheet.create({
container: { flex: 1 },
});
+155
View File
@@ -0,0 +1,155 @@
import React, { useEffect, useState } from "react";
import {
StyleSheet,
View,
Text,
FlatList,
ActivityIndicator,
} from "react-native";
import { useLibrary, useTheme } from "../../Hooks/reduxHooks";
import { useDispatch, useSelector } from "react-redux";
import { Searchbar } from "../../Components/Searchbar";
import NovelCover from "../../Components/NovelCover";
import NovelList from "../../Components/NovelList";
import EmptyView from "../../Components/EmptyView";
import GlobalSearchNovelList from "./components/GlobalSearchNovelList";
const GlobalSearch = ({ navigation }) => {
const theme = useTheme();
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState("");
const [searchResults, setSearchResults] = useState("");
const { sources, pinned } = useSelector((state) => state.sourceReducer);
const library = useLibrary();
const pinnedSources = sources.filter(
(source) => pinned.indexOf(source.sourceId) !== -1
);
const clearSearchbar = () => {
setSearchText("");
};
const onChangeText = (text) => setSearchText(text);
const onSubmitEditing = () => {
setSearchResults("");
const getSearchUrl = (sourceId) => {
if (sourceId === 1) {
return `https://lnreader-extensions.vercel.app/api/1/search/?s=${searchText}&?o=rating`;
} else {
return `https://lnreader-extensions.vercel.app/api/${sourceId}/search/?s=${searchText}`;
}
};
pinnedSources.map((source, index) =>
setTimeout(() => {
setLoading(true);
fetch(getSearchUrl(source.sourceId))
.then((response) => response.json())
.then((json) => {
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
sourceLanguage: source.sourceLanguage,
novels: json,
},
]);
setLoading(false);
});
}, 3000 * index)
);
};
const renderItem = ({ item }) => (
<>
<View style={{ padding: 8, paddingVertical: 16 }}>
<Text style={{ color: theme.textColorPrimary }}>
{item.sourceName}
</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 12 }}>
{item.sourceLanguage}
</Text>
</View>
<GlobalSearchNovelList
data={item.novels}
theme={theme}
library={library}
navigation={navigation}
/>
</>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
theme={theme}
placeholder="Global Search"
left="arrow-left"
onPressLeft={() => navigation.goBack()}
searchText={searchText}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
clearSearchbar={clearSearchbar}
/>
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
data={searchResults}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in your pinned sources ${
pinned.length === 0
? "(No sources pinned)"
: ""
}`}
/>
)}
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: "center",
padding: 16,
}}
>
<ActivityIndicator
size="large"
color={theme.colorAccent}
/>
</View>
)
}
/>
</View>
);
};
export default GlobalSearch;
const styles = StyleSheet.create({
container: { flex: 1 },
});
@@ -0,0 +1,71 @@
import React from "react";
import { StyleSheet, View, Text, ImageBackground } from "react-native";
import { TouchableRipple } from "react-native-paper";
import { useSettings, useTheme } from "../../../Hooks/reduxHooks";
const GlobalSearchNovelCover = ({ item, onPress, libraryStatus }) => {
const theme = useTheme();
const comfortableTitle = () => (
<Text
numberOfLines={2}
style={[
styles.title,
{
width: 115,
color: theme.textColorPrimary,
padding: 4,
flexWrap: "wrap",
},
]}
>
{item.novelName}
</Text>
);
return (
<View style={{ flex: 1 }}>
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.opac}
onPress={onPress}
>
<>
<ImageBackground
source={{ uri: item.novelCover }}
style={{ height: 150, width: 115 }}
imageStyle={[
{ borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
]}
progressiveRenderingEnabled={true}
/>
{comfortableTitle()}
</>
</TouchableRipple>
</View>
);
};
export default GlobalSearchNovelCover;
const styles = StyleSheet.create({
titleContainer: {
flex: 1,
justifyContent: "flex-end",
borderRadius: 4,
},
title: {
fontFamily: "pt-sans-bold",
fontSize: 14,
padding: 8,
},
opac: {
paddingHorizontal: 5,
paddingVertical: 4,
borderRadius: 4,
},
});
@@ -0,0 +1,55 @@
import React from "react";
import { StyleSheet, FlatList, Text } from "react-native";
import GlobalSearchNovelCover from "./GlobalSearchNovelCover";
const GlobalSearchNovelList = ({ data, theme, library, navigation }) => {
const checkIFInLibrary = (sourceId, novelUrl) => {
return library.some(
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
);
};
const renderItem = ({ item }) => (
<GlobalSearchNovelCover
item={item}
onPress={() =>
navigation.navigate("Novel", {
...item,
sourceId: item.extensionId,
})
}
libraryStatus={checkIFInLibrary(item.extensionId, item.novelUrl)}
/>
);
return (
<FlatList
contentContainerStyle={styles.flatListCont}
horizontal={true}
data={data}
keyExtractor={(item) => item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 8,
paddingVertical: 4,
}}
>
No results found
</Text>
}
/>
);
};
export default GlobalSearchNovelList;
const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
@@ -1,29 +1,28 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple, Button } from "react-native-paper";
import { TouchableRipple, Button, IconButton } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { pinSourceAction } from "../../../redux/source/source.actions";
const ExtensionCard = ({ item }) => {
const { sourceName, sourceCover, sourceLanguage, status } = item;
const ExtensionCard = ({ item, theme, isPinned }) => {
const { sourceId, sourceName, sourceCover, sourceLanguage, status } = item;
const navigation = useNavigation();
const dispatch = useDispatch();
const theme = useSelector((state) => state.themeReducer.theme);
const navigateToExtension = () =>
navigation.navigate("Extension", {
sourceId: item.sourceId,
sourceName: item.sourceName,
sourceUrl: item.sourceUrl,
});
return (
<TouchableRipple
style={styles.extensionCard}
onPress={() =>
navigation.navigate("Extension", {
sourceId: item.sourceId,
sourceName: item.sourceName,
sourceUrl: item.sourceUrl,
})
}
onPress={navigateToExtension}
rippleColor={theme.rippleColor}
borderless
>
<>
<Image
@@ -50,34 +49,34 @@ const ExtensionCard = ({ item }) => {
>
{sourceLanguage}
</Text>
<Text
style={{
color: "#D63B2F",
fontSize: 12,
marginLeft: 5,
}}
>
{status === 0 && "(Not Working)"}
<Text style={styles.sourceStatus}>
{status === 0 && "• Down"}
</Text>
</View>
</View>
<View>
<View
style={{ flexDirection: "row", alignItems: "center" }}
>
<Button
labelStyle={{
letterSpacing: 0,
}}
labelStyle={{ letterSpacing: 0 }}
uppercase={false}
color={theme.colorAccentDark}
onPress={() =>
navigation.navigate("Extension", {
sourceId: item.sourceId,
sourceName: item.sourceName,
sourceUrl: item.sourceUrl,
})
}
color={theme.colorAccent}
onPress={navigateToExtension}
>
Browse
</Button>
<IconButton
icon={isPinned ? "pin" : "pin-outline"}
animated
size={21}
onPress={() => dispatch(pinSourceAction(sourceId))}
color={
isPinned
? theme.colorAccent
: theme.textColorSecondary
}
style={{ margin: 2 }}
/>
</View>
</View>
</>
@@ -107,4 +106,9 @@ const styles = StyleSheet.create({
alignItems: "center",
marginLeft: 16,
},
sourceStatus: {
color: "#C14033",
fontSize: 12,
marginLeft: 5,
},
});
@@ -12,14 +12,15 @@ import {
markChapterReadAction,
} from "../../redux/novel/novel.actions";
import { connect } from "react-redux";
import { connect, useSelector, useDispatch } from "react-redux";
import { parseChapterNumber } from "../../Services/updates";
import { updateChaptersRead } from "../../redux/tracker/tracker.actions";
const ChapterItem = ({
route,
navigation,
theme,
insertHistoryAction,
markChapterReadAction,
getChapterAction,
chapter,
loading,
@@ -35,6 +36,19 @@ const ChapterItem = ({
let _panel = useRef(null);
const dispatch = useDispatch();
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
const tracker = useSelector((state) => state.trackerReducer.tracker);
let isTracked = false;
if (!loading) {
isTracked = trackedNovels.find((obj) => obj.novelId === novelId);
}
useEffect(() => {
getChapterAction(extensionId, chapterUrl, novelUrl, chapterId);
insertHistoryAction(novelId, chapterId);
@@ -56,6 +70,8 @@ const ChapterItem = ({
return textColor;
};
const chapterNumber = !loading && parseChapterNumber(chapter.chapterName);
const readerLineHeight = (val) => {
const lineHeight = {
12: 20,
@@ -107,7 +123,7 @@ const ChapterItem = ({
size={26}
disabled={!chapter.prevChapter}
onPress={() => {
navigation.replace("ChapterItem", {
navigation.replace("Chapter", {
chapterUrl: chapter.prevChapter,
chapterId: chapterId - 1,
extensionId,
@@ -123,7 +139,7 @@ const ChapterItem = ({
size={26}
disabled={!chapter.nextChapter}
onPress={() => {
navigation.replace("ChapterItem", {
navigation.replace("Chapter", {
chapterUrl: chapter.nextChapter,
extensionId,
novelUrl,
@@ -153,7 +169,21 @@ const ChapterItem = ({
]}
onScroll={({ nativeEvent }) => {
if (isCloseToBottom(nativeEvent)) {
markChapterReadAction(chapterId);
dispatch(markChapterReadAction(chapterId, novelId));
if (
isTracked &&
chapterNumber &&
chapterNumber >
isTracked.my_list_status.num_chapters_read
) {
dispatch(
updateChaptersRead(
isTracked.id,
tracker.access_token,
chapterNumber
)
);
}
}
}}
>
@@ -161,7 +191,7 @@ const ChapterItem = ({
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
size={50}
color={theme.colorAccentDark}
color={theme.colorAccent}
/>
</View>
) : (
@@ -192,7 +222,7 @@ const ChapterItem = ({
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
theme: state.settingsReducer.theme,
chapter: state.novelReducer.chapter,
loading: state.novelReducer.chapterLoading,
reader: state.settingsReducer.reader,
@@ -200,7 +230,6 @@ const mapStateToProps = (state) => ({
export default connect(mapStateToProps, {
insertHistoryAction,
markChapterReadAction,
getChapterAction,
})(ChapterItem);
@@ -3,7 +3,7 @@ import { Animated, StyleSheet, View, Text, Dimensions } from "react-native";
import { ToggleButton, IconButton, Chip } from "react-native-paper";
import Slider from "@react-native-community/slider";
import Bottomsheet from "rn-sliding-up-panel";
import { fonts } from "../../../utils/constants";
import { fonts } from "../../../Services/utils/constants";
import { connect } from "react-redux";
@@ -14,7 +14,7 @@ import {
updateReaderTextSize,
updateReaderTheme,
} from "../../../redux/settings/settings.actions";
import BottomSheetHandle from "../../../components/BottomSheetHandle";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
const ChapterBottomSheet = ({
bottomSheetRef,
@@ -54,9 +54,9 @@ const ChapterBottomSheet = ({
minimumValue={12}
maximumValue={20}
step={2}
minimumTrackTintColor={theme.colorAccentDark}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccentDark}
thumbTintColor={theme.colorAccent}
onValueChange={(value) => updateReaderTextSize(value)}
/>
<View
@@ -185,7 +185,7 @@ const ChapterBottomSheet = ({
>
<IconButton
icon="minus"
color={theme.colorAccentDark}
color={theme.colorAccent}
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
@@ -202,7 +202,7 @@ const ChapterBottomSheet = ({
</Text>
<IconButton
icon="plus"
color={theme.colorAccentDark}
color={theme.colorAccent}
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
@@ -253,7 +253,7 @@ const ChapterBottomSheet = ({
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
theme: state.settingsReducer.theme,
reader: state.settingsReducer.reader,
});
+139
View File
@@ -0,0 +1,139 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, View, ActivityIndicator } from "react-native";
import * as WebBrowser from "expo-web-browser";
import NovelCover from "../../Components/NovelCover";
import EmptyView from "../../Components/EmptyView";
import { Searchbar } from "../../Components/Searchbar";
import { useTheme, useLibrary } from "../../Hooks/reduxHooks";
import NovelList from "../../Components/NovelList";
const Extension = ({ navigation, route }) => {
const { sourceId, sourceName, sourceUrl } = route.params;
const theme = useTheme();
const library = useLibrary();
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState();
const [searchText, setSearchText] = useState("");
const getSourceUrl = () => {
if (sourceId === 1) {
return `https://lnreader-extensions.vercel.app/api/1/novels/1/?o=rating`;
} else {
return `https://lnreader-extensions.vercel.app/api/${sourceId}/novels/`;
}
};
const getSearchUrl = () => {
if (sourceId === 1) {
return `https://lnreader-extensions.vercel.app/api/1/search/?s=${searchText}&?o=rating`;
} else {
return `https://lnreader-extensions.vercel.app/api/${sourceId}/search/?s=${searchText}`;
}
};
const getNovels = () => {
fetch(getSourceUrl())
.then((response) => response.json())
.then((json) => {
setNovels(json);
setLoading(false);
});
};
const clearSearchbar = () => {
getNovels();
setLoading(true);
setSearchText("");
};
const getSearchResults = () => {
setLoading(true);
fetch(getSearchUrl())
.then((response) => response.json())
.then((json) => {
setNovels(json);
setLoading(false);
});
};
const checkIFInLibrary = (sourceId, novelUrl) => {
return library.some(
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
);
};
useEffect(() => {
getNovels();
}, []);
const renderItem = ({ item }) => (
<NovelCover
item={item}
onPress={() =>
navigation.navigate("Novel", {
novelName: item.novelName,
novelCover: item.novelCover,
novelUrl: item.novelUrl,
sourceId: item.extensionId,
})
}
libraryStatus={checkIFInLibrary(item.extensionId, item.novelUrl)}
/>
);
const listEmptyComponent = () =>
novels.length === 0 && (
<EmptyView icon="(; ̄Д ̄)" description="No results found" />
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
theme={theme}
placeholder={`Search ${sourceName}`}
left="arrow-left"
onPressLeft={() => navigation.goBack()}
searchText={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchResults}
clearSearchbar={clearSearchbar}
right="earth"
displayMenu={true}
onPressRight={() => WebBrowser.openBrowserAsync(sourceUrl)}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={60} color={theme.colorAccent} />
</View>
) : (
<NovelList
data={novels}
renderItem={renderItem}
ListEmptyComponent={listEmptyComponent()}
/>
)}
</View>
);
};
export default Extension;
const styles = StyleSheet.create({
container: {
flex: 1,
},
contentContainer: {
flex: 1,
},
});
@@ -1,13 +1,11 @@
import React, { useState, useCallback } from "react";
import { useFocusEffect } from "@react-navigation/native";
import React, { useEffect } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar } from "../../components/Appbar";
import { Appbar } from "../../Components/Appbar";
import HistoryCard from "./components/HistoryCard";
import { connect } from "react-redux";
import EmptyView from "../../components/EmptyView";
import EmptyView from "../../Components/EmptyView";
import {
getHistoryAction,
@@ -22,15 +20,14 @@ const History = ({
getHistoryAction,
deleteHistoryAction,
}) => {
useFocusEffect(
useCallback(() => {
getHistoryAction();
}, [getHistoryAction])
);
useEffect(() => {
getHistoryAction();
}, [getHistoryAction]);
const renderHistoryCard = ({ item }) => (
<HistoryCard
item={item}
theme={theme}
deleteHistoryAction={deleteHistoryAction}
navigation={navigation}
/>
@@ -46,7 +43,7 @@ const History = ({
]}
>
<FlatList
contentContainerStyle={{ flexGrow: 1 }}
contentContainerStyle={{ flexGrow: 1, padding: 8 }}
data={history}
keyExtractor={(item) => item.novelUrl.toString()}
renderItem={renderHistoryCard}
@@ -54,7 +51,7 @@ const History = ({
loading && (
<ActivityIndicator
size="small"
color={theme.colorAccentDark}
color={theme.colorAccent}
/>
)
}
@@ -73,7 +70,7 @@ const History = ({
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
theme: state.settingsReducer.theme,
history: state.historyReducer.history,
loading: state.historyReducer.loading,
});
@@ -86,6 +83,5 @@ export default connect(mapStateToProps, {
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
},
});
@@ -5,24 +5,20 @@ import { TouchableRipple, IconButton } from "react-native-paper";
import moment from "moment";
import { useSelector } from "react-redux";
const HistoryCard = ({ item, deleteHistoryAction, navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
import { useDispatch } from "react-redux";
import { setNovel } from "../../../redux/novel/novel.actions";
const HistoryCard = ({ item, deleteHistoryAction, navigation, theme }) => {
const dispatch = useDispatch();
return (
<TouchableRipple
style={styles.historyCard}
rippleColor={theme.rippleColor}
borderless
onPress={() =>
navigation.navigate("NovelItem", {
novelName: item.novelName,
novelCover: item.novelCover,
extensionId: item.extensionId,
novelUrl: item.novelUrl,
})
}
onPress={() => {
navigation.navigate("Novel", item);
dispatch(setNovel(item));
}}
>
<>
<Image
@@ -74,7 +70,7 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation }) => {
color={theme.textColorPrimary}
style={{ marginRight: 10 }}
onPress={() =>
navigation.navigate("ChapterItem", {
navigation.navigate("Chapter", {
chapterId: item.historyChapterId,
chapterUrl: item.chapterUrl,
extensionId: item.sourceId,
+126
View File
@@ -0,0 +1,126 @@
import React, { useState, useEffect } from "react";
import {
StyleSheet,
Text,
View,
RefreshControl,
ActivityIndicator,
} from "react-native";
import { useSelector, useDispatch } from "react-redux";
import { Searchbar } from "../../Components/Searchbar";
import NovelList from "../../Components/NovelList";
import NovelCover from "../../Components/NovelCover";
import EmptyView from "../../Components/EmptyView";
import {
getLibraryAction,
searchLibraryAction,
} from "../../redux/library/library.actions";
import { updateLibraryAction } from "../../redux/updates/updates.actions";
import { useTheme } from "../../Hooks/reduxHooks";
import { setNovel } from "../../redux/novel/novel.actions";
const LibraryScreen = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { loading, novels } = useSelector((state) => state.libraryReducer);
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
useEffect(() => {
dispatch(getLibraryAction());
}, [getLibraryAction]);
const onRefresh = () => {
setRefreshing(true);
dispatch(updateLibraryAction());
setRefreshing(false);
};
const clearSearchbar = () => {
dispatch(getLibraryAction());
setSearchText("");
};
const onChangeText = (text) => {
setSearchText(text);
dispatch(searchLibraryAction(text));
};
const renderItem = ({ item }) => (
<NovelCover
item={item}
onPress={() => {
navigation.navigate("Novel", item);
dispatch(setNovel(item));
}}
/>
);
const refreshControl = () => (
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["white"]}
progressBackgroundColor={theme.colorAccent}
/>
);
const listEmptyComponent = () =>
searchText !== "" ? (
<Text style={[styles.emptySearch, { color: theme.colorAccent }]}>
{`"${searchText}" not in library`}
</Text>
) : (
<EmptyView
icon="Σ(ಠ_ಠ)"
description="Your library is empty. Add series to your library from Browse."
/>
);
return (
<>
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
placeholder="Search Library"
searchText={searchText}
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
left="magnify"
theme={theme}
/>
{loading ? (
<ActivityIndicator size="small" color={theme.colorAccent} />
) : (
<NovelList
data={novels}
renderItem={renderItem}
refreshControl={refreshControl()}
ListEmptyComponent={listEmptyComponent}
/>
)}
</View>
</>
);
};
export default LibraryScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
},
emptySearch: {
marginTop: 8,
textAlign: "center",
paddingHorizontal: 16,
},
});
@@ -2,13 +2,13 @@ import React from "react";
import { View } from "react-native";
import * as Linking from "expo-linking";
import { Appbar } from "../../components/Appbar";
import { Appbar } from "../../Components/Appbar";
import { useSelector } from "react-redux";
import { Divider, ListItem, ListSection } from "../../components/List";
import { Divider, ListItem, ListSection } from "../../Components/List";
const AboutScreen = ({ navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<>
@@ -17,12 +17,12 @@ const AboutScreen = ({ navigation }) => {
<ListSection>
<ListItem
title="Version"
description="Stable 1.0.13"
description="Stable 1.0.17"
theme={theme}
/>
<ListItem
title="Build Time"
description="24-04-21 21:00 PM"
description="06-05-21 21:00 PM"
theme={theme}
/>
<ListItem
@@ -2,11 +2,11 @@ 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 { Appbar } from "../../Components/Appbar";
import { ListItem, ListSection } from "../../Components/List";
const MoreScreen = ({ navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<>
@@ -37,17 +37,27 @@ const MoreScreen = ({ navigation }) => {
title="Settings"
icon="cog-outline"
onPress={() =>
navigation.navigate("SettingsStack", {
navigation.navigate("MoreStack", {
screen: "Settings",
})
}
theme={theme}
/>
<ListItem
title="Tracking"
icon="sync"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "Trackers",
})
}
theme={theme}
/>
<ListItem
title="About"
icon="information-outline"
onPress={() =>
navigation.navigate("SettingsStack", {
navigation.navigate("MoreStack", {
screen: "About",
})
}
@@ -1,30 +1,32 @@
import React, { useState } from "react";
import { View } from "react-native";
import { connect } from "react-redux";
import { connect, useDispatch } from "react-redux";
import { switchTheme } from "../../redux/theme/theme.actions";
import { switchTheme } from "../../redux/settings/settings.actions";
import {
setDisplayMode,
setItemsPerRow,
} from "../../redux/settings/settings.actions";
import { deleteAllHistory } from "../../database/queries/HistoryQueries";
import { deleteNovelCache } from "../../database/queries/NovelQueries";
import { deleteNovelCache } from "../../Database/queries/NovelQueries";
import { Appbar } from "../../components/Appbar";
import { Appbar } from "../../Components/Appbar";
import {
Divider,
ListItem,
ListSection,
ListSubHeader,
} from "../../components/List";
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,
theme,
themeCode,
displayMode,
itemsPerRow,
switchTheme,
@@ -41,6 +43,10 @@ const SettingsScreen = ({
return label[displayMode];
};
const theme = useTheme();
const dispatch = useDispatch();
// Display Mode Modal
const [displayModalVisible, setDisplayModalVisible] = useState(false);
const showDisplayModal = () => setDisplayModalVisible(true);
@@ -77,7 +83,7 @@ const SettingsScreen = ({
<ListItem
title="Clear history"
description="Delete reading history for all novels"
onPress={() => deleteAllHistory()}
onPress={() => dispatch(clearAllHistoryAction())}
theme={theme}
/>
<Divider theme={theme} />
@@ -102,6 +108,26 @@ const SettingsScreen = ({
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}
@@ -129,7 +155,6 @@ const SettingsScreen = ({
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
displayMode: state.settingsReducer.displayMode,
itemsPerRow: state.settingsReducer.itemsPerRow,
});
+158
View File
@@ -0,0 +1,158 @@
import React, { useState } from "react";
import { View } from "react-native";
import { Modal, Portal, Text, Button, Provider } from "react-native-paper";
import * as WebBrowser from "expo-web-browser";
import {
myAnimeListConfig,
getAccessToken,
malTokenWatcher,
} from "../../Services/Trackers/myAnimeList";
import { Appbar } from "../../Components/Appbar";
import { useSelector, useDispatch } from "react-redux";
import {
Divider,
ListItem,
ListSection,
ListSubHeader,
} from "../../Components/List";
import { removeTracker, setTracker } from "../../redux/tracker/tracker.actions";
const TrackerScreen = ({ navigation }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
const tracker = useSelector((state) => state.trackerReducer.tracker);
const dispatch = useDispatch();
// Tracker Modal
const [visible, setVisible] = useState(false);
const showModal = () => setVisible(true);
const hideModal = () => setVisible(false);
return (
<Provider>
<Appbar title="Tacking" onBackAction={() => navigation.goBack()} />
<View
style={{
flex: 1,
backgroundColor: theme.colorPrimaryDark,
paddingVertical: 8,
}}
>
<ListSection>
<ListSubHeader theme={theme}>Services</ListSubHeader>
<ListItem
title="MyAnimeList"
onPress={async () => {
if (tracker) {
showModal();
} else {
await WebBrowser.openAuthSessionAsync(
myAnimeListConfig.authUrl,
myAnimeListConfig.redirectUri
).then((res) => {
console.log(res.type);
if (res.type === "success") {
const { url } = res;
const codeExtractor = new RegExp(
/=([^&]+)/
);
let code = url.match(codeExtractor);
console.log(code);
if (code) {
code = code[1];
getAccessToken(
code,
myAnimeListConfig.codeChallenge
).then((objects) => {
console.log(objects);
dispatch(setTracker(objects));
});
}
}
});
}
}}
right={tracker && "check"}
theme={theme}
/>
{tracker && tracker.expires_in < new Date(Date.now()) && (
<>
<Divider theme={theme} />
<ListSubHeader theme={theme}>
Settings
</ListSubHeader>
<ListItem
title="Revalidate MyAnimeList"
onPress={() => {
const res = malTokenWatcher(tracker);
dispatch(setTracker(res));
}}
theme={theme}
/>
</>
)}
</ListSection>
<Portal>
<Modal
visible={visible}
onDismiss={hideModal}
contentContainerStyle={{
padding: 20,
margin: 20,
borderRadius: 6,
backgroundColor: theme.colorPrimaryDark,
}}
>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 20,
}}
>
Log out from MyAnimeList?
</Text>
<View
style={{
flexDirection: "row",
justifyContent: "flex-end",
}}
>
<Button
style={{ marginTop: 30 }}
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
onPress={hideModal}
>
Cancel
</Button>
<Button
style={{ marginTop: 30 }}
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
onPress={() => {
dispatch(removeTracker());
hideModal();
}}
>
Logout
</Button>
</View>
</Modal>
</Portal>
</View>
</Provider>
);
};
export default TrackerScreen;
@@ -26,7 +26,7 @@ const DisplayModeModal = ({
}
mode="ios"
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccentDark}
color={theme.colorAccent}
onPress={() => setDisplayMode(mode.displayMode)}
/>
));
@@ -2,6 +2,7 @@ import React from "react";
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";
const GridSizeModal = ({
itemsPerRow,
@@ -10,6 +11,14 @@ const GridSizeModal = ({
setItemsPerRow,
theme,
}) => {
const gridSizes = {
1: "XS",
2: "S",
3: "M",
4: "L",
5: "XL",
};
return (
<Portal>
<Modal
@@ -42,11 +51,24 @@ const GridSizeModal = ({
minimumValue={1}
maximumValue={5}
step={1}
minimumTrackTintColor={theme.colorAccentDark}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccentDark}
thumbTintColor={theme.colorAccent}
onValueChange={(value) => setItemsPerRow(value)}
/>
{/* <RadioButtonGroup
onValueChange={(value) => setItemsPerRow(value)}
value={itemsPerRow}
>
{Object.keys(gridSizes).map((item) => (
<RadioButton
key={item}
value={item}
label={gridSizes[item]}
theme={theme}
/>
))}
</RadioButtonGroup> */}
</Modal>
</Portal>
);
@@ -65,7 +87,7 @@ const styles = StyleSheet.create({
marginBottom: 10,
},
modalDescription: {
marginBottom: 4,
marginBottom: 16,
},
slider: {
width: "100%",
@@ -15,6 +15,7 @@ const ThemeModal = ({
{ id: 1, name: "Light", statusBar: "dark" },
{ id: 2, name: "Dark", statusBar: "light" },
{ id: 3, name: "Midnight Dusk", statusBar: "light" },
{ id: 4, name: "Lime", statusBar: "light" },
];
const renderThemeCheckboxes = () =>
@@ -25,7 +26,7 @@ const ThemeModal = ({
labelStyle={{ color: theme.textColorPrimary }}
status={theme.id === item.id ? "checked" : "unchecked"}
mode="ios"
color={theme.colorAccentDark}
color={theme.colorAccent}
onPress={() => {
switchTheme(item.id);
setStatusBarStyle(item.statusBar);
+146
View File
@@ -0,0 +1,146 @@
import React, { useEffect, useRef, useState } from "react";
import { StyleSheet, View, FlatList, RefreshControl } from "react-native";
import { Provider, Portal } from "react-native-paper";
import {
useContinueReading,
useNovel,
usePreferences,
useTheme,
} from "../../Hooks/reduxHooks";
import ChapterItem from "./components/ChapterItem";
import NovelInfoHeader from "./components/NovelHeader";
import ChaptersSettingsSheet from "./components/ChaptersSettingsSheet";
import { useDispatch } from "react-redux";
import {
getNovelAction,
sortAndFilterChapters,
updateNovelAction,
} from "../../redux/novel/novel.actions";
import TrackSheet from "./components/Tracker/TrackSheet";
import { showToast } from "../../Hooks/showToast";
const Novel = ({ route, navigation }) => {
const item = route.params;
const {
sourceId,
novelUrl,
novelName,
novelCover,
followed,
novelId,
} = item;
const theme = useTheme();
const dispatch = useDispatch();
const { novel, chapters, loading, fetching } = useNovel();
let chaptersSettingsSheetRef = useRef(null);
let trackerSheetRef = useRef(null);
const { sort, filter } = usePreferences(
novelId || (!loading && novel.novelId)
);
let lastRead = useContinueReading(
chapters,
novelId || (!loading && novel.novelId)
);
useEffect(() => {
dispatch(
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter)
);
}, [getNovelAction]);
const onRefresh = () => {
dispatch(updateNovelAction(sourceId, novelUrl, novelId));
showToast(`Updated ${novelName}`);
};
const refreshControl = () => (
<RefreshControl
onRefresh={onRefresh}
refreshing={fetching}
colors={[theme.textColorPrimary]}
progressBackgroundColor={theme.colorPrimary}
/>
);
const ListHeaderComponent = () => (
<NovelInfoHeader
item={item}
novel={novel}
theme={theme}
filter={filter}
loading={loading}
lastRead={lastRead}
dispatch={dispatch}
chapters={chapters}
navigation={navigation}
trackerSheetRef={trackerSheetRef}
chaptersSettingsSheetRef={chaptersSettingsSheetRef}
/>
);
const renderItem = ({ item }) => (
<ChapterItem
theme={theme}
chapter={item}
novelUrl={novelUrl}
dispatch={dispatch}
sourceId={sourceId}
navigation={navigation}
/>
);
return (
<Provider>
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
data={!loading && chapters}
keyExtractor={(item) => item.chapterId.toString()}
removeClippedSubviews={true}
maxToRenderPerBatch={5}
windowSize={15}
initialNumToRender={7}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
refreshControl={refreshControl()}
/>
{!loading && (
<Portal>
<ChaptersSettingsSheet
novelUrl={novelUrl}
bottomSheetRef={chaptersSettingsSheetRef}
dispatch={dispatch}
sortAndFilterChapters={sortAndFilterChapters}
novelId={novelId || novel.novelId}
savedSort={sort}
savedFilter={filter}
/>
<TrackSheet
bottomSheetRef={trackerSheetRef}
novelId={novelId || novel.novelId}
novelName={novelName || novel.novelName}
theme={theme}
/>
</Portal>
)}
</View>
</Provider>
);
};
export default Novel;
const styles = StyleSheet.create({
container: { flex: 1 },
});
@@ -7,19 +7,15 @@ import {
deleteChapterAction,
downloadChapterAction,
} from "../../../redux/novel/novel.actions";
import { connect } from "react-redux";
const ChapterCard = ({
const ChapterItem = ({
novelUrl,
chapter,
extensionId,
sourceId,
dispatch,
theme,
// downloading,
downloadChapterAction,
deleteChapterAction,
navigation,
}) => {
const navigation = useNavigation();
const [visible, setVisible] = useState(false);
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
@@ -28,7 +24,7 @@ const ChapterCard = ({
if (chapter.downloaded === 3) {
return (
<ActivityIndicator
color={theme.textColorHintDark}
color={theme.textColorHint}
size={25}
style={{ margin: 3.5, padding: 5 }}
/>
@@ -52,9 +48,11 @@ const ChapterCard = ({
>
<Menu.Item
onPress={() =>
deleteChapterAction(
chapter.chapterId,
chapter.chapterName
dispatch(
deleteChapterAction(
chapter.chapterId,
chapter.chapterName
)
)
}
title="Delete"
@@ -67,18 +65,19 @@ const ChapterCard = ({
<IconButton
icon="arrow-down-circle-outline"
animated
color={theme.textColorHintDark}
color={theme.textColorHint}
size={25}
onPress={() => {
downloadChapterAction(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId
);
// console.log(downloading);
}}
onPress={() =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId
)
)
}
style={{ margin: 2 }}
/>
);
@@ -89,10 +88,10 @@ const ChapterCard = ({
<TouchableRipple
style={styles.chapterCardContainer}
onPress={() =>
navigation.navigate("ChapterItem", {
navigation.navigate("Chapter", {
chapterId: chapter.chapterId,
chapterUrl: chapter.chapterUrl,
extensionId,
extensionId: sourceId,
novelUrl: novelUrl,
chapterName: chapter.chapterName,
novelId: chapter.novelId,
@@ -108,7 +107,7 @@ const ChapterCard = ({
color: theme.textColorPrimary,
},
chapter.read === 1 && {
color: theme.textColorHintDark,
color: theme.textColorHint,
},
]}
numberOfLines={1}
@@ -124,7 +123,7 @@ const ChapterCard = ({
fontSize: 12,
},
chapter.read === 1 && {
color: theme.textColorHintDark,
color: theme.textColorHint,
},
]}
numberOfLines={1}
@@ -139,15 +138,7 @@ const ChapterCard = ({
);
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
// downloading: state.novelReducer.downloading,
});
export default connect(mapStateToProps, {
downloadChapterAction,
deleteChapterAction,
})(ChapterCard);
export default ChapterItem;
const styles = StyleSheet.create({
chapterCardContainer: {
@@ -1,24 +1,40 @@
import React, { useState } from "react";
import { StyleSheet, View, Text } from "react-native";
import { StyleSheet, View, Text, Animated } from "react-native";
import { List, RadioButton, Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import { useSelector } from "react-redux";
export const BottomSheet = ({
const ChaptersSettingsSheet = ({
bottomSheetRef,
sort,
filter,
setSort,
setFilter,
novelUrl,
novelId,
sortAndFilterChapters,
dispatch,
savedSort,
savedFilter,
}) => {
const [sort, setSort] = useState(savedSort || "ORDER BY chapterId ASC");
const [filter, setFilter] = useState(savedFilter || "");
const sortChapters = (val) => {
setSort(val);
dispatch(sortAndFilterChapters(novelId, val, filter));
};
const filterChapters = (val) => {
setFilter(val);
dispatch(sortAndFilterChapters(novelId, sort, val));
};
const checkBoxStyle = { flexDirection: "row", alignItems: "center" };
const theme = useSelector((state) => state.themeReducer.theme);
const theme = useSelector((state) => state.settingsReducer.theme);
const [animatedValue] = useState(new Animated.Value(0));
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 370, bottom: 0 }}
snappingPoints={[0, 370]}
@@ -42,7 +58,9 @@ export const BottomSheet = ({
</List.Subheader>
<View>
<RadioButton.Group
onValueChange={(newValue) => setSort(newValue)}
onValueChange={(newValue) =>
sortChapters(newValue, filter)
}
value={sort}
>
<View
@@ -58,8 +76,8 @@ export const BottomSheet = ({
}}
>
<RadioButton.Item
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
value="ORDER BY chapterId DESC"
/>
<Text
@@ -77,8 +95,8 @@ export const BottomSheet = ({
}}
>
<RadioButton.Item
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
value="ORDER BY chapterId ASC"
/>
<Text
@@ -105,9 +123,9 @@ export const BottomSheet = ({
<View style={checkBoxStyle}>
<Checkbox.Item
status={filter === "" ? "checked" : "unchecked"}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => setFilter("")}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show all
@@ -118,9 +136,9 @@ export const BottomSheet = ({
status={
filter === "AND `read`=1" ? "checked" : "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => setFilter("AND `read`=1")}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("AND `read`=1")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show read chapters
@@ -131,9 +149,9 @@ export const BottomSheet = ({
status={
filter === "AND `read`=0" ? "checked" : "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => setFilter("AND `read`=0")}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("AND `read`=0")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show unread chapters
@@ -146,9 +164,9 @@ export const BottomSheet = ({
? "checked"
: "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => setFilter("AND downloaded=1")}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("AND downloaded=1")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show downloaded chapters
@@ -159,6 +177,8 @@ export const BottomSheet = ({
);
};
export default ChaptersSettingsSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
@@ -0,0 +1,53 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Chip } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useDispatch } from "react-redux";
import { followNovelAction } from "../../../../redux/novel/novel.actions";
const FollowChip = ({ followed, novel, theme }) => {
const dispatch = useDispatch();
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,
},
});
@@ -0,0 +1,30 @@
import React from "react";
import { Text, StyleSheet } from "react-native";
export const GenreChip = ({ children, theme }) => (
<Text
style={[
styles.genreChip,
{
color: theme.colorAccent,
borderColor: theme.colorAccent,
backgroundColor: theme.colorPrimary,
},
]}
>
{children}
</Text>
);
const styles = StyleSheet.create({
genreChip: {
flex: 1,
justifyContent: "center",
paddingVertical: 2,
paddingHorizontal: 10,
marginHorizontal: 2,
fontSize: 13,
borderWidth: 1,
borderRadius: 50,
},
});
@@ -0,0 +1,36 @@
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,66 @@
import React, { useState } from "react";
import { View, Text, StyleSheet } from "react-native";
const NovelSummary = ({ summary, followed, theme }) => {
const [more, showMore] = useState(!followed);
return (
summary !== "" && (
<View style={styles.summaryContainer}>
<Text
style={[
{ color: theme.textColorPrimary },
styles.summaryHeader,
]}
>
About
</Text>
<Text
style={{
color: theme.textColorSecondary,
lineHeight: 20,
}}
numberOfLines={more ? 200 : 2}
onPress={() => showMore(!more)}
ellipsizeMode="clip"
>
{summary}
</Text>
<Text
style={[
{
color: theme.colorAccent,
backgroundColor: theme.colorPrimaryDark,
},
styles.showMoreButton,
]}
onPress={() => showMore(!more)}
>
{more ? "Less" : "More"}
</Text>
</View>
)
);
};
export default NovelSummary;
const styles = StyleSheet.create({
summaryContainer: {
paddingHorizontal: 16,
marginBottom: 8,
},
summaryHeader: {
marginTop: 5,
paddingVertical: 5,
fontSize: 15,
fontWeight: "bold",
},
showMoreButton: {
fontWeight: "bold",
position: "absolute",
bottom: 0,
right: 18,
paddingLeft: 6,
},
});
@@ -0,0 +1,56 @@
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 (
chapters.length > 0 &&
(lastRead ? (
<Button
color="white"
style={[
{ backgroundColor: theme.colorAccent },
styles.startButton,
]}
uppercase={false}
labelStyle={{ letterSpacing: 0, color: theme.colorButtonText }}
onPress={() => {
navigation.navigate("Chapter", {
chapterId: lastRead.chapterId,
chapterUrl: lastRead.chapterUrl,
novelUrl: novel.novelUrl,
novelId: lastRead.novelId,
extensionId: novel.sourceId,
chapterName: lastRead.chapterName,
});
}}
>
{novel.unread ? `Start reading ` : `Continue reading `}
{lastRead.chapterName}
</Button>
) : (
<Button
color={theme.textColorHint}
style={[
styles.startButton,
{ backgroundColor: theme.colorDisabled },
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
>
All chapters read
</Button>
))
);
};
export default ReadButton;
const styles = StyleSheet.create({
startButton: {
marginTop: 8,
marginBottom: 16,
marginHorizontal: 16,
},
});
@@ -0,0 +1,48 @@
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,
},
});
+280
View File
@@ -0,0 +1,280 @@
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 * as WebBrowser from "expo-web-browser";
import { useSelector, useDispatch } from "react-redux";
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";
import NovelSummary from "./Info/NovelSummary";
import ReadButton from "./Info/ReadButton";
const NovelInfoHeader = ({
item,
novel,
theme,
filter,
loading,
dispatch,
chapters,
lastRead,
navigation,
trackerSheetRef,
chaptersSettingsSheetRef,
}) => {
const [downloadMenu, showDownloadMenu] = useState(false);
const tracker = useSelector((state) => state.trackerReducer.tracker);
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
let isTracked = false;
if (!loading) {
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
theme={theme}
followed={novel.followed}
novel={novel}
/>
{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>
</View>
<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}
/>
<ReadButton
novel={novel}
chapters={chapters}
navigation={navigation}
theme={theme}
lastRead={lastRead}
/>
<TouchableRipple
style={styles.bottomsheet}
onPress={() =>
chaptersSettingsSheetRef.current.show({
velocity: -1.5,
})
}
rippleColor={theme.rippleColor}
>
<>
<Text
style={[
{ color: theme.textColorPrimary },
styles.chapters,
]}
>
{`${chapters.length} Chapters`}
</Text>
<IconButton
icon="filter-variant"
color={
filter
? theme.filterColor
: theme.textColorPrimary
}
size={24}
/>
</>
</TouchableRipple>
</>
)}
</View>
);
};
export default NovelInfoHeader;
const styles = StyleSheet.create({
nameContainer: {
flex: 1,
marginHorizontal: 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,
fontSize: 16,
fontWeight: "bold",
},
bottomsheet: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingRight: 12,
},
genreContainer: {
paddingHorizontal: 15,
paddingBottom: 15,
},
});
@@ -0,0 +1,194 @@
import React from "react";
import { StyleSheet, View, Text, Image } from "react-native";
import { TouchableRipple, IconButton } from "react-native-paper";
import { useDispatch } from "react-redux";
import { untrackNovel } from "../../../../redux/tracker/tracker.actions";
export const AddMalTrackingCard = ({ theme, setTrackSearchDialog }) => (
<View
style={[
styles.addCardContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<Image
source={require("../../../../../assets/mal.png")}
style={styles.trackerIcon}
/>
<TouchableRipple
style={styles.rippleContainer}
borderless
onPress={() => setTrackSearchDialog(true)}
rippleColor={theme.colorAccent}
>
<Text
style={{
flex: 1,
textAlignVertical: "center",
color: theme.colorAccent,
}}
>
Add Tracking
</Text>
</TouchableRipple>
</View>
);
export const MalTrackItemCard = ({
trackItem,
setTrackStatusDialog,
setTrackChaptersDialog,
setTrackScoreDialog,
getStatus,
theme,
}) => {
const dispatch = useDispatch();
return (
<View
style={[
styles.cardContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<View
style={[
styles.titleContainer,
{ borderBottomColor: theme.dividerColor },
]}
>
<Image
source={require("../../../../../assets/mal.png")}
style={[styles.trackerIcon, { borderBottomLeftRadius: 0 }]}
/>
<View style={styles.listItemContainer}>
<Text
style={[
styles.listItem,
{ color: theme.textColorSecondary },
]}
>
{trackItem.title}
</Text>
<IconButton
icon="close"
color={theme.textColorSecondary}
size={21}
onPress={() => dispatch(untrackNovel(trackItem.id))}
/>
</View>
</View>
<View style={{ height: 50, flexDirection: "row" }}>
<TouchableRipple
style={[
styles.listItemLeft,
{ borderRightColor: theme.dividerColor },
]}
borderless
onPress={() => setTrackStatusDialog(true)}
rippleColor={theme.rippleColor}
>
<Text
style={[
styles.listItem,
{ color: theme.textColorSecondary },
]}
>
{getStatus(trackItem.my_list_status.status)}
</Text>
</TouchableRipple>
<TouchableRipple
style={{ flex: 1 }}
borderless
onPress={() => setTrackChaptersDialog(true)}
rippleColor={theme.rippleColor}
>
<Text
style={[
styles.listItem,
{ color: theme.textColorSecondary },
]}
>
{`${trackItem.my_list_status.num_chapters_read}/${
trackItem.num_chapters !== 0
? trackItem.num_chapters
: "-"
}`}
</Text>
</TouchableRipple>
<TouchableRipple
style={[
styles.listItemRight,
{ borderLeftColor: theme.dividerColor },
]}
borderless
onPress={() => setTrackScoreDialog(true)}
rippleColor={theme.rippleColor}
>
<Text
style={[
styles.listItem,
{ color: theme.textColorSecondary },
]}
>
{trackItem.my_list_status.score === 0
? "-"
: trackItem.my_list_status.score}
</Text>
</TouchableRipple>
</View>
</View>
);
};
const styles = StyleSheet.create({
trackerIcon: {
height: 50,
width: 50,
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
rippleContainer: {
flex: 1,
alignItems: "center",
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
addCardContainer: {
flexDirection: "row",
alignItems: "center",
margin: 8,
borderRadius: 4,
elevation: 2,
},
cardContainer: {
margin: 8,
elevation: 2,
borderRadius: 4,
},
titleContainer: {
flexDirection: "row",
alignItems: "center",
borderBottomWidth: 1,
},
listItemContainer: {
flex: 1,
flexDirection: "row",
borderTopRightRadius: 4,
},
listItem: {
flex: 1,
textAlign: "center",
textAlignVertical: "center",
},
listItemLeft: {
flex: 1,
borderRightWidth: 1,
borderBottomLeftRadius: 4,
},
listItemRight: {
flex: 1,
borderLeftWidth: 1,
borderBottomRightRadius: 4,
},
});
@@ -0,0 +1,67 @@
import React, { useState } from "react";
import { StyleSheet, Text } from "react-native";
import { Modal, TextInput } from "react-native-paper";
import {
RadioButton,
RadioButtonGroup,
} from "../../../../Components/RadioButton";
const SetTrackChaptersDialog = ({
trackItem,
trackChaptersDialog,
setTrackChaptersDialog,
updateTrackChapters,
theme,
}) => {
const [trackChapters, setTrackChapters] = useState(
trackItem.my_list_status.num_chapters_read
);
return (
<Modal
visible={trackChaptersDialog}
onDismiss={() => setTrackChaptersDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={[styles.dialogTitle, { color: theme.textColorPrimary }]}
>
Chapters
</Text>
<TextInput
value={trackChapters.toString()}
onChangeText={(text) => setTrackChapters(text ? +text : "")}
onSubmitEditing={() => updateTrackChapters(trackChapters)}
mode="outlined"
keyboardType="numeric"
theme={{
colors: {
primary: theme.colorAccent,
placeholder: theme.textColorHint,
text: theme.textColorPrimary,
background: "transparent",
},
}}
underlineColor={theme.textColorHint}
dense
/>
</Modal>
);
};
export default SetTrackChaptersDialog;
const styles = StyleSheet.create({
containerStyle: {
margin: 30,
padding: 20,
borderRadius: 8,
},
dialogTitle: {
fontSize: 18,
marginBottom: 16,
},
});
@@ -0,0 +1,78 @@
import React from "react";
import { StyleSheet, Text } from "react-native";
import { Modal } from "react-native-paper";
import {
RadioButton,
RadioButtonGroup,
} from "../../../../Components/RadioButton";
const SetTrackScoreDialog = ({
trackItem,
trackScoreDialog,
setTrackScoreDialog,
updateTrackScore,
theme,
}) => {
const getScoreLabel = (score) => {
const myanimeListScores = {
0: "No Score",
1: "Apalling",
2: "Horrible",
3: "Very Bad",
4: "Bad",
5: "Average",
6: "Fine",
7: "Good",
8: "Very Good",
9: "Great",
10: "Masterpiece",
};
return `(${score}) ${myanimeListScores[score]}`;
};
return (
<Modal
visible={trackScoreDialog}
onDismiss={() => setTrackScoreDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
theme={{ colors: { backdrop: "rgba(0,0,0,0.25)" } }}
>
<Text
style={[styles.dialogTitle, { color: theme.textColorPrimary }]}
>
Score
</Text>
<RadioButtonGroup
onValueChange={updateTrackScore}
value={trackItem.my_list_status.score}
>
{[...Array(11).keys()].map((item, index) => (
<RadioButton
key={index}
value={item}
label={getScoreLabel(item)}
theme={theme}
/>
))}
</RadioButtonGroup>
</Modal>
);
};
export default SetTrackScoreDialog;
const styles = StyleSheet.create({
containerStyle: {
margin: 30,
padding: 20,
borderRadius: 8,
},
dialogTitle: {
fontSize: 18,
marginBottom: 16,
},
});
@@ -0,0 +1,67 @@
import React from "react";
import { StyleSheet, Text } from "react-native";
import { Modal } from "react-native-paper";
import {
RadioButton,
RadioButtonGroup,
} from "../../../../Components/RadioButton";
const SetTrackStatusDialog = ({
trackItem,
trackStatusDialog,
setTrackStatusDialog,
updateTrackStatus,
theme,
}) => {
const myAnimeListStatus = {
reading: "Reading",
completed: "Completed",
on_hold: "On Hold",
dropped: "Dropped",
plan_to_read: "Plan to read",
};
return (
<Modal
visible={trackStatusDialog}
onDismiss={() => setTrackStatusDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={[styles.dialogTitle, { color: theme.textColorPrimary }]}
>
Status
</Text>
<RadioButtonGroup
onValueChange={updateTrackStatus}
value={trackItem.my_list_status.status}
>
{Object.keys(myAnimeListStatus).map((key, index) => (
<RadioButton
key={index}
value={key}
label={myAnimeListStatus[key]}
theme={theme}
/>
))}
</RadioButtonGroup>
</Modal>
);
};
export default SetTrackStatusDialog;
const styles = StyleSheet.create({
containerStyle: {
margin: 30,
padding: 20,
borderRadius: 8,
},
dialogTitle: {
fontSize: 18,
marginBottom: 16,
},
});
@@ -0,0 +1,184 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, Text, View, Image, ActivityIndicator } from "react-native";
import { Button, Modal, TextInput, TouchableRipple } from "react-native-paper";
import { searchNovels } from "../../../../Services/Trackers/myAnimeList";
import { useSelector, useDispatch } from "react-redux";
import { ScrollView } from "react-native-gesture-handler";
import { trackNovel } from "../../../../redux/tracker/tracker.actions";
const TrackSearchDialog = ({
trackSearchDialog,
setTrackSearchDialog,
novelId,
novelName,
theme,
}) => {
const [loading, setLoading] = useState(true);
const [searchResults, setSearchResults] = useState([]);
const [searchText, setSearchText] = useState(novelName);
const [selectedNovel, setSelectedNovel] = useState();
const tracker = useSelector((state) => state.trackerReducer.tracker);
const dispatch = useDispatch();
const getSearchresults = async () => {
const res = await searchNovels(searchText, tracker.access_token);
setSearchResults(res);
setLoading(false);
};
useEffect(() => {
trackSearchDialog && getSearchresults();
}, [trackSearchDialog]);
const renderSearchResultCard = (item) => (
<TouchableRipple
style={[
{ flexDirection: "row", borderRadius: 4, margin: 8 },
selectedNovel &&
selectedNovel.id === item.node.id && {
backgroundColor: theme.rippleColor,
},
]}
key={item.node.id}
onPress={() => setSelectedNovel(item.node)}
rippleColor={theme.rippleColor}
borderless
>
<>
<Image
source={{ uri: item.node.main_picture.large }}
style={{ height: 150, width: 100, borderRadius: 4 }}
/>
<Text
style={{
flex: 1,
color: theme.textColorPrimary,
marginLeft: 20,
fontSize: 16,
flexWrap: "wrap",
padding: 8,
paddingLeft: 0,
}}
>
{item.node.title}
</Text>
</>
</TouchableRipple>
);
return (
<Modal
visible={trackSearchDialog}
onDismiss={() => setTrackSearchDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
theme={{ colors: { backdrop: "rgba(0,0,0,0.25)" } }}
>
<TextInput
value={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchresults}
theme={{
colors: {
primary: theme.colorAccent,
text: theme.textColorPrimary,
background: "transparent",
},
}}
underlineColor={theme.textColorHint}
right={
<TextInput.Icon
color={theme.textColorSecondary}
icon="close"
onPress={() => setSearchText("")}
/>
}
/>
<ScrollView
style={{ flexGrow: 1, maxHeight: 500, marginVertical: 8 }}
>
{loading ? (
<ActivityIndicator
color={theme.colorAccent}
size={45}
style={{ margin: 16 }}
/>
) : (
searchResults &&
searchResults.data &&
searchResults.data.map((result) =>
renderSearchResultCard(result)
)
)}
</ScrollView>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
marginTop: 30,
}}
>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => setSelectedNovel(null)}
>
Remove
</Button>
<View style={{ flexDirection: "row" }}>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => setTrackSearchDialog(false)}
>
Cancel
</Button>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => {
if (selectedNovel) {
dispatch(
trackNovel(
{ ...selectedNovel, novelId },
tracker.access_token
)
);
}
setTrackSearchDialog(false);
}}
>
OK
</Button>
</View>
</View>
</Modal>
);
};
export default TrackSearchDialog;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
});
@@ -0,0 +1,180 @@
import React, { useState, useEffect } from "react";
import { StyleSheet, View, ToastAndroid } from "react-native";
import { Portal } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import { useSelector, useDispatch } from "react-redux";
import { updateTracker } from "../../../../redux/tracker/tracker.actions";
import TrackSearchDialog from "./TrackSearchDialog";
import SetTrackStatusDialog from "./SetTrackStatusDialog";
import SetTrackScoreDialog from "./SetTrackScoreDialog";
import SetTrackChaptersDialog from "./SetTrackChaptersDialog";
import { AddMalTrackingCard, MalTrackItemCard } from "./MyAnimeListCards";
const TrackSheet = ({ bottomSheetRef, novelId, novelName, theme }) => {
const tracker = useSelector((state) => state.trackerReducer.tracker);
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
const dispatch = useDispatch();
const [trackItem, setTrackItem] = useState();
useEffect(() => {
setTrackItem(trackedNovels.find((obj) => obj.novelId === novelId));
}, [trackedNovels, novelId]);
const [trackSearchDialog, setTrackSearchDialog] = useState(false);
const [trackStatusDialog, setTrackStatusDialog] = useState(false);
const [trackChaptersDialog, setTrackChaptersDialog] = useState(false);
const [trackScoreDialog, setTrackScoreDialog] = useState(false);
const getStatus = (status) => {
const myAnimeListStatus = {
reading: "Reading",
completed: "Completed",
on_hold: "On Hold",
dropped: "Dropped",
plan_to_read: "Plan to read",
};
return myAnimeListStatus[status];
};
const updateTrackChapters = (newChapters) => {
if (newChapters !== "") {
setTrackItem({
...trackItem,
my_list_status: {
...trackItem.my_list_status,
num_chapters_read: newChapters,
},
});
dispatch(
updateTracker(trackItem.id, tracker.access_token, {
...trackItem.my_list_status,
num_chapters_read: newChapters,
})
);
setTrackChaptersDialog(false);
} else {
ToastAndroid.show("Enter a valid number", ToastAndroid.SHORT);
}
};
const updateTrackStatus = (newStatus) => {
setTrackItem({
...trackItem,
my_list_status: {
...trackItem.my_list_status,
status: newStatus,
},
});
dispatch(
updateTracker(trackItem.id, tracker.access_token, {
...trackItem.my_list_status,
status: newStatus,
})
);
setTrackStatusDialog(false);
};
const updateTrackScore = (newScore) => {
setTrackItem({
...trackItem,
my_list_status: {
...trackItem.my_list_status,
score: newScore,
},
});
dispatch(
updateTracker(trackItem.id, tracker.access_token, {
...trackItem.my_list_status,
score: newScore,
})
);
setTrackScoreDialog(false);
};
return (
<>
<Bottomsheet
ref={bottomSheetRef}
draggableRange={{ top: 120, bottom: 0 }}
snappingPoints={[0, 120]}
backdropOpacity={0.25}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
{!trackItem ? (
<AddMalTrackingCard
theme={theme}
setTrackSearchDialog={setTrackSearchDialog}
/>
) : (
<MalTrackItemCard
trackItem={trackItem}
setTrackStatusDialog={setTrackStatusDialog}
setTrackChaptersDialog={setTrackChaptersDialog}
setTrackScoreDialog={setTrackScoreDialog}
theme={theme}
getStatus={getStatus}
/>
)}
</View>
</Bottomsheet>
<Portal>
{trackItem ? (
<>
<SetTrackStatusDialog
trackItem={trackItem}
trackStatusDialog={trackStatusDialog}
setTrackStatusDialog={setTrackStatusDialog}
updateTrackStatus={updateTrackStatus}
theme={theme}
/>
<SetTrackChaptersDialog
trackItem={trackItem}
trackChaptersDialog={trackChaptersDialog}
setTrackChaptersDialog={setTrackChaptersDialog}
updateTrackChapters={updateTrackChapters}
theme={theme}
/>
<SetTrackScoreDialog
trackItem={trackItem}
trackScoreDialog={trackScoreDialog}
setTrackScoreDialog={setTrackScoreDialog}
updateTrackScore={updateTrackScore}
theme={theme}
/>
</>
) : (
<TrackSearchDialog
trackSearchDialog={trackSearchDialog}
setTrackSearchDialog={setTrackSearchDialog}
novelId={novelId}
novelName={novelName}
theme={theme}
/>
)}
</Portal>
</>
);
};
export default TrackSheet;
const styles = StyleSheet.create({
contentContainer: { flex: 1 },
});
+95
View File
@@ -0,0 +1,95 @@
import React, { useState, useEffect } from "react";
import {
StyleSheet,
View,
FlatList,
ActivityIndicator,
RefreshControl,
} from "react-native";
import { useDispatch, useSelector } from "react-redux";
import EmptyView from "../../Components/EmptyView";
import {
getUpdatesAction,
updateLibraryAction,
} from "../../redux/updates/updates.actions";
import { Appbar } from "./components/Appbar";
import UpdatesItem from "./components/UpdatesItem";
import { useTheme } from "../../Hooks/reduxHooks";
const Updates = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { updates, loading } = useSelector((state) => state.updatesReducer);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
dispatch(getUpdatesAction());
}, [getUpdatesAction, updates]);
const onRefresh = async () => {
setRefreshing(true);
dispatch(updateLibraryAction());
setRefreshing(false);
};
const onPress = (item) =>
navigation.navigate("Chapter", {
chapterId: item.chapterId,
chapterUrl: item.chapterUrl,
extensionId: item.sourceId,
novelUrl: item.novelUrl,
chapterName: item.chapterName,
novelId: item.novelId,
});
const renderItem = ({ item }) => (
<UpdatesItem item={item} theme={theme} onPress={() => onPress(item)} />
);
const ListFooterComponent = () =>
loading && <ActivityIndicator size="small" color={theme.colorAccent} />;
const ListEmptyComponent = () =>
!loading && (
<EmptyView icon="(˘・_・˘)" description="No recent updates" />
);
const refreshControl = () => (
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["white"]}
progressBackgroundColor={theme.colorAccent}
/>
);
return (
<>
<Appbar theme={theme} dispatch={dispatch} />
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
contentContainerStyle={styles.flatList}
data={updates}
keyExtractor={(item) => item.updateId.toString()}
renderItem={renderItem}
ListFooterComponent={ListFooterComponent()}
ListEmptyComponent={ListEmptyComponent()}
refreshControl={refreshControl()}
/>
</View>
</>
);
};
export default Updates;
const styles = StyleSheet.create({
container: { flex: 1 },
flatList: { flexGrow: 1, paddingVertical: 8 },
});
+17
View File
@@ -0,0 +1,17 @@
import React from "react";
import { Appbar as MaterialAppbar } from "react-native-paper";
import { updateLibraryAction } from "../../../redux/updates/updates.actions";
export const Appbar = ({ theme, dispatch }) => (
<MaterialAppbar.Header style={{ backgroundColor: theme.colorPrimary }}>
<MaterialAppbar.Content
title="Updates"
titleStyle={{ color: theme.textColorPrimary }}
/>
<MaterialAppbar.Action
icon="reload"
onPress={() => dispatch(updateLibraryAction())}
/>
</MaterialAppbar.Header>
);
@@ -0,0 +1,65 @@
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 (
<TouchableRipple
style={styles.updateCard}
onPress={onPress}
rippleColor={theme.rippleColor}
borderless
>
<>
<Image
source={{ uri: item.novelCover }}
style={styles.updateIcon}
/>
<View style={styles.chapterDetails}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={{ flexDirection: "row" }}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 12,
}}
>
{`${item.chapterName} · ${moment(
item.updateTime
).calendar()}`}
</Text>
</View>
</View>
</>
</TouchableRipple>
);
};
export default UpdateCard;
const styles = StyleSheet.create({
updateCard: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 12,
},
updateIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
chapterDetails: {
flex: 1,
marginLeft: 16,
},
});
@@ -1,5 +1,5 @@
export const fetchSources = async () => {
const url = `https://lnreader-extensions.herokuapp.com/api/`;
const url = `https://lnreader-extensions.vercel.app/api/`;
let res = await fetch(url);
let sources = await res.json();
@@ -8,7 +8,7 @@ export const fetchSources = async () => {
};
export const fetchNovel = async (sourceId, novelUrl) => {
const url = `https://lnreader-extensions.herokuapp.com/api/${sourceId}/novel/${novelUrl}`;
const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}`;
let data = await fetch(url);
let res = await data.json();
@@ -33,10 +33,21 @@ export const fetchNovel = async (sourceId, novelUrl) => {
};
export const fetchChapter = async (sourceId, novelUrl, chapterUrl) => {
const url = `https://lnreader-extensions.herokuapp.com/api/${sourceId}/novel/${novelUrl}${chapterUrl}`;
const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}${chapterUrl}`;
let res = await fetch(url);
let chapter = await res.json();
return chapter;
};
export const fetchChapters = async (sourceId, novelUrl) => {
const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}`;
let data = await fetch(url);
let res = await data.json();
const chapters = res.novelChapters;
return chapters;
};
+171
View File
@@ -0,0 +1,171 @@
import * as Linking from "expo-linking";
import qs from "qs";
const clientId = "d2f499825d64b0213bb77e78193ccbb1";
const baseOAuthUrl = "https://myanimelist.net/v1/oauth2/authorize";
const baseApiUrl = "https://api.myanimelist.net/v2";
const pkceChallenger = () => {
const MAX_LENGTH = 88;
let code = "";
const codes =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const randomPicker = () => Math.floor(Math.random() * codes.length);
for (let index = 0; index < MAX_LENGTH; index++) {
code += codes.charAt(randomPicker());
}
return code;
};
const codeChallenge = pkceChallenger();
const redirectUri = Linking.makeUrl();
export const authUrl = `${baseOAuthUrl}?response_type=code&client_id=${clientId}&code_challenge_method=plain&code_challenge=${codeChallenge}`;
const tokenUrl = `https://myanimelist.net/v1/oauth2/token`;
export const myAnimeListConfig = {
clientId,
redirectUri,
authUrl,
codeChallenge,
tokenUrl,
};
export const getAccessToken = async (code, codeChallenge) => {
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
};
const body = {
client_id: myAnimeListConfig.clientId,
grant_type: "authorization_code",
code,
code_verifier: codeChallenge,
};
const response = await fetch(myAnimeListConfig.tokenUrl, {
headers,
method: "POST",
body: qs.stringify(body),
});
let tokenResponse = await response.json();
tokenResponse = {
...tokenResponse,
expires_in: new Date(Date.now() + tokenResponse.expires_in),
};
console.log(tokenResponse);
return tokenResponse;
};
export const searchNovels = async (novelName, bearerToken) => {
const searchrl = `${baseApiUrl}/manga?q=${novelName}`;
const headers = {
Authorization: "Bearer " + bearerToken,
"Content-Type": "application/x-www-form-urlencoded",
};
const res = await fetch(searchrl, { method: "GET", headers });
const searchResults = await res.json();
return searchResults;
};
export const findListItem = async (malId, bearerToken) => {
const url = `${baseApiUrl}/manga/${malId}?fields=id,num_chapters,my_list_status{start_date,finish_date}`;
const headers = {
Authorization: "Bearer " + bearerToken,
"Content-Type": "application/x-www-form-urlencoded",
};
const res = await fetch(url, { method: "GET", headers });
const listItem = await res.json();
if (!listItem.my_list_status) {
listItem.my_list_status = {
status: "reading",
score: 0,
num_chapters_read: 0,
};
}
return listItem;
};
export const updateItem = async (malId, bearerToken, body) => {
const url = `${baseApiUrl}/manga/${malId}/my_list_status`;
const headers = {
Authorization: "Bearer " + bearerToken,
"Content-Type": "application/x-www-form-urlencoded",
};
const res = await fetch(url, {
method: "PUT",
headers,
body: qs.stringify(body),
});
const listItem = await res.json();
console.log(listItem);
return listItem;
};
export const updateMalChaptersRead = async (malId, bearerToken, body) => {
const url = `${baseApiUrl}/manga/${malId}/my_list_status`;
const headers = {
Authorization: "Bearer " + bearerToken,
"Content-Type": "application/x-www-form-urlencoded",
};
const res = await fetch(url, {
method: "PUT",
headers,
body: qs.stringify(body),
});
const listItem = await res.json();
console.log(listItem.num_chapters_read);
return listItem.num_chapters_read;
};
const revalidateMalToken = async (refreshToken) => {
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
};
const body = {
grant_type: "refresh_token",
refresh_token: refreshToken,
};
const res = await fetch(url, {
headers,
method: "POST",
body: qs.stringify(body),
});
let tokenResponse = res.json();
return tokenResponse;
};
export const malTokenWatcher = async (tracker) => {
if (tracker) {
const date = new Date(Date.now());
if (tracker.expires_in < date) {
return await revalidateMalToken(tracker.refresh_token);
}
}
};
+44
View File
@@ -0,0 +1,44 @@
import * as DocumentPicker from "expo-document-picker";
import { StorageAccessFramework } from "expo-file-system";
import { getLibrary } from "../Database/queries/LibraryQueries";
import { restoreLibrary } from "../Database/queries/NovelQueries";
import { showToast } from "../Hooks/showToast";
export const createBackup = async () => {
const novels = await getLibrary();
const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
if (!permissions.granted) {
return;
}
const uri = permissions.directoryUri;
if (uri) {
const fileUri = await StorageAccessFramework.createFileAsync(
uri,
"backup",
"application/json"
);
await StorageAccessFramework.writeAsStringAsync(
fileUri,
JSON.stringify(novels)
);
showToast("Backup created");
}
};
export const restoreBackup = async () => {
const backup = await DocumentPicker.getDocumentAsync();
if (backup.uri) {
let novels = await StorageAccessFramework.readAsStringAsync(backup.uri);
novels = await JSON.parse(novels);
await novels.map((novel) => restoreLibrary(novel));
showToast("Restored backup");
}
};
+145
View File
@@ -0,0 +1,145 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import { getLibrary } from "../Database/queries/LibraryQueries";
import { fetchChapters, fetchNovel } from "../Services/Source/source";
import * as Notifications from "expo-notifications";
Notifications.setNotificationHandler({
handleNotification: async () => {
return {
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
});
const updateNovelDetails = async (novel, novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET novelName = ?, novelCover = ?, novelSummary = ?, author = ?, artist = ?, genre = ?, status = ? WHERE novelId = ?",
[
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.genre,
novel.status,
novelId,
],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
export const updateNovel = async (extensionId, novelUrl, novelId) => {
let novel = await fetchNovel(extensionId, novelUrl);
await updateNovelDetails(novel, novelId);
db.transaction((tx) => {
novel.chapters.map((chapter) =>
tx.executeSql(
"INSERT OR IGNORE INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)",
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
novelId,
],
(txObj, { insertId }) => {
if (insertId !== -1) {
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
(txObj, res) => {
console.log(
"Inserted Chapter Id -> " +
insertId +
" Novel Id " +
novelId
);
},
(txObj, error) => console.log("Error ", error)
);
}
},
(txObj, error) => console.log("Error ", error)
)
);
});
};
export const updateNovelChapters = async (extensionId, novelUrl, novelId) => {
let chapters = await fetchChapters(extensionId, novelUrl);
db.transaction((tx) => {
chapters.map((chapter) =>
tx.executeSql(
"INSERT OR IGNORE INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)",
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
novelId,
],
(txObj, { insertId }) => {
if (insertId !== -1) {
console.log(
"Inserted Chapter " +
chapter.chapterUrl +
" Chapter Name " +
chapter.chapterName +
" Novel Id " +
novelId
);
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
}
},
(txObj, error) => console.log("Error ", error)
)
);
});
};
export const updateAllNovels = async () => {
const libraryNovels = await getLibrary();
libraryNovels.map((novel, index) =>
setTimeout(async () => {
updateNovelChapters(novel.sourceId, novel.novelUrl, novel.novelId);
console.log(novel.novelName + " Updated");
if (index + 1 === libraryNovels.length) {
Notifications.scheduleNotificationAsync({
content: { title: "Library Updated" },
trigger: null,
});
}
}, 3000 * index)
);
};
export const parseChapterNumber = (chapterName) => {
chapterName = chapterName.toLowerCase();
chapterName = chapterName.replace(/volume (\d+)/, "");
const basic = chapterName.match(/ch (\d+)/);
const occurrence = chapterName.match(/\d+/);
if (basic) {
return basic[0];
} else if (occurrence) {
return occurrence[0];
} else {
return 0;
}
};
+13
View File
@@ -0,0 +1,13 @@
/**
* Reader fonts
*/
export const fonts = {
"pt-sans-bold": require("../../assets/fonts/PTSansNarrow-Bold.ttf"),
"arbutus-slab": require("../../assets/fonts/ArbutusSlab-Regular.ttf"),
domine: require("../../assets/fonts/Domine-VariableFont_wght.ttf"),
lato: require("../../assets/fonts/Lato-Regular.ttf"),
"noto-sans": require("../../assets/fonts/NotoSans-Regular.ttf"),
"open-sans": require("../../assets/fonts/OpenSans-Regular.ttf"),
"pt-serif": require("../../assets/fonts/PTSerif-Regular.ttf"),
};
+41 -10
View File
@@ -4,10 +4,10 @@
export const theme = {
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccentDark: "#3399FF",
colorAccent: "#3399FF",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
};
@@ -19,14 +19,17 @@ export const lightTheme = {
name: "Light",
colorPrimary: "#FFFFFF",
colorPrimaryDark: "#FAFAFA",
colorAccentDark: "#2979FF",
colorAccent: "#2979FF",
textColorPrimary: "#000000",
textColorSecondary: "rgba(0,0,0,0.54)",
textColorHintDark: "rgba(0,0,0,0.38)",
textColorHint: "rgba(0,0,0,0.38)",
rippleColor: "rgba(0,0,0,0.12)",
searchBarColor: "#FFFFFF",
menuColor: "#FFFFFF",
dividerColor: "rgba(0,0,0,0.1)",
filterColor: "#FFC107",
colorDisabled: "rgba(220,220,220,255)",
colorButtonText: "#000000",
statusBar: "dark",
};
@@ -38,14 +41,17 @@ export const darkTheme = {
name: "Dark",
colorPrimary: "#242529",
colorPrimaryDark: "#202125",
colorAccentDark: "#3399FF",
colorAccent: "#3399FF",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#303135",
menuColor: "#242529",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
colorButtonText: "#FFFFFF",
statusBar: "light",
};
@@ -57,14 +63,17 @@ export const amoledDarkTheme = {
name: "AMOLED Dark",
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccentDark: "#3399FF",
colorAccent: "#3399FF",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#1F1F1F",
menuColor: "#242529",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.15)",
colorDisabled: "rgba(33,33,33,255)",
colorButtonText: "#FFFFFF",
statusBar: "light",
};
@@ -76,13 +85,35 @@ export const midnightDuskTheme = {
name: "Midnight Dusk",
colorPrimary: "#201F27",
colorPrimaryDark: "#16151D",
colorAccentDark: "#F02475",
colorAccent: "#F02475",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#201F27",
menuColor: "#201F27",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
colorButtonText: "#FFFFFF",
statusBar: "light",
};
export const limeTheme = {
id: 4,
name: "Lime",
colorPrimary: "#201F27",
colorPrimaryDark: "#16151D",
colorAccent: "#4AF88A",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#201F27",
menuColor: "#201F27",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
colorButtonText: "#16151D",
statusBar: "light",
};
-212
View File
@@ -1,212 +0,0 @@
import React from "react";
import { StyleSheet, View, Text, ImageBackground, Image } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { TouchableRipple } from "react-native-paper";
import { useSelector } from "react-redux";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const displayMode = useSelector(
(state) => state.settingsReducer.displayMode
);
const theme = useSelector((state) => state.themeReducer.theme);
const itemsPerRow = useSelector(
(state) => state.settingsReducer.itemsPerRow
);
const height = {
1: 550,
2: 280,
3: 180,
4: 140,
5: 100,
6: 80,
7: 70,
8: 58,
9: 55,
10: 50,
};
return displayMode !== 2 ? (
<View style={{ flex: 1 / itemsPerRow }}>
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.opac}
onPress={onPress}
>
<>
{item.novelCover && (
<ImageBackground
source={{ uri: item.novelCover }}
style={{ height: height[itemsPerRow] }}
imageStyle={[
{ borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
]}
progressiveRenderingEnabled={true}
>
<View style={{ flexDirection: "row", margin: 4 }}>
{item.chaptersDownloaded && (
<Text
style={[
{
flex: 0,
backgroundColor: "#47a84a",
color: "#FFFFFF",
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
alignSelf: "flex-start",
fontSize: 12,
},
!item.chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
]}
>
{item.chaptersDownloaded}
</Text>
)}
{item.chaptersUnread && (
<Text
style={[
{
flex: 0,
backgroundColor: "#2979FF",
color: "#FFFFFF",
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
alignSelf: "flex-start",
fontSize: 12,
},
!item.chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
]}
>
{item.chaptersUnread}
</Text>
)}
</View>
{displayMode === 0 && (
<View style={styles.titleContainer}>
<LinearGradient
colors={[
"transparent",
"rgba(0,0,0,0.6)",
]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[
styles.title,
{
color:
"rgba(255,255,255,1)",
},
]}
>
{item.novelName}
</Text>
</LinearGradient>
</View>
)}
</ImageBackground>
)}
{displayMode === 1 && (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{item.novelName}
</Text>
)}
</>
</TouchableRipple>
</View>
) : (
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.listView}
onPress={onPress}
>
<>
<Image
source={{ uri: item.novelCover }}
style={styles.extensionIcon}
/>
<Text
style={{
flex: 1,
color: theme.textColorPrimary,
marginLeft: 16,
fontSize: 15,
paddingRight: 8,
}}
numberOfLines={1}
>
{item.novelName}
</Text>
</>
</TouchableRipple>
);
};
export default NovelCover;
const styles = StyleSheet.create({
// logo: {
// height: 180,
// },
titleContainer: {
flex: 1,
justifyContent: "flex-end",
borderRadius: 4,
},
title: {
fontFamily: "pt-sans-bold",
fontSize: 14,
padding: 8,
},
linearGradient: {
bottom: -1,
borderRadius: 4,
},
opac: {
paddingHorizontal: 4.5,
paddingVertical: 4,
borderRadius: 4,
},
extensionIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
listView: {
flex: 1,
paddingVertical: 8,
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
borderRadius: 4,
},
});
-160
View File
@@ -1,160 +0,0 @@
import React, { useEffect } from "react";
import {
createStackNavigator,
TransitionPresets,
} from "@react-navigation/stack";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import { Provider } from "react-native-paper";
import { setStatusBarStyle } from "expo-status-bar";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import Library from "../screens/Library/Library";
import Updates from "../screens/Updates/Updates";
import History from "../screens/History/History";
import Browse from "../screens/Browse/Browse";
import More from "../screens/More/More";
import About from "../screens/More/About";
import Settings from "../screens/More/Settings";
import NovelItem from "../screens/Novel/Novel";
import ChapterItem from "../screens/Chapter/Chapter";
import Extension from "../screens/Extension/Extension";
const Stack = createStackNavigator();
const Tab = createMaterialBottomTabNavigator();
const stackNavigatorConfig = {
headerShown: false,
...TransitionPresets.RevealFromBottomAndroid,
};
const SettingsStack = () => {
return (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="About" component={About} />
<Stack.Screen name="Settings" component={Settings} />
</Stack.Navigator>
);
};
import { useSelector } from "react-redux";
const BottomNavigator = () => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<Tab.Navigator
shifting={false}
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccentDark}
>
<Tab.Screen
name="My 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>
);
};
const Router = () => {
const theme = useSelector((state) => state.themeReducer.theme);
useEffect(() => setStatusBarStyle(theme.id === 1 ? "dark" : "light"), []);
return (
<Provider>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Router" component={BottomNavigator} />
<Stack.Screen
name="ChapterItem"
component={ChapterItem}
options={{ ...TransitionPresets.SlideFromRightIOS }}
/>
<Stack.Screen
name="NovelItem"
component={NovelItem}
options={{
headerTitle: "",
headerShown: true,
headerTransparent: true,
headerTintColor: "white",
}}
/>
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="Extension" component={Extension} />
{/* <Stack.Screen
name="ExtensionStack"
component={ExtensionStack}
/> */}
</Stack.Navigator>
</Provider>
);
};
export default Router;
+23 -11
View File
@@ -9,7 +9,7 @@ import {
CHAPTER_DELETED,
} from "./chapter.types";
import { fetchChapter } from "../../source/Source";
import { fetchChapter } from "../../Services/Source/source";
import {
markChapterRead,
@@ -17,7 +17,8 @@ import {
getChapter,
downloadChapter,
deleteChapter,
} from "../../database/queries/ChapterQueries";
} from "../../Database/queries/ChapterQueries";
import { SET_LAST_READ } from "../preferences/preference.types";
export const getChapterAction = (
extensionId,
@@ -27,22 +28,33 @@ export const getChapterAction = (
) => async (dispatch) => {
dispatch({ type: CHAPTER_LOADING });
const isDownloaded = await isChapterDownloaded(novelId);
let chapter;
let chapter = await getChapter(novelId);
if (isDownloaded) {
chapter = await getChapter(novelId);
} else {
if (!chapter) {
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
}
console.log(isDownloaded);
dispatch({ type: GET_CHAPTER, payload: chapter });
};
export const markChapterReadAction = (chapterId) => async (dispatch) => {
await markChapterRead(chapterId);
dispatch({ type: CHAPTER_READ, payload: { chapterId } });
};
// export const markChapterReadAction = (chapterId, novelId) => async (
// dispatch
// ) => {
// await markChapterRead(chapterId);
// console.log("DIspatchedbsjs");
// dispatch({
// type: CHAPTER_READ,
// payload: { chapterId },
// });
// dispatch({
// type: SET_LAST_READ,
// payload: { novelId, chapterId: chapterId + 1 },
// });
// console.log("Set Last Read Diaptched", chapterId);
// };
export const downloadChapterAction = (
extensionId,
+19 -1
View File
@@ -3,12 +3,15 @@ import {
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
CLEAR_HISTORY,
} from "./history.types";
import {
getHistory,
insertHistory,
deleteHistory,
} from "../../database/queries/HistoryQueries";
deleteAllHistory,
} from "../../Database/queries/HistoryQueries";
import { SET_LAST_READ } from "../preferences/preference.types";
export const getHistoryAction = () => async (dispatch) => {
dispatch({ type: LOAD_HISTORY });
@@ -24,6 +27,15 @@ export const insertHistoryAction = (novelId, chapterId) => async (dispatch) => {
dispatch({
type: UPDATE_NOVEL_HISTORY,
});
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId },
});
const history = await getHistory();
dispatch({ type: GET_HISTORY, payload: history });
};
export const deleteHistoryAction = (novelId) => async (dispatch) => {
@@ -34,3 +46,9 @@ export const deleteHistoryAction = (novelId) => async (dispatch) => {
payload: { novelId },
});
};
export const clearAllHistoryAction = () => async (dispatch) => {
deleteAllHistory();
dispatch({ type: CLEAR_HISTORY });
};
+6 -6
View File
@@ -1,4 +1,5 @@
import {
CLEAR_HISTORY,
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
@@ -27,12 +28,11 @@ const historyReducer = (state = initialState, action) => {
(item) => item.novelId !== payload.novelId
),
};
// return {
// ...state,
// history: state.history.map((novel) =>
// novel.novelUrl === payload.novelUrl ? payload : novel
// ),
// };
case CLEAR_HISTORY:
return {
...state,
history: [],
};
default:
return state;
}
+1
View File
@@ -2,3 +2,4 @@ export const LOAD_HISTORY = "LOAD_HISTORY";
export const GET_HISTORY = "GET_HISTORY";
export const UPDATE_NOVEL_HISTORY = "UPDATE_NOVEL_HISTORY";
export const CLEAR_NOVEL_HISTORY = "CLEAR_NOVEL_HISTORY";
export const CLEAR_HISTORY = "CLEAR_HISTORY";
+13 -1
View File
@@ -5,7 +5,8 @@ import {
import {
getLibrary,
searchLibrary,
} from "../../database/queries/LibraryQueries";
} from "../../Database/queries/LibraryQueries";
import { restoreBackup } from "../../Services/backup";
export const getLibraryAction = () => async (dispatch) => {
const res = await getLibrary();
@@ -24,3 +25,14 @@ export const searchLibraryAction = (searchText) => async (dispatch) => {
payload: res,
});
};
export const restoreLibraryAction = () => async (dispatch) => {
await restoreBackup();
const res = await getLibrary();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
+8 -11
View File
@@ -1,4 +1,8 @@
import { FOLLOW_NOVEL, UNFOLLOW_NOVEL } from "../novel/novel.types";
import {
FOLLOW_NOVEL,
UNFOLLOW_NOVEL,
UPDATE_IN_LIBRARY,
} from "../novel/novel.types";
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
@@ -17,18 +21,11 @@ const libraryReducer = (state = initialState, action) => {
case GET_LIBRARY_NOVELS:
return { ...state, novels: payload, loading: false };
case GET_LIBRARY_SEARCH_RESULTS:
return { ...state, searchResults: payload, loading: false };
case FOLLOW_NOVEL:
return { ...state, novels: payload, loading: false };
case UPDATE_IN_LIBRARY:
return {
...state,
novels: [...state.novels, { ...payload, followed: 1 }],
};
case UNFOLLOW_NOVEL:
return {
...state,
novels: state.novels.filter(
(novel) => novel.novelId !== payload
),
novels: [...state.novels, { novelUrl: payload.novelUrl }],
};
default:
return state;
+48 -26
View File
@@ -14,15 +14,16 @@ import {
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
UPDATE_NOVEL,
SET_NOVEL_SETTINGS,
} from "./novel.types";
import { updateNovel } from "../../services/updates";
import { fetchChapter, fetchNovel } from "../../source/Source";
import { updateNovel } from "../../Services/updates";
import { fetchChapter, fetchNovel } from "../../Services/Source/source";
import {
followNovel,
insertNovel,
getNovel,
} from "../../database/queries/NovelQueries";
} from "../../Database/queries/NovelQueries";
import {
getChapters,
insertChapters,
@@ -31,7 +32,14 @@ import {
getChapter,
downloadChapter,
deleteChapter,
} from "../../database/queries/ChapterQueries";
} from "../../Database/queries/ChapterQueries";
import { deleteNovelUpdates } from "../../Database/queries/UpdateQueries";
import {
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";
export const setNovel = (novel) => async (dispatch) => {
dispatch({ type: SET_NOVEL, payload: novel });
@@ -57,7 +65,7 @@ export const getNovelAction = (
} else {
dispatch({ type: FETCHING_NOVEL });
const novel = await getNovel(novelUrl);
const novel = await getNovel(sourceId, novelUrl);
if (novel) {
novel.chapters = await getChapters(novel.novelId, sort, filter);
@@ -66,21 +74,12 @@ export const getNovelAction = (
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(novelUrl);
const novel = await getNovel(sourceId, novelUrl);
novel.chapters = await getChapters(novel.novelId);
dispatch({
@@ -91,22 +90,39 @@ export const getNovelAction = (
}
};
export const sortAndFilterChapters = (novelUrl, filter, sort) => async (
export const sortAndFilterChapters = (novelId, sort, filter) => async (
dispatch
) => {
// const chapters = await getChaptersFromDb(novelUrl, filter, sort);
// dispatch({
// type: GET_CHAPTERS,
// payload: chapters,
// });
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
dispatch({
type: SET_CHAPTER_LIST_PREF,
payload: { novelId, sort, filter },
});
};
export const followNovelAction = (novel) => async (dispatch) => {
await followNovel(novel.followed, novel.novelId);
if (!novel.followNovel) {
deleteNovelUpdates(novel.novelId);
}
dispatch({
type: UPDATE_IN_LIBRARY,
payload: !novel.followed,
payload: { novelUrl: novel.novelUrl, followed: !novel.followed },
});
const res = await getLibrary();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
ToastAndroid.show(
@@ -134,9 +150,15 @@ export const getChapterAction = (
dispatch({ type: GET_CHAPTER, payload: chapter });
};
export const markChapterReadAction = (chapterId) => async (dispatch) => {
export const markChapterReadAction = (chapterId, novelId) => async (
dispatch
) => {
await markChapterRead(chapterId);
dispatch({ type: CHAPTER_READ, payload: { chapterId } });
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId: chapterId + 1 },
});
};
export const downloadChapterAction = (
@@ -218,14 +240,14 @@ export const deleteAllChaptersAction = (chapters) => async (dispatch) => {
ToastAndroid.show(`Deleted all chapters`, ToastAndroid.SHORT);
};
export const updateNovelAction = (extensionId, novelUrl, novelId) => async (
export const updateNovelAction = (sourceId, novelUrl, novelId) => async (
dispatch
) => {
dispatch({ type: FETCHING_NOVEL });
await updateNovel(extensionId, novelUrl, novelId);
await updateNovel(sourceId, novelUrl, novelId);
let novel = await getNovel(novelUrl);
let novel = await getNovel(sourceId, novelUrl);
let chapters = await getChapters(novel.novelId);
dispatch({
+1 -3
View File
@@ -53,7 +53,7 @@ const novelReducer = (state = initialState, action) => {
case UPDATE_IN_LIBRARY:
return {
...state,
novel: { ...state.novel, followed: payload },
novel: { ...state.novel, followed: payload.followed },
};
case UPDATE_NOVEL:
return {
@@ -91,7 +91,6 @@ const novelReducer = (state = initialState, action) => {
case CHAPTER_DOWNLOADED:
return {
...state,
// downloading: state.downloading.filter((id) => id === payload),
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload
? { ...chapter, downloaded: 1 }
@@ -107,7 +106,6 @@ const novelReducer = (state = initialState, action) => {
: chapter
),
};
case UPDATE_LAST_READ:
return {
...state,
+2
View File
@@ -17,3 +17,5 @@ export const GET_CHAPTER = "GET_CHAPTER";
export const FOLLOW_NOVEL = "FOLLOW_NOVEL";
export const UNFOLLOW_NOVEL = "UNFOLLOW_NOVEL";
export const SET_NOVEL_SETTINGS = "SET_NOVEL_SETTINGS";
@@ -0,0 +1,58 @@
import { SET_CHAPTER_LIST_PREF, SET_LAST_READ } from "./preference.types";
const initialState = {
novelSettings: [],
};
const novelReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case SET_LAST_READ:
return {
...state,
novelSettings: !state.novelSettings.some(
(novel) => novel.novelId === payload.novelId
)
? [
{
novelId: payload.novelId,
lastRead: payload.chapterId,
},
]
: state.novelSettings.map((novel) =>
novel.novelId === payload.novelId
? { ...novel, lastRead: payload.chapterId }
: novel
),
};
case SET_CHAPTER_LIST_PREF:
return {
...state,
novelSettings: state.novelSettings.some(
(novel) => novel.novelId === payload.novelId
)
? state.novelSettings.map((novel) =>
novel.novelId === payload.novelId
? {
...novel,
sort: payload.sort,
filter: payload.filter,
}
: novel
)
: [
...state.novelSettings,
{
novelId: payload.novelId,
sort: payload.sort,
filter: payload.filter,
},
],
};
default:
return state;
}
};
export default novelReducer;
@@ -0,0 +1,2 @@
export const SET_CHAPTER_LIST_PREF = "SET_CHAPTER_LIST_PREF";
export const SET_LAST_READ = "SET_LAST_READ";
+8
View File
@@ -1,4 +1,5 @@
import {
SWITCH_THEME,
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
UPDATE_READER_TEXT_SIZE,
@@ -8,6 +9,13 @@ import {
SET_READER_FONT,
} from "./settings.types";
export const switchTheme = (val) => (dispatch) => {
dispatch({
type: SWITCH_THEME,
payload: val,
});
};
export const setDisplayMode = (val) => (dispatch) => {
dispatch({
type: SET_DISPLAY_MODE,
+22 -1
View File
@@ -1,4 +1,6 @@
import { SET_NOVEL_SETTINGS } from "../novel/novel.types";
import {
SWITCH_THEME,
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
SET_READER_FONT,
@@ -7,6 +9,21 @@ import {
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
} from "./settings.types";
import {
amoledDarkTheme,
lightTheme,
darkTheme,
midnightDuskTheme,
limeTheme,
} from "../../Theme/theme";
const themes = {
0: amoledDarkTheme,
1: lightTheme,
2: darkTheme,
3: midnightDuskTheme,
4: limeTheme,
};
const initialState = {
displayMode: 0,
@@ -18,6 +35,7 @@ const initialState = {
padding: 5,
fontFamily: null,
},
theme: themes[2],
};
/**
@@ -39,11 +57,12 @@ const settingsReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case SWITCH_THEME:
return { ...state, theme: themes[payload] };
case SET_DISPLAY_MODE:
return {
...state,
displayMode: payload,
itemsPerRow: payload === 2 ? 1 : state.itemsPerRow,
};
case SET_ITEMS_PER_ROW:
return {
@@ -69,6 +88,8 @@ const settingsReducer = (state = initialState, action) => {
...state,
reader: { ...state.reader, fontFamily: payload },
};
case SET_NOVEL_SETTINGS:
console.log(state.novelSettings);
default:
return state;
}
+1
View File
@@ -1,3 +1,4 @@
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";
+16 -2
View File
@@ -1,5 +1,5 @@
import { GET_SOURCES } from "./source.types";
import { fetchSources } from "../../source/Source";
import { GET_SOURCES, PIN_SOURCES, SEARCH_SOURCES } from "./source.types";
import { fetchSources } from "../../Services/Source/source";
export const getSourcesAction = () => async (dispatch) => {
const res = await fetchSources();
@@ -9,3 +9,17 @@ export const getSourcesAction = () => async (dispatch) => {
payload: res,
});
};
export const searchSourcesAction = (searchText) => async (dispatch) => {
dispatch({
type: SEARCH_SOURCES,
payload: searchText,
});
};
export const pinSourceAction = (sourceId) => async (dispatch) => {
dispatch({
type: PIN_SOURCES,
payload: sourceId,
});
};
+24 -5
View File
@@ -1,19 +1,38 @@
import { GET_SOURCES } from "./source.types";
import { GET_SOURCES, SEARCH_SOURCES, PIN_SOURCES } from "./source.types";
const initialState = {
extensions: [],
sources: [],
pinnedSources: [],
search: [],
pinned: [],
loading: true,
};
const libraryReducer = (state = initialState, action) => {
const sourceReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_SOURCES:
return { extensions: payload, loading: false };
return { ...state, sources: payload, loading: false };
case PIN_SOURCES:
return {
...state,
pinned:
state.pinned.indexOf(payload) === -1
? [...state.pinned, payload]
: state.pinned.filter((source) => source !== payload),
loading: false,
};
case SEARCH_SOURCES:
return {
...state,
search: state.sources.filter((source) =>
source.sourceName.includes(payload)
),
};
default:
return state;
}
};
export default libraryReducer;
export default sourceReducer;
+2
View File
@@ -1 +1,3 @@
export const GET_SOURCES = "GET_SOURCES";
export const SEARCH_SOURCES = "SEARCH_SOURCES";
export const PIN_SOURCES = "PIN_SOURCES";
+6 -4
View File
@@ -3,13 +3,14 @@ import { persistStore, persistReducer } from "redux-persist";
import AsyncStorage from "@react-native-async-storage/async-storage";
import thunk from "redux-thunk";
import themeReducer from "./theme/theme.reducer";
import settingsReducer from "./settings/settings.reducer";
import libraryReducer from "./library/library.reducer";
import extensionReducer from "./source/source.reducers";
import sourceReducer from "./source/source.reducers";
import novelReducer from "./novel/novel.reducer";
import historyReducer from "./history/history.reducer";
import updatesReducer from "./updates/updates.reducer";
import trackerReducer from "./tracker/tracker.reducer";
import preferenceReducer from "./preferences/preference.reducer";
const persistConfig = {
key: "root",
@@ -20,13 +21,14 @@ const persistConfig = {
const persistedReducer = persistReducer(
persistConfig,
combineReducers({
themeReducer,
settingsReducer,
libraryReducer,
extensionReducer,
sourceReducer,
novelReducer,
historyReducer,
updatesReducer,
trackerReducer,
preferenceReducer,
})
);

Some files were not shown because too many files have changed in this diff Show More