Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ed0e8b4c4 | |||
| 458cdb2c2a | |||
| 0a476ba6b4 | |||
| 6e85909f6f | |||
| 5642122a5e | |||
| 77481dfc06 | |||
| a870442022 | |||
| a9a176297e | |||
| f4c0a647fb | |||
| 06e7e45df0 | |||
| c538bf4956 | |||
| e1679a41f1 | |||
| 3c2f4a59b7 | |||
| 8c13b1ff12 | |||
| 45083e5fd1 | |||
| 3b37c1cb33 | |||
| 48187d7d54 |
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
@@ -15,13 +15,18 @@ 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"),
|
||||
});
|
||||
|
||||
const App = () => {
|
||||
const [fontsLoaded, setFontsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// deleteDb();
|
||||
createDB();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"expo": {
|
||||
"name": "LNReader",
|
||||
"slug": "LNReader",
|
||||
"version": "1.0.0",
|
||||
"scheme": "lnreader",
|
||||
"version": "1.0.15",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"splash": {
|
||||
@@ -11,7 +12,8 @@
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"updates": {
|
||||
"fallbackToCacheTimeout": 0
|
||||
"fallbackToCacheTimeout": 0,
|
||||
"enabled": false
|
||||
},
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"ios": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Generated
+34
-778
File diff suppressed because it is too large
Load Diff
+54
-54
@@ -1,56 +1,56 @@
|
||||
{
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.2",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-native-community/netinfo": "^5.9.7",
|
||||
"@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-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",
|
||||
"moment": "^2.29.1",
|
||||
"react": "16.13.1",
|
||||
"react-color-extractor": "^1.1.2",
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
|
||||
"react-native-collapsible-header-views": "^1.1.2",
|
||||
"react-native-gesture-handler": "^1.8.0",
|
||||
"react-native-input-spinner": "^1.7.7",
|
||||
"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-vector-icons": "^7.1.0",
|
||||
"react-native-web": "~0.13.12",
|
||||
"react-native-webview": "11.0.0",
|
||||
"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",
|
||||
"typeorm": "^0.2.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.9.0",
|
||||
"@types/node": "^14.14.35"
|
||||
},
|
||||
"private": true
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.2",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-native-community/netinfo": "^5.9.7",
|
||||
"@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-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",
|
||||
"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-collapsible-header-views": "^1.1.2",
|
||||
"react-native-gesture-handler": "^1.8.0",
|
||||
"react-native-inappbrowser-reborn": "^3.5.1",
|
||||
"react-native-offline": "^5.7.0",
|
||||
"react-native-paper": "^4.5.0",
|
||||
"react-native-picker-scrollview": "^1.0.1",
|
||||
"react-native-reanimated": "^1.13.2",
|
||||
"react-native-safe-area-context": "3.1.9",
|
||||
"react-native-screens": "~2.15.0",
|
||||
"react-native-vector-icons": "^7.1.0",
|
||||
"react-native-web": "~0.13.12",
|
||||
"react-native-webview": "11.0.0",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.9.0",
|
||||
"@types/node": "^14.14.35"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ import Constants from "expo-constants";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const Appbar = ({ title, onBackAction }) => {
|
||||
export const Appbar = ({ title, onBackAction, style }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<MaterialAppbar.Header style={{ backgroundColor: theme.colorPrimary }}>
|
||||
<MaterialAppbar.Header
|
||||
style={[{ backgroundColor: theme.colorPrimary }, style]}
|
||||
>
|
||||
{onBackAction && (
|
||||
<MaterialAppbar.BackAction onPress={onBackAction} />
|
||||
)}
|
||||
@@ -34,7 +36,7 @@ export const SearchAppbar = ({
|
||||
getNovels,
|
||||
setLoading,
|
||||
screen,
|
||||
onFilter,
|
||||
openWebView,
|
||||
}) => {
|
||||
const searchRef = useRef(null);
|
||||
|
||||
@@ -110,17 +112,15 @@ export const SearchAppbar = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon="filter-variant"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
disabled={typeof onFilter === "function" ? false : true}
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
onPress={() => onFilter?.()}
|
||||
/>
|
||||
{openWebView && (
|
||||
<IconButton
|
||||
icon="earth"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={openWebView}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
@@ -10,9 +10,7 @@ const EmptyView = ({ icon, description }) => {
|
||||
<Text
|
||||
style={[
|
||||
styles.emptyViewIcon,
|
||||
{
|
||||
color: theme.textColorSecondary,
|
||||
},
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
{icon}
|
||||
@@ -20,9 +18,7 @@ const EmptyView = ({ icon, description }) => {
|
||||
<Text
|
||||
style={[
|
||||
styles.emptyViewText,
|
||||
{
|
||||
color: theme.textColorSecondary,
|
||||
},
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
{description}
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { List, Divider as PaperDivider } from "react-native-paper";
|
||||
|
||||
const ListSection = ({ children }) => (
|
||||
<List.Section style={styles.listSection}>{children}</List.Section>
|
||||
);
|
||||
|
||||
const ListSubHeader = ({ children, theme }) => (
|
||||
<List.Subheader
|
||||
style={[styles.listSubHeader, { color: theme.colorAccentDark }]}
|
||||
>
|
||||
{children}
|
||||
</List.Subheader>
|
||||
);
|
||||
|
||||
const ListItem = ({ title, description, icon, onPress, theme, right }) => (
|
||||
<List.Item
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
description={description}
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
descriptionNumberOfLines={1}
|
||||
left={() =>
|
||||
icon && (
|
||||
<List.Icon
|
||||
color={theme.colorAccentDark}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export { ListSection, ListSubHeader, ListItem, Divider };
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listSection: {
|
||||
flex: 1,
|
||||
marginVertical: 0,
|
||||
},
|
||||
listSubHeader: {
|
||||
paddingBottom: 5,
|
||||
},
|
||||
});
|
||||
@@ -148,24 +148,86 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
|
||||
style={styles.listView}
|
||||
onPress={onPress}
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.extensionIcon}
|
||||
/>
|
||||
<Text
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
color: theme.textColorPrimary,
|
||||
marginLeft: 16,
|
||||
fontSize: 15,
|
||||
paddingRight: 8,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
</>
|
||||
<Image
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.extensionIcon}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
marginLeft: 16,
|
||||
fontSize: 15,
|
||||
paddingRight: 8,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{item.chaptersDownloaded && (
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
backgroundColor: "#47a84a",
|
||||
color: "#FFFFFF",
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
!item.chaptersUnread && {
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.chaptersDownloaded}
|
||||
</Text>
|
||||
)}
|
||||
{item.chaptersUnread && (
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
backgroundColor: "#2979FF",
|
||||
color: "#FFFFFF",
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
!item.chaptersDownloaded && {
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.chaptersUnread}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
@@ -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.colorAccentDark}
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from "react";
|
||||
import { Checkbox } from "react-native-paper";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const DisplayCheckbox = ({ displayMode, onPress, value }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
const label = {
|
||||
0: "Compact Grid",
|
||||
1: "Comfortable Grid",
|
||||
2: "List",
|
||||
};
|
||||
|
||||
return (
|
||||
<Checkbox.Item
|
||||
label={label[value]}
|
||||
labelStyle={{ color: theme.textColorPrimary }}
|
||||
status={displayMode === value ? "checked" : "unchecked"}
|
||||
mode="ios"
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThemeCheckbox = ({ onPress, label, checked }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<Checkbox.Item
|
||||
label={label}
|
||||
labelStyle={{ color: theme.textColorPrimary }}
|
||||
status={checked ? "checked" : "unchecked"}
|
||||
mode="ios"
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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,7 +113,7 @@ export const downloadChapter = async (
|
||||
chapterUrl,
|
||||
chapterId
|
||||
) => {
|
||||
const downloadUrl = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
|
||||
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
|
||||
|
||||
// console.log(downloadUrl);
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,15 +17,12 @@ 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 Trackers from "../screens/More/Trackers";
|
||||
|
||||
import NovelItem from "../screens/Novel/Novel";
|
||||
import ChapterItem from "../screens/Chapter/Chapter";
|
||||
|
||||
// Extensions
|
||||
import BoxNovel from "../screens/extensions/boxnovel/BoxNovel";
|
||||
import ReadLightNovel from "../screens/extensions/readlightnovel/ReadLightNovel";
|
||||
import FastNovel from "../screens/extensions/fastnovel/FastNovel";
|
||||
import ReadNovelFull from "../screens/extensions/readnovelfull/ReadNovelFull";
|
||||
import Extension from "../screens/Extension/Extension";
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
@@ -41,17 +38,7 @@ const SettingsStack = () => {
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Screen name="About" component={About} />
|
||||
<Stack.Screen name="Settings" component={Settings} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
const ExtensionStack = () => {
|
||||
return (
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Screen name="BoxNovel" component={BoxNovel} />
|
||||
<Stack.Screen name="ReadLightNovel" component={ReadLightNovel} />
|
||||
<Stack.Screen name="FastNovel" component={FastNovel} />
|
||||
<Stack.Screen name="ReadNovelFull" component={ReadNovelFull} />
|
||||
<Stack.Screen name="Trackers" component={Trackers} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
@@ -138,9 +125,9 @@ const BottomNavigator = () => {
|
||||
};
|
||||
|
||||
const Router = () => {
|
||||
const themeCode = useSelector((state) => state.themeReducer.themeCode);
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
useEffect(() => setStatusBarStyle(themeCode === 1 ? "dark" : "light"), []);
|
||||
useEffect(() => setStatusBarStyle(theme.id === 1 ? "dark" : "light"), []);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
@@ -162,10 +149,11 @@ const Router = () => {
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name="SettingsStack" component={SettingsStack} />
|
||||
<Stack.Screen
|
||||
<Stack.Screen name="Extension" component={Extension} />
|
||||
{/* <Stack.Screen
|
||||
name="ExtensionStack"
|
||||
component={ExtensionStack}
|
||||
/>
|
||||
/> */}
|
||||
</Stack.Navigator>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@@ -27,12 +27,9 @@ 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);
|
||||
|
||||
@@ -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,23 @@ 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 FOLLOW_NOVEL:
|
||||
// return {
|
||||
// ...state,
|
||||
// novels: [...state.novels, { ...payload, followed: 1 }],
|
||||
// };
|
||||
// case UNFOLLOW_NOVEL:
|
||||
// return {
|
||||
// ...state,
|
||||
// novels: state.novels.filter(
|
||||
// (novel) => novel.novelId !== payload
|
||||
// ),
|
||||
// };
|
||||
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;
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
downloadChapter,
|
||||
deleteChapter,
|
||||
} from "../../database/queries/ChapterQueries";
|
||||
import { deleteNovelUpdates } from "../../database/queries/UpdateQueries";
|
||||
|
||||
export const setNovel = (novel) => async (dispatch) => {
|
||||
dispatch({ type: SET_NOVEL, payload: novel });
|
||||
@@ -104,9 +105,13 @@ export const sortAndFilterChapters = (novelUrl, filter, sort) => async (
|
||||
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 },
|
||||
});
|
||||
|
||||
ToastAndroid.show(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
UPDATE_READER_THEME,
|
||||
UPDATE_READER_TEXT_ALIGN,
|
||||
UPDATE_READER_PADDING,
|
||||
SET_READER_FONT,
|
||||
} from "./settings.types";
|
||||
|
||||
export const setDisplayMode = (val) => (dispatch) => {
|
||||
@@ -48,3 +49,10 @@ export const updateReaderPadding = (val) => (dispatch) => {
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const setReaderFont = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_READER_FONT,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
SET_DISPLAY_MODE,
|
||||
SET_ITEMS_PER_ROW,
|
||||
SET_READER_FONT,
|
||||
UPDATE_READER_PADDING,
|
||||
UPDATE_READER_TEXT_ALIGN,
|
||||
UPDATE_READER_TEXT_SIZE,
|
||||
@@ -14,7 +15,8 @@ const initialState = {
|
||||
theme: 1,
|
||||
textSize: 16,
|
||||
textAlign: "left",
|
||||
padding: "5",
|
||||
padding: 5,
|
||||
fontFamily: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -62,6 +64,11 @@ const settingsReducer = (state = initialState, action) => {
|
||||
...state,
|
||||
reader: { ...state.reader, padding: payload },
|
||||
};
|
||||
case SET_READER_FONT:
|
||||
return {
|
||||
...state,
|
||||
reader: { ...state.reader, fontFamily: payload },
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ export const UPDATE_READER_THEME = "UPDATE_READER_THEME";
|
||||
export const UPDATE_READER_TEXT_SIZE = "UPDATE_READER_TEXT_SIZE";
|
||||
export const UPDATE_READER_TEXT_ALIGN = "UPDATE_READER_TEXT_ALIGN";
|
||||
export const UPDATE_READER_PADDING = "UPDATE_READER_PADDING";
|
||||
export const SET_READER_FONT = "SET_READER_FONT";
|
||||
|
||||
@@ -10,6 +10,7 @@ import extensionReducer 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";
|
||||
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
@@ -27,6 +28,7 @@ const persistedReducer = persistReducer(
|
||||
novelReducer,
|
||||
historyReducer,
|
||||
updatesReducer,
|
||||
trackerReducer,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { SWITCH_THEME } from "./theme.types";
|
||||
|
||||
/**
|
||||
* 0 -> Amoled Dark Theme
|
||||
* 1 -> Light Theme
|
||||
* 2 -> Dark Theme
|
||||
* 3 -> Midnight Dusk Theme
|
||||
*/
|
||||
|
||||
export const switchTheme = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SWITCH_THEME,
|
||||
|
||||
@@ -13,7 +13,6 @@ const themes = {
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
themeCode: 0,
|
||||
theme: themes[0],
|
||||
};
|
||||
|
||||
@@ -22,7 +21,7 @@ const themeReducer = (state = initialState, action) => {
|
||||
|
||||
switch (type) {
|
||||
case "SWITCH_THEME":
|
||||
return { ...state, themeCode: payload, theme: themes[payload] };
|
||||
return { ...state, theme: themes[payload] };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { findListItem, updateItem } from "../../trackers/MyAnimeList";
|
||||
import {
|
||||
REMOVE_TRACKER,
|
||||
SET_TRACKER,
|
||||
TRACK_NOVEL,
|
||||
UNTRACK_NOVEL,
|
||||
UPDATE_TRACKER,
|
||||
UPDATE_CHAPTERS_READ,
|
||||
} from "./tracker.types";
|
||||
|
||||
import { updateMalChaptersRead } from "../../trackers/MyAnimeList";
|
||||
|
||||
export const setTracker = (res) => async (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_TRACKER,
|
||||
payload: res,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeTracker = () => async (dispatch) => {
|
||||
dispatch({ type: REMOVE_TRACKER });
|
||||
};
|
||||
|
||||
export const trackNovel = (trackObj, accessToken) => async (dispatch) => {
|
||||
let res = await findListItem(trackObj.id, accessToken);
|
||||
trackObj.num_chapters = res.num_chapters;
|
||||
trackObj.my_list_status = {
|
||||
num_chapters_read: res.my_list_status.num_chapters_read,
|
||||
score: res.my_list_status.score,
|
||||
status: res.my_list_status.status,
|
||||
};
|
||||
|
||||
dispatch({ type: TRACK_NOVEL, payload: trackObj });
|
||||
};
|
||||
|
||||
export const updateTracker = (malId, accessToken, body) => async (dispatch) => {
|
||||
let res = await updateItem(malId, accessToken, body);
|
||||
|
||||
dispatch({ type: UPDATE_TRACKER, payload: { malId, ...res } });
|
||||
};
|
||||
|
||||
export const untrackNovel = (id) => async (dispatch) => {
|
||||
dispatch({ type: UNTRACK_NOVEL, payload: id });
|
||||
};
|
||||
|
||||
export const updateChaptersRead = (malId, accessToken, chaptersRead) => async (
|
||||
dispatch
|
||||
) => {
|
||||
let res = await updateMalChaptersRead(malId, accessToken, {
|
||||
num_chapters_read: chaptersRead,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_CHAPTERS_READ,
|
||||
payload: { malId, chaptersRead: res },
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
REMOVE_TRACKER,
|
||||
SET_TRACKER,
|
||||
TRACK_NOVEL,
|
||||
UNTRACK_NOVEL,
|
||||
UPDATE_CHAPTERS_READ,
|
||||
UPDATE_TRACKER,
|
||||
} from "./tracker.types";
|
||||
|
||||
const initialState = {
|
||||
tracker: null,
|
||||
trackedNovels: [],
|
||||
};
|
||||
|
||||
const trackerReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case SET_TRACKER:
|
||||
return {
|
||||
...state,
|
||||
tracker: { ...payload, name: "MyAnimeList" },
|
||||
};
|
||||
case REMOVE_TRACKER:
|
||||
return {
|
||||
...state,
|
||||
tracker: null,
|
||||
};
|
||||
case TRACK_NOVEL:
|
||||
return {
|
||||
...state,
|
||||
trackedNovels: state.trackedNovels.some(
|
||||
(obj) => obj.novelId === payload.novelId
|
||||
)
|
||||
? state.trackedNovels
|
||||
: [...state.trackedNovels, payload],
|
||||
};
|
||||
case UPDATE_TRACKER:
|
||||
return {
|
||||
...state,
|
||||
trackedNovels: state.trackedNovels.map((novel) =>
|
||||
novel.id === payload.malId
|
||||
? {
|
||||
...novel,
|
||||
my_list_status: {
|
||||
num_chapters_read: payload.num_chapters_read,
|
||||
status: payload.status,
|
||||
score: payload.score,
|
||||
},
|
||||
}
|
||||
: novel
|
||||
),
|
||||
};
|
||||
case UNTRACK_NOVEL:
|
||||
return {
|
||||
...state,
|
||||
trackedNovels: state.trackedNovels.filter(
|
||||
(id) => id === payload
|
||||
),
|
||||
};
|
||||
case UPDATE_CHAPTERS_READ:
|
||||
return {
|
||||
...state,
|
||||
trackedNovels: state.trackedNovels.map((novel) =>
|
||||
novel.id === payload.malId
|
||||
? {
|
||||
...novel,
|
||||
my_list_status: {
|
||||
...novel.my_list_status,
|
||||
num_chapters_read: payload.chaptersRead,
|
||||
},
|
||||
}
|
||||
: novel
|
||||
),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default trackerReducer;
|
||||
@@ -0,0 +1,9 @@
|
||||
export const SET_TRACKER = "SET_TRACKER";
|
||||
export const REMOVE_TRACKER = "REMOVE_TRACKER";
|
||||
|
||||
export const TRACK_NOVEL = "TRACK_NOVEL";
|
||||
export const UNTRACK_NOVEL = "UNTRACK_NOVEL";
|
||||
|
||||
export const UPDATE_TRACKER = "UPDATE_TRACKER";
|
||||
|
||||
export const UPDATE_CHAPTERS_READ = "UPDATE_CHAPTERS_READ";
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
import { Appbar } from "../../components/common/Appbar";
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { getSourcesAction } from "../../redux/source/source.actions";
|
||||
@@ -51,6 +51,5 @@ export default connect(mapStateToProps, { getSourcesAction })(Browse);
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useNavigation } from "@react-navigation/native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const ExtensionCard = ({ item }) => {
|
||||
const { sourceId, sourceName, sourceCover, sourceLanguage } = item;
|
||||
const { sourceName, sourceCover, sourceLanguage, status } = item;
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
@@ -16,8 +16,10 @@ const ExtensionCard = ({ item }) => {
|
||||
<TouchableRipple
|
||||
style={styles.extensionCard}
|
||||
onPress={() =>
|
||||
navigation.navigate("ExtensionStack", {
|
||||
screen: sourceName,
|
||||
navigation.navigate("Extension", {
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
sourceUrl: item.sourceUrl,
|
||||
})
|
||||
}
|
||||
rippleColor={theme.rippleColor}
|
||||
@@ -25,9 +27,7 @@ const ExtensionCard = ({ item }) => {
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{
|
||||
uri: sourceCover,
|
||||
}}
|
||||
source={{ uri: sourceCover }}
|
||||
style={styles.extensionIcon}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
@@ -57,7 +57,7 @@ const ExtensionCard = ({ item }) => {
|
||||
marginLeft: 5,
|
||||
}}
|
||||
>
|
||||
{sourceId === 3 && "(Not Working)"}
|
||||
{status === 0 && "(Not Working)"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -69,8 +69,10 @@ const ExtensionCard = ({ item }) => {
|
||||
uppercase={false}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() =>
|
||||
navigation.navigate("ExtensionStack", {
|
||||
screen: sourceName,
|
||||
navigation.navigate("Extension", {
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
sourceUrl: item.sourceUrl,
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -92,7 +94,6 @@ const styles = StyleSheet.create({
|
||||
marginVertical: 4,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 4,
|
||||
},
|
||||
extensionIcon: {
|
||||
width: 40,
|
||||
|
||||
@@ -12,7 +12,9 @@ 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,
|
||||
@@ -35,6 +37,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,10 +71,14 @@ const ChapterItem = ({
|
||||
return textColor;
|
||||
};
|
||||
|
||||
const chapterNumber = !loading && parseChapterNumber(chapter.chapterName);
|
||||
|
||||
const readerLineHeight = (val) => {
|
||||
const lineHeight = {
|
||||
12: 20,
|
||||
14: 22,
|
||||
16: 25,
|
||||
18: 26,
|
||||
20: 28,
|
||||
};
|
||||
return lineHeight[val];
|
||||
@@ -77,6 +96,8 @@ const ChapterItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
// !loading && parseChapterNumber(chapter.chapterName);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<CollapsibleHeaderScrollView
|
||||
@@ -152,6 +173,20 @@ const ChapterItem = ({
|
||||
onScroll={({ nativeEvent }) => {
|
||||
if (isCloseToBottom(nativeEvent)) {
|
||||
markChapterReadAction(chapterId);
|
||||
if (
|
||||
isTracked &&
|
||||
chapterNumber &&
|
||||
chapterNumber >
|
||||
isTracked.my_list_status.num_chapters_read
|
||||
) {
|
||||
dispatch(
|
||||
updateChaptersRead(
|
||||
isTracked.id,
|
||||
tracker.access_token,
|
||||
chapterNumber
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -173,6 +208,9 @@ const ChapterItem = ({
|
||||
textAlign: reader.textAlign,
|
||||
paddingHorizontal: `${reader.padding}%`,
|
||||
},
|
||||
reader.fontFamily && {
|
||||
fontFamily: reader.fontFamily,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{chapter.chapterText.trim()}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { Animated, StyleSheet, View, Text, Dimensions } from "react-native";
|
||||
import { ToggleButton, IconButton } from "react-native-paper";
|
||||
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 { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
setReaderFont,
|
||||
updateReaderPadding,
|
||||
updateReaderTextAlign,
|
||||
updateReaderTextSize,
|
||||
updateReaderTheme,
|
||||
} from "../../../redux/settings/settings.actions";
|
||||
import BottomSheetHandle from "../../../components/common/BottomSheetHandle";
|
||||
import BottomSheetHandle from "../../../components/BottomSheetHandle";
|
||||
|
||||
const ChapterBottomSheet = ({
|
||||
bottomSheetRef,
|
||||
@@ -23,13 +24,14 @@ const ChapterBottomSheet = ({
|
||||
updateReaderTheme,
|
||||
updateReaderTextAlign,
|
||||
updateReaderPadding,
|
||||
setReaderFont,
|
||||
}) => {
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={new Animated.Value(0)}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 360, bottom: 0 }}
|
||||
snappingPoints={[0, 360]}
|
||||
draggableRange={{ top: 400, bottom: 0 }}
|
||||
snappingPoints={[0, 400]}
|
||||
showBackdrop={false}
|
||||
>
|
||||
<View style={styles.contentContainer}>
|
||||
@@ -37,7 +39,7 @@ const ChapterBottomSheet = ({
|
||||
<View style={styles.readerSettingsContainer}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
@@ -57,94 +59,116 @@ const ChapterBottomSheet = ({
|
||||
thumbTintColor={theme.colorAccentDark}
|
||||
onValueChange={(value) => updateReaderTextSize(value)}
|
||||
/>
|
||||
<Text
|
||||
<View
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontWeight: "bold",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
Reader Theme
|
||||
</Text>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) => updateReaderTheme(value)}
|
||||
value={reader.theme}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="#FFFFFF"
|
||||
value={1}
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
marginHorizontal: 10,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="#000000"
|
||||
value={2}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Reader Theme
|
||||
</Text>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) =>
|
||||
updateReaderTheme(value)
|
||||
}
|
||||
value={reader.theme}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="#FFFFFF"
|
||||
value={1}
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="#000000"
|
||||
value={2}
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="#000000"
|
||||
value={3}
|
||||
style={{
|
||||
backgroundColor: "#F4ECD8",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
</ToggleButton.Row>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="#000000"
|
||||
value={3}
|
||||
style={{
|
||||
backgroundColor: "#F4ECD8",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
</ToggleButton.Row>
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Text Align
|
||||
</Text>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) =>
|
||||
updateReaderTextAlign(value ?? "left")
|
||||
}
|
||||
value={reader.textAlign}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<ToggleButton
|
||||
icon="format-align-left"
|
||||
color="#000000"
|
||||
value="left"
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-align-justify"
|
||||
color="#000000"
|
||||
value="justify"
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-align-right"
|
||||
color="#000000"
|
||||
value="right"
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
</ToggleButton.Row>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
Text Align
|
||||
</Text>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) =>
|
||||
updateReaderTextAlign(value ?? "left")
|
||||
}
|
||||
value={reader.textAlign}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<ToggleButton
|
||||
icon="format-align-left"
|
||||
color="#000000"
|
||||
value="left"
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-align-justify"
|
||||
color="#000000"
|
||||
value="justify"
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-align-right"
|
||||
color="#000000"
|
||||
value="right"
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
</ToggleButton.Row>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
}}
|
||||
@@ -170,7 +194,7 @@ const ChapterBottomSheet = ({
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
color: "#FFFFFF",
|
||||
paddingHorizontal: 50,
|
||||
}}
|
||||
>
|
||||
@@ -186,6 +210,42 @@ const ChapterBottomSheet = ({
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
Font Style
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{fonts.map((font, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
onPress={() => setReaderFont(font.fontFamily)}
|
||||
textStyle={{ fontFamily: font.fontFamily }}
|
||||
style={{
|
||||
borderWidth: 0,
|
||||
marginHorizontal: 5,
|
||||
marginVertical: 5,
|
||||
}}
|
||||
selected={
|
||||
reader.fontFamily === font.fontFamily
|
||||
? true
|
||||
: false
|
||||
}
|
||||
>
|
||||
{font.name}
|
||||
</Chip>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
@@ -202,6 +262,7 @@ export default connect(mapStateToProps, {
|
||||
updateReaderTheme,
|
||||
updateReaderTextAlign,
|
||||
updateReaderPadding,
|
||||
setReaderFont,
|
||||
})(ChapterBottomSheet);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
+28
-12
@@ -1,14 +1,17 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
import { Provider } from "react-native-paper";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
import NovelCover from "../../../components/common/NovelCover";
|
||||
import EmptyView from "../../../components/common/EmptyView";
|
||||
import { SearchAppbar } from "../../../components/common/Appbar";
|
||||
import NovelCover from "../../components/NovelCover";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
import { SearchAppbar } from "../../components/Appbar";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const ReadLightNovel = ({ navigation }) => {
|
||||
const Extension = ({ navigation, route }) => {
|
||||
const { sourceId, sourceName, sourceUrl } = route.params;
|
||||
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
const library = useSelector((state) => state.libraryReducer.novels);
|
||||
const itemsPerRow = useSelector(
|
||||
@@ -16,13 +19,28 @@ const ReadLightNovel = ({ navigation }) => {
|
||||
);
|
||||
|
||||
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(`https://lnreader-extensions.herokuapp.com/api/2/novels/`)
|
||||
fetch(getSourceUrl())
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
@@ -32,9 +50,7 @@ const ReadLightNovel = ({ navigation }) => {
|
||||
|
||||
const getSearchResults = (searchText) => {
|
||||
setLoading(true);
|
||||
fetch(
|
||||
`https://lnreader-extensions.herokuapp.com/api/2/search/?s=${searchText}`
|
||||
)
|
||||
fetch(getSearchUrl())
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
@@ -60,12 +76,13 @@ const ReadLightNovel = ({ navigation }) => {
|
||||
>
|
||||
<SearchAppbar
|
||||
screen="Extension"
|
||||
placeholder="Search ReadLightNovel"
|
||||
placeholder={`Search ${sourceName}`}
|
||||
searchText={searchText}
|
||||
setSearchText={setSearchText}
|
||||
getSearchResults={getSearchResults}
|
||||
getNovels={getNovels}
|
||||
setLoading={setLoading}
|
||||
openWebView={() => WebBrowser.openBrowserAsync(sourceUrl)}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
@@ -98,7 +115,6 @@ const ReadLightNovel = ({ navigation }) => {
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
searchText !== "" &&
|
||||
novels.length === 0 && (
|
||||
<EmptyView
|
||||
icon="(; ̄Д ̄)"
|
||||
@@ -113,7 +129,7 @@ const ReadLightNovel = ({ navigation }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadLightNovel;
|
||||
export default Extension;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
@@ -3,11 +3,11 @@ import { useFocusEffect } from "@react-navigation/native";
|
||||
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
|
||||
import { Appbar } from "../../components/common/Appbar";
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import HistoryCard from "./components/HistoryCard";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import EmptyView from "../../components/common/EmptyView";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
|
||||
import {
|
||||
getHistoryAction,
|
||||
|
||||
@@ -75,11 +75,11 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation }) => {
|
||||
style={{ marginRight: 10 }}
|
||||
onPress={() =>
|
||||
navigation.navigate("ChapterItem", {
|
||||
chapterId: item.historyChapterId,
|
||||
chapterUrl: item.chapterUrl,
|
||||
extensionId: item.extensionId,
|
||||
extensionId: item.sourceId,
|
||||
novelUrl: item.novelUrl,
|
||||
novelName: item.novelName,
|
||||
novelCover: item.novelCover,
|
||||
novelId: item.novelId,
|
||||
chapterName: item.chapterName,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ import { FlatList } from "react-native-gesture-handler";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import NovelCover from "../../components/common/NovelCover";
|
||||
import EmptyView from "../../components/common/EmptyView";
|
||||
import NovelCover from "../../components/NovelCover";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
|
||||
import {
|
||||
getLibraryAction,
|
||||
searchLibraryAction,
|
||||
} from "../../redux/library/library.actions";
|
||||
import { SearchAppbar } from "../../components/common/Appbar";
|
||||
import { SearchAppbar } from "../../components/Appbar";
|
||||
import { setNovel } from "../../redux/novel/novel.actions";
|
||||
import { updateAllNovels } from "../../services/updates";
|
||||
|
||||
@@ -30,16 +30,18 @@ const LibraryScreen = ({
|
||||
getLibraryAction,
|
||||
searchLibraryAction,
|
||||
setNovel,
|
||||
searchResults,
|
||||
itemsPerRow,
|
||||
}) => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
getLibraryAction();
|
||||
}, [getLibraryAction, novels]);
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setSearchText("");
|
||||
getLibraryAction();
|
||||
}, [getLibraryAction])
|
||||
);
|
||||
|
||||
/**
|
||||
* TODO: fix refreshing
|
||||
@@ -85,7 +87,7 @@ const LibraryScreen = ({
|
||||
}}
|
||||
numColumns={itemsPerRow}
|
||||
key={itemsPerRow}
|
||||
data={!searchText ? novels : searchResults}
|
||||
data={novels}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
@@ -129,7 +131,6 @@ const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
itemsPerRow: state.settingsReducer.itemsPerRow,
|
||||
novels: state.libraryReducer.novels,
|
||||
searchResults: state.libraryReducer.searchResults,
|
||||
loading: state.libraryReducer.loading,
|
||||
});
|
||||
|
||||
|
||||
+55
-61
@@ -1,10 +1,11 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import * as Linking from "expo-linking";
|
||||
import { List, Divider } from "react-native-paper";
|
||||
|
||||
import { Appbar } from "../../components/common/Appbar";
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { Divider, ListItem, ListSection } from "../../components/List";
|
||||
|
||||
const AboutScreen = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
@@ -12,65 +13,58 @@ const AboutScreen = ({ navigation }) => {
|
||||
return (
|
||||
<>
|
||||
<Appbar title="About" onBackAction={() => navigation.goBack()} />
|
||||
<List.Section
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: 0,
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
title="Version"
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
description="Stable 1.0.12"
|
||||
/>
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
title="Build Time"
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
description="08-04-21 08:00 AM"
|
||||
/>
|
||||
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader/commits/main"
|
||||
)
|
||||
}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
title="Github"
|
||||
description="https://github.com/rajarsheechatterjee/lnreader"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader"
|
||||
)
|
||||
}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
title="Extensions"
|
||||
description="https://github.com/rajarsheechatterjee/lnreader-extensions"
|
||||
descriptionNumberOfLines={1}
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader-extensions"
|
||||
)
|
||||
}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
</List.Section>
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<ListSection>
|
||||
<ListItem
|
||||
title="Version"
|
||||
description="Stable 1.0.15"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Build Time"
|
||||
description="30-04-21 11:00 AM"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader/commits/main"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<Divider theme={theme} />
|
||||
<ListItem
|
||||
title="Discord"
|
||||
description="https://discord.gg/QdcWN4MD63"
|
||||
onPress={() =>
|
||||
Linking.openURL("https://discord.gg/QdcWN4MD63")
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Github"
|
||||
description="https://github.com/rajarsheechatterjee/lnreader"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Extensions"
|
||||
description="https://github.com/rajarsheechatterjee/lnreader-extensions"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader-extensions"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+58
-45
@@ -1,57 +1,70 @@
|
||||
import React from "react";
|
||||
|
||||
import { StyleSheet } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
|
||||
import { Appbar } from "../../components/common/Appbar";
|
||||
|
||||
import { StyleSheet, View, Image } from "react-native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { ListItem, ListSection } from "../../components/List";
|
||||
|
||||
const MoreScreen = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar title="More" />
|
||||
<List.Section
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
title="Settings"
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.colorAccentDark}
|
||||
icon="cog-outline"
|
||||
<Appbar title="More" style={{ elevation: 0 }} />
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
|
||||
<View
|
||||
style={{
|
||||
paddingTop: 20,
|
||||
paddingBottom: 30,
|
||||
alignItems: "center",
|
||||
backgroundColor: theme.colorPrimary,
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={require("../../../assets/logo.png")}
|
||||
style={{
|
||||
height: 60,
|
||||
width: 60,
|
||||
tintColor: theme.textColorPrimary,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "Settings",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
title="About"
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.colorAccentDark}
|
||||
icon="information-outline"
|
||||
/>
|
||||
)}
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "About",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</List.Section>
|
||||
</View>
|
||||
</View>
|
||||
<ListSection>
|
||||
<ListItem
|
||||
title="Settings"
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "Settings",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Tracking"
|
||||
icon="sync"
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "Trackers",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="About"
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "About",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+75
-238
@@ -1,26 +1,25 @@
|
||||
import React, { useState } from "react";
|
||||
import { View, Text, Dimensions } from "react-native";
|
||||
import { setStatusBarStyle } from "expo-status-bar";
|
||||
import { List, Modal, Portal } from "react-native-paper";
|
||||
import Slider from "@react-native-community/slider";
|
||||
import InputSpinner from "react-native-input-spinner";
|
||||
|
||||
import { View } from "react-native";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { switchTheme } from "../../redux/theme/theme.actions";
|
||||
import {
|
||||
setDisplayMode,
|
||||
setItemsPerRow,
|
||||
} from "../../redux/settings/settings.actions";
|
||||
|
||||
import { Appbar } from "../../components/common/Appbar";
|
||||
import {
|
||||
DisplayCheckbox,
|
||||
ThemeCheckbox,
|
||||
} from "../../components/settings/Checkbox";
|
||||
|
||||
import { deleteAllHistory } from "../../database/queries/HistoryQueries";
|
||||
import { deleteNovelCache } from "../../database/queries/NovelQueries";
|
||||
// import { deleteDb } from "../../database/DBHelper";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import {
|
||||
Divider,
|
||||
ListItem,
|
||||
ListSection,
|
||||
ListSubHeader,
|
||||
} from "../../components/List";
|
||||
import DisplayModeModal from "./components/DisplayModeModal";
|
||||
import GridSizeModal from "./components/GridSizeModal";
|
||||
import ThemeModal from "./components/ThemeModal";
|
||||
|
||||
const SettingsScreen = ({
|
||||
navigation,
|
||||
@@ -32,266 +31,104 @@ const SettingsScreen = ({
|
||||
setDisplayMode,
|
||||
setItemsPerRow,
|
||||
}) => {
|
||||
const display = {
|
||||
0: "Compact Grid",
|
||||
1: "Comfortable Grid",
|
||||
2: "List",
|
||||
const displayModeLabel = (displayMode) => {
|
||||
const label = {
|
||||
0: "Compact Grid",
|
||||
1: "Comfortable Grid",
|
||||
2: "List",
|
||||
};
|
||||
|
||||
return label[displayMode];
|
||||
};
|
||||
|
||||
const themes = [
|
||||
{ themeCode: 0, label: "AMOLED Dark Theme", statusBar: "light" },
|
||||
{ themeCode: 1, label: "Light Theme", statusBar: "dark" },
|
||||
{ themeCode: 2, label: "Dark Theme", statusBar: "light" },
|
||||
{ themeCode: 3, label: "Midnight Dusk Theme", statusBar: "light" },
|
||||
];
|
||||
|
||||
const desciptionStyles = {
|
||||
color: theme.textColorSecondary,
|
||||
};
|
||||
|
||||
const titleStyles = { color: theme.textColorPrimary };
|
||||
|
||||
/**
|
||||
* Display Mode Modal
|
||||
*/
|
||||
// Display Mode Modal
|
||||
const [displayModalVisible, setDisplayModalVisible] = useState(false);
|
||||
const showDisplayModal = () => setDisplayModalVisible(true);
|
||||
const hideDisplayModal = () => setDisplayModalVisible(false);
|
||||
|
||||
/**
|
||||
* Change Theme Modal
|
||||
*/
|
||||
// Theme Modal
|
||||
const [themeModalVisible, setthemeModalVisible] = useState(false);
|
||||
const showthemeModal = () => setthemeModalVisible(true);
|
||||
const hidethemeModal = () => setthemeModalVisible(false);
|
||||
|
||||
const [itemsModalVisible, setitemsModalVisible] = useState(false);
|
||||
const showitemsModal = () => setitemsModalVisible(true);
|
||||
const hideitemsModal = () => setitemsModalVisible(false);
|
||||
// Grid Size Modal
|
||||
const [gridSizeModalVisible, setGridSizeModalVisible] = useState(false);
|
||||
const showGridSizeModal = () => setGridSizeModalVisible(true);
|
||||
const hideGridSizeModal = () => setGridSizeModalVisible(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar title="Settings" onBackAction={() => navigation.goBack()} />
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<List.Section
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
}}
|
||||
>
|
||||
<List.Subheader
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
paddingBottom: 5,
|
||||
}}
|
||||
>
|
||||
Data
|
||||
</List.Subheader>
|
||||
<List.Item
|
||||
titleStyle={titleStyles}
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Data</ListSubHeader>
|
||||
<ListItem
|
||||
title="Clear database"
|
||||
descriptionStyle={desciptionStyles}
|
||||
description="Delete history for novels not in your library"
|
||||
onPress={() => deleteNovelCache()}
|
||||
rippleColor={theme.rippleColor}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
titleStyle={titleStyles}
|
||||
<ListItem
|
||||
title="Clear history"
|
||||
descriptionStyle={desciptionStyles}
|
||||
description="Delete reading history for all novels"
|
||||
onPress={() => deleteAllHistory()}
|
||||
rippleColor={theme.rippleColor}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Subheader
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
paddingBottom: 5,
|
||||
}}
|
||||
>
|
||||
Display
|
||||
</List.Subheader>
|
||||
<List.Item
|
||||
titleStyle={titleStyles}
|
||||
<Divider theme={theme} />
|
||||
<ListSubHeader theme={theme}>Display</ListSubHeader>
|
||||
<ListItem
|
||||
title="Display Mode"
|
||||
descriptionStyle={desciptionStyles}
|
||||
description={display[displayMode]}
|
||||
description={displayModeLabel(displayMode)}
|
||||
onPress={showDisplayModal}
|
||||
rippleColor={theme.rippleColor}
|
||||
theme={theme}
|
||||
/>
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={displayModalVisible}
|
||||
onDismiss={hideDisplayModal}
|
||||
contentContainerStyle={{
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<DisplayCheckbox
|
||||
value={1}
|
||||
displayMode={displayMode}
|
||||
onPress={() => setDisplayMode(1)}
|
||||
/>
|
||||
<DisplayCheckbox
|
||||
value={0}
|
||||
displayMode={displayMode}
|
||||
onPress={() => setDisplayMode(0)}
|
||||
/>
|
||||
<DisplayCheckbox
|
||||
value={2}
|
||||
displayMode={displayMode}
|
||||
onPress={() => setDisplayMode(2)}
|
||||
/>
|
||||
</Modal>
|
||||
</Portal>
|
||||
<List.Item
|
||||
titleStyle={titleStyles}
|
||||
<ListItem
|
||||
title="Items per row in library"
|
||||
descriptionStyle={desciptionStyles}
|
||||
description={itemsPerRow}
|
||||
onPress={showitemsModal}
|
||||
rippleColor={theme.rippleColor}
|
||||
description={`${itemsPerRow} items per row`}
|
||||
onPress={showGridSizeModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={itemsModalVisible}
|
||||
onDismiss={hideitemsModal}
|
||||
contentContainerStyle={{
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 18,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
Grid size
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
{itemsPerRow} per row
|
||||
</Text>
|
||||
{/* <InputSpinner
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
elevation: 0,
|
||||
}}
|
||||
max={10}
|
||||
min={1}
|
||||
skin="paper"
|
||||
background={theme.colorPrimaryDark}
|
||||
colorMax={theme.colorAccentDark}
|
||||
colorLeft={theme.colorAccentDark}
|
||||
colorRight={theme.colorAccentDark}
|
||||
buttonTextColor={theme.textColorPrimary}
|
||||
colorPress={"white"}
|
||||
colorMin={theme.colorAccentDark}
|
||||
value={itemsPerRow}
|
||||
onChange={(num) => setItemsPerRow(num)}
|
||||
inputStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
/> */}
|
||||
|
||||
<Slider
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 40,
|
||||
// backgroundColor: "red",
|
||||
}}
|
||||
value={itemsPerRow}
|
||||
minimumValue={1}
|
||||
maximumValue={5}
|
||||
step={1}
|
||||
minimumTrackTintColor={theme.colorAccentDark}
|
||||
maximumTrackTintColor="#000000"
|
||||
thumbTintColor={theme.colorAccentDark}
|
||||
onValueChange={(value) => setItemsPerRow(value)}
|
||||
/>
|
||||
</Modal>
|
||||
</Portal>
|
||||
|
||||
{/* <List.Subheader
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
paddingBottom: 5,
|
||||
}}
|
||||
>
|
||||
Devloper options
|
||||
</List.Subheader>
|
||||
<List.Item
|
||||
titleStyle={titleStyles}
|
||||
title="Delete database"
|
||||
descriptionStyle={desciptionStyles}
|
||||
description="Delete entire database"
|
||||
rippleColor={theme.rippleColor}
|
||||
onPress={() => deleteDb()}
|
||||
/> */}
|
||||
<List.Subheader
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
paddingBottom: 5,
|
||||
}}
|
||||
>
|
||||
Theme
|
||||
</List.Subheader>
|
||||
<List.Item
|
||||
titleStyle={titleStyles}
|
||||
<Divider theme={theme} />
|
||||
<ListSubHeader theme={theme}>Theme</ListSubHeader>
|
||||
<ListItem
|
||||
title="Theme"
|
||||
descriptionStyle={desciptionStyles}
|
||||
description={themes[themeCode].label}
|
||||
description={theme.name}
|
||||
onPress={showthemeModal}
|
||||
rippleColor={theme.rippleColor}
|
||||
theme={theme}
|
||||
/>
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={themeModalVisible}
|
||||
onDismiss={hidethemeModal}
|
||||
contentContainerStyle={{
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
{themes.map((item, index) => (
|
||||
<ThemeCheckbox
|
||||
key={index}
|
||||
label={themes[item.themeCode].label}
|
||||
checked={
|
||||
themeCode === item.themeCode
|
||||
? true
|
||||
: false
|
||||
}
|
||||
onPress={() => {
|
||||
switchTheme(item.themeCode);
|
||||
setStatusBarStyle(item.statusBar);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Modal>
|
||||
</Portal>
|
||||
</List.Section>
|
||||
</ListSection>
|
||||
<DisplayModeModal
|
||||
displayMode={displayMode}
|
||||
displayModalVisible={displayModalVisible}
|
||||
hideDisplayModal={hideDisplayModal}
|
||||
setDisplayMode={setDisplayMode}
|
||||
theme={theme}
|
||||
/>
|
||||
<GridSizeModal
|
||||
itemsPerRow={itemsPerRow}
|
||||
gridSizeModalVisible={gridSizeModalVisible}
|
||||
hideGridSizeModal={hideGridSizeModal}
|
||||
setItemsPerRow={setItemsPerRow}
|
||||
theme={theme}
|
||||
/>
|
||||
<ThemeModal
|
||||
themeModalVisible={themeModalVisible}
|
||||
hidethemeModal={hidethemeModal}
|
||||
switchTheme={switchTheme}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
themeCode: state.themeReducer.themeCode,
|
||||
theme: state.themeReducer.theme,
|
||||
displayMode: state.settingsReducer.displayMode,
|
||||
itemsPerRow: state.settingsReducer.itemsPerRow,
|
||||
|
||||
@@ -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 "../../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.themeReducer.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.colorAccentDark,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
onPress={hideModal}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginTop: 30 }}
|
||||
labelStyle={{
|
||||
color: theme.colorAccentDark,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(removeTracker());
|
||||
hideModal();
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
</View>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrackerScreen;
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Portal, Modal, Checkbox } from "react-native-paper";
|
||||
|
||||
const DisplayModeModal = ({
|
||||
displayMode,
|
||||
displayModalVisible,
|
||||
setDisplayMode,
|
||||
hideDisplayModal,
|
||||
theme,
|
||||
}) => {
|
||||
const displayModes = [
|
||||
{ displayMode: 0, label: "Compact Grid" },
|
||||
{ displayMode: 1, label: "Comfortable Grid" },
|
||||
{ displayMode: 2, label: "List" },
|
||||
];
|
||||
|
||||
const renderCheckboxes = () => {
|
||||
return displayModes.map((mode) => (
|
||||
<Checkbox.Item
|
||||
key={mode.displayMode}
|
||||
label={mode.label}
|
||||
labelStyle={{ color: theme.textColorPrimary }}
|
||||
status={
|
||||
displayMode === mode.displayMode ? "checked" : "unchecked"
|
||||
}
|
||||
mode="ios"
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() => setDisplayMode(mode.displayMode)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={displayModalVisible}
|
||||
onDismiss={hideDisplayModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
{renderCheckboxes()}
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisplayModeModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
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,
|
||||
gridSizeModalVisible,
|
||||
hideGridSizeModal,
|
||||
setItemsPerRow,
|
||||
theme,
|
||||
}) => {
|
||||
const gridSizes = {
|
||||
1: "XS",
|
||||
2: "S",
|
||||
3: "M",
|
||||
4: "L",
|
||||
5: "XL",
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={gridSizeModalVisible}
|
||||
onDismiss={hideGridSizeModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.modalHeader,
|
||||
{ color: theme.textColorPrimary },
|
||||
]}
|
||||
>
|
||||
Grid size
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.modalDescription,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
{`${itemsPerRow} per row`}
|
||||
</Text>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={itemsPerRow}
|
||||
minimumValue={1}
|
||||
maximumValue={5}
|
||||
step={1}
|
||||
minimumTrackTintColor={theme.colorAccentDark}
|
||||
maximumTrackTintColor="#000000"
|
||||
thumbTintColor={theme.colorAccentDark}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default GridSizeModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
modalHeader: {
|
||||
fontSize: 18,
|
||||
marginBottom: 10,
|
||||
},
|
||||
modalDescription: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
slider: {
|
||||
width: "100%",
|
||||
height: 40,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Portal, Modal, Checkbox } from "react-native-paper";
|
||||
|
||||
import { setStatusBarStyle } from "expo-status-bar";
|
||||
|
||||
const ThemeModal = ({
|
||||
themeModalVisible,
|
||||
hidethemeModal,
|
||||
switchTheme,
|
||||
theme,
|
||||
}) => {
|
||||
const themes = [
|
||||
{ id: 0, name: "AMOLED Dark", statusBar: "light" },
|
||||
{ id: 1, name: "Light", statusBar: "dark" },
|
||||
{ id: 2, name: "Dark", statusBar: "light" },
|
||||
{ id: 3, name: "Midnight Dusk", statusBar: "light" },
|
||||
];
|
||||
|
||||
const renderThemeCheckboxes = () =>
|
||||
themes.map((item) => (
|
||||
<Checkbox.Item
|
||||
key={item.id}
|
||||
label={item.name}
|
||||
labelStyle={{ color: theme.textColorPrimary }}
|
||||
status={theme.id === item.id ? "checked" : "unchecked"}
|
||||
mode="ios"
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() => {
|
||||
switchTheme(item.id);
|
||||
setStatusBarStyle(item.statusBar);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={themeModalVisible}
|
||||
onDismiss={hidethemeModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
{renderThemeCheckboxes()}
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
});
|
||||
+37
-49
@@ -10,18 +10,16 @@ import { Provider, Portal } from "react-native-paper";
|
||||
|
||||
import ChapterCard from "./components/ChapterCard";
|
||||
import NovelInfoHeader from "./components/NovelHeader";
|
||||
import { BottomSheet } from "./components/BottomSheet";
|
||||
import ChaptersSettingsSheet from "./components/ChaptersSettingsSheet";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
getNovelAction,
|
||||
followNovelAction,
|
||||
sortAndFilterChapters,
|
||||
updateNovelAction,
|
||||
downloadAllChaptersAction,
|
||||
deleteAllChaptersAction,
|
||||
} from "../../redux/novel/novel.actions";
|
||||
import TrackSheet from "./components/Tracker/TrackSheet";
|
||||
|
||||
const Novel = ({
|
||||
route,
|
||||
@@ -31,11 +29,8 @@ const Novel = ({
|
||||
loading,
|
||||
getNovelAction,
|
||||
fetching,
|
||||
followNovelAction,
|
||||
sortAndFilterChapters,
|
||||
updateNovelAction,
|
||||
downloadAllChaptersAction,
|
||||
deleteAllChaptersAction,
|
||||
}) => {
|
||||
const {
|
||||
sourceId,
|
||||
@@ -46,16 +41,20 @@ const Novel = ({
|
||||
novelId,
|
||||
} = route.params;
|
||||
|
||||
let _panel = useRef(null); // Bottomsheet ref
|
||||
let chaptersSettingsSheetRef = useRef(null);
|
||||
let trackerSheetRef = useRef(null);
|
||||
|
||||
const [sort, setSort] = useState("ORDER BY chapterId ASC");
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter);
|
||||
}, [getNovelAction, followNovelAction, sort, filter]);
|
||||
}, [getNovelAction, sort, filter]);
|
||||
|
||||
let lastRead = !loading && chapters.find((obj) => obj.read === 0);
|
||||
const onRefresh = async () => {
|
||||
await updateNovelAction(sourceId, novelUrl, novelId);
|
||||
ToastAndroid.show(`Updated ${novelName}`, ToastAndroid.SHORT);
|
||||
};
|
||||
|
||||
const renderChapterCard = ({ item }) => (
|
||||
<ChapterCard
|
||||
@@ -65,11 +64,6 @@ const Novel = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
updateNovelAction(sourceId, novelUrl, novelId);
|
||||
ToastAndroid.show(`Updated ${novelName}`, ToastAndroid.SHORT);
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<View
|
||||
@@ -88,24 +82,14 @@ const Novel = ({
|
||||
renderItem={renderChapterCard}
|
||||
ListHeaderComponent={
|
||||
<NovelInfoHeader
|
||||
theme={theme}
|
||||
item={{ novelName, novelCover }}
|
||||
novel={novel}
|
||||
noOfChapters={chapters?.length}
|
||||
followNovelAction={followNovelAction}
|
||||
loading={loading}
|
||||
lastRead={lastRead}
|
||||
bottomSheetRef={_panel}
|
||||
downloadAllChapters={() =>
|
||||
downloadAllChaptersAction(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapters
|
||||
)
|
||||
}
|
||||
deleteAllChapters={() =>
|
||||
deleteAllChaptersAction(chapters)
|
||||
}
|
||||
novelName={novelName}
|
||||
novelCover={novelCover}
|
||||
novel={novel}
|
||||
chapters={chapters}
|
||||
trackerSheetRef={trackerSheetRef}
|
||||
chaptersSettingsSheetRef={chaptersSettingsSheetRef}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
refreshControl={
|
||||
@@ -117,17 +101,25 @@ const Novel = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Portal>
|
||||
<BottomSheet
|
||||
novelUrl={novelUrl}
|
||||
bottomSheetRef={_panel}
|
||||
sortAndFilterChapters={sortAndFilterChapters}
|
||||
sort={sort}
|
||||
filter={filter}
|
||||
setSort={setSort}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
</Portal>
|
||||
{!loading && (
|
||||
<Portal>
|
||||
<ChaptersSettingsSheet
|
||||
novelUrl={novelUrl}
|
||||
bottomSheetRef={chaptersSettingsSheetRef}
|
||||
sortAndFilterChapters={sortAndFilterChapters}
|
||||
sort={sort}
|
||||
filter={filter}
|
||||
setSort={setSort}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
<TrackSheet
|
||||
bottomSheetRef={trackerSheetRef}
|
||||
novelId={novel.novelId}
|
||||
novelName={novel.novelName}
|
||||
theme={theme}
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
</View>
|
||||
</Provider>
|
||||
);
|
||||
@@ -142,15 +134,11 @@ const mapStateToProps = (state) => ({
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
getNovelAction,
|
||||
followNovelAction,
|
||||
|
||||
sortAndFilterChapters,
|
||||
updateNovelAction,
|
||||
downloadAllChaptersAction,
|
||||
deleteAllChaptersAction,
|
||||
})(Novel);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
container: { flex: 1 },
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ const ChapterCard = ({
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
}
|
||||
contentStyle={{ backgroundColor: "#242529" }}
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
>
|
||||
<Menu.Item
|
||||
onPress={() =>
|
||||
|
||||
+7
-2
@@ -1,11 +1,11 @@
|
||||
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,
|
||||
@@ -17,8 +17,11 @@ export const BottomSheet = ({
|
||||
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 370, bottom: 0 }}
|
||||
snappingPoints={[0, 370]}
|
||||
@@ -159,6 +162,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.colorAccentDark}
|
||||
/>
|
||||
)}
|
||||
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.colorAccentDark,
|
||||
borderColor: theme.colorAccentDark,
|
||||
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,24 @@
|
||||
import React from "react";
|
||||
import { View, ImageBackground, StyleSheet } from "react-native";
|
||||
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
|
||||
const NovelCoverImage = ({ children, source, theme }) => {
|
||||
return (
|
||||
<ImageBackground source={source} style={styles.background}>
|
||||
<LinearGradient
|
||||
colors={["rgba(0,0,0,0.2)", theme.colorPrimaryDark]}
|
||||
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.colorAccentDark,
|
||||
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,57 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Button } from "react-native-paper";
|
||||
|
||||
const ReadButton = ({ navigation, novel, chapters, theme }) => {
|
||||
const lastRead = chapters.find((obj) => obj.read === 0);
|
||||
|
||||
return (
|
||||
chapters.length > 0 &&
|
||||
(lastRead ? (
|
||||
<Button
|
||||
color="white"
|
||||
style={[
|
||||
{ backgroundColor: theme.colorAccentDark },
|
||||
styles.startButton,
|
||||
]}
|
||||
uppercase={false}
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
onPress={() => {
|
||||
navigation.navigate("ChapterItem", {
|
||||
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.textColorHintDark}
|
||||
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.colorAccentDark}
|
||||
/>
|
||||
)}
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -1,163 +1,125 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ImageBackground,
|
||||
StyleSheet,
|
||||
Image,
|
||||
FlatList,
|
||||
Share,
|
||||
} from "react-native";
|
||||
import {
|
||||
TouchableRipple,
|
||||
IconButton,
|
||||
Button,
|
||||
Chip,
|
||||
Menu,
|
||||
} from "react-native-paper";
|
||||
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 { LinearGradient } from "expo-linear-gradient";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
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,
|
||||
noOfChapters,
|
||||
followNovelAction,
|
||||
novelName,
|
||||
novelCover,
|
||||
loading,
|
||||
bottomSheetRef,
|
||||
novel,
|
||||
chapters,
|
||||
theme,
|
||||
lastRead,
|
||||
downloadAllChapters,
|
||||
deleteAllChapters,
|
||||
chaptersSettingsSheetRef,
|
||||
trackerSheetRef,
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [more, setMore] = useState(
|
||||
!loading && novel.followed !== 1 ? true : false
|
||||
const [downloadMenu, showDownloadMenu] = useState(false);
|
||||
|
||||
const tracker = useSelector((state) => state.trackerReducer.tracker);
|
||||
const trackedNovels = useSelector(
|
||||
(state) => state.trackerReducer.trackedNovels
|
||||
);
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const openMenu = () => setVisible(true);
|
||||
const closeMenu = () => setVisible(false);
|
||||
let isTracked = false;
|
||||
|
||||
const renderGenreChip = ({ item }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.genre,
|
||||
{
|
||||
color: theme.colorAccentDark,
|
||||
borderColor: theme.colorAccentDark,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item}
|
||||
</Text>
|
||||
);
|
||||
if (!loading) {
|
||||
isTracked = trackedNovels.find((obj) => obj.novelId === novel.novelId);
|
||||
}
|
||||
|
||||
const getGenres = ({ item }) => <GenreChip theme={theme}>{item}</GenreChip>;
|
||||
|
||||
return (
|
||||
<View style={{ flexGrow: 1 }}>
|
||||
<ImageBackground
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.background}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={["rgba(0,0,0,0.2)", theme.colorPrimaryDark]}
|
||||
style={styles.linearGradient}
|
||||
>
|
||||
<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>
|
||||
{!loading && (
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
marginVertical: 3,
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{novel.author.replace(",", ", ")}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
marginVertical: 3,
|
||||
fontSize: 14,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{novel.status}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{novel.source}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<NovelCoverImage source={{ uri: novelCover }} theme={theme}>
|
||||
<View style={styles.detailsContainer}>
|
||||
<View>
|
||||
<Image
|
||||
source={{ uri: novelCover }}
|
||||
style={styles.logo}
|
||||
/>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</ImageBackground>
|
||||
<View style={styles.nameContainer}>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
styles.name,
|
||||
{ color: theme.textColorPrimary },
|
||||
]}
|
||||
>
|
||||
{novelName}
|
||||
</Text>
|
||||
{!loading && (
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
marginVertical: 3,
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{novel.author.replace(",", ", ")}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
marginVertical: 3,
|
||||
fontSize: 14,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{novel.status}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{novel.source}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</NovelCoverImage>
|
||||
{!loading && (
|
||||
<>
|
||||
<View style={styles.buttonsContainer}>
|
||||
<Chip
|
||||
mode="outlined"
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name={
|
||||
novel.followed
|
||||
? "heart"
|
||||
: "heart-outline"
|
||||
}
|
||||
size={21}
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
)}
|
||||
onPress={() => followNovelAction(novel)}
|
||||
style={[
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
styles.toggleFavourite,
|
||||
]}
|
||||
textStyle={{
|
||||
fontWeight: "bold",
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{novel.followed ? "In Library" : "Add to library"}
|
||||
</Chip>
|
||||
|
||||
<FollowChip
|
||||
theme={theme}
|
||||
followed={novel.followed}
|
||||
novel={novel}
|
||||
/>
|
||||
{tracker && (
|
||||
<TrackerChip
|
||||
theme={theme}
|
||||
isTracked={isTracked}
|
||||
trackerSheetRef={trackerSheetRef}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
onPress={() =>
|
||||
WebBrowser.openBrowserAsync(
|
||||
novel.sourceUrl && novel.sourceUrl
|
||||
)
|
||||
WebBrowser.openBrowserAsync(novel.sourceUrl)
|
||||
}
|
||||
icon="earth"
|
||||
color={theme.colorAccentDark}
|
||||
@@ -165,84 +127,54 @@ const NovelInfoHeader = ({
|
||||
/>
|
||||
<IconButton
|
||||
onPress={() =>
|
||||
Share.share({
|
||||
message: novel.sourceUrl,
|
||||
})
|
||||
Share.share({ message: novel.sourceUrl })
|
||||
}
|
||||
icon="share-variant"
|
||||
color={theme.colorAccentDark}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
visible={visible}
|
||||
onDismiss={closeMenu}
|
||||
visible={downloadMenu}
|
||||
onDismiss={() => showDownloadMenu(false)}
|
||||
anchor={
|
||||
<IconButton
|
||||
icon="download"
|
||||
color={theme.colorAccentDark}
|
||||
size={21}
|
||||
onPress={openMenu}
|
||||
onPress={() => showDownloadMenu(true)}
|
||||
/>
|
||||
}
|
||||
contentStyle={{ backgroundColor: "#242529" }}
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
>
|
||||
<Menu.Item
|
||||
title="Download all"
|
||||
style={{ backgroundColor: "#242529" }}
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
onPress={downloadAllChapters}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.sourceId,
|
||||
novel.novelUrl,
|
||||
chapters
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Delete downloads"
|
||||
style={{ backgroundColor: "#242529" }}
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
onPress={deleteAllChapters}
|
||||
onPress={() =>
|
||||
dispatch(deleteAllChaptersAction(chapters))
|
||||
}
|
||||
/>
|
||||
</Menu>
|
||||
</View>
|
||||
|
||||
{novel.novelSummary !== "" && (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary },
|
||||
styles.summaryHeader,
|
||||
]}
|
||||
>
|
||||
About
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
lineHeight: 20,
|
||||
}}
|
||||
numberOfLines={more ? 100 : 2}
|
||||
onPress={() => setMore(!more)}
|
||||
ellipsizeMode="clip"
|
||||
>
|
||||
{novel.novelSummary}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
color: theme.colorAccentDark,
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
},
|
||||
styles.moreBtn,
|
||||
]}
|
||||
onPress={() => setMore(!more)}
|
||||
>
|
||||
{more ? "Less " : "More "}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<NovelSummary
|
||||
summary={novel.novelSummary}
|
||||
followed={novel.followed}
|
||||
theme={theme}
|
||||
/>
|
||||
<FlatList
|
||||
style={
|
||||
novel.novelSummary === ""
|
||||
@@ -251,57 +183,23 @@ const NovelInfoHeader = ({
|
||||
}
|
||||
contentContainerStyle={styles.genreContainer}
|
||||
horizontal
|
||||
data={novel.genre && novel.genre.split(",")}
|
||||
data={novel.genre.split(",")}
|
||||
keyExtractor={(item) => item}
|
||||
renderItem={renderGenreChip}
|
||||
renderItem={getGenres}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
{noOfChapters > 0 &&
|
||||
(lastRead ? (
|
||||
<Button
|
||||
color="white"
|
||||
style={[
|
||||
{ backgroundColor: theme.colorAccentDark },
|
||||
styles.startButton,
|
||||
]}
|
||||
uppercase={false}
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
onPress={() => {
|
||||
navigation.navigate("ChapterItem", {
|
||||
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.textColorPrimary}
|
||||
style={[
|
||||
{
|
||||
backgroundColor: theme.textColorPrimary,
|
||||
opacity: 0.2,
|
||||
},
|
||||
styles.startButton,
|
||||
]}
|
||||
uppercase={false}
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
>
|
||||
All chapters read
|
||||
</Button>
|
||||
))}
|
||||
<ReadButton
|
||||
novel={novel}
|
||||
chapters={chapters}
|
||||
navigation={navigation}
|
||||
theme={theme}
|
||||
/>
|
||||
<TouchableRipple
|
||||
style={styles.bottomsheet}
|
||||
onPress={() =>
|
||||
bottomSheetRef.current.show({ velocity: -1.5 })
|
||||
chaptersSettingsSheetRef.current.show({
|
||||
velocity: -1.5,
|
||||
})
|
||||
}
|
||||
rippleColor={theme.rippleColor}
|
||||
>
|
||||
@@ -312,7 +210,7 @@ const NovelInfoHeader = ({
|
||||
styles.chapters,
|
||||
]}
|
||||
>
|
||||
{noOfChapters + " Chapters"}
|
||||
{`${chapters.length} Chapters`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="filter-variant"
|
||||
@@ -335,12 +233,6 @@ const styles = StyleSheet.create({
|
||||
marginHorizontal: 16,
|
||||
paddingTop: 8,
|
||||
},
|
||||
background: {
|
||||
height: 285,
|
||||
},
|
||||
linearGradient: {
|
||||
height: 286,
|
||||
},
|
||||
detailsContainer: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
@@ -353,16 +245,6 @@ const styles = StyleSheet.create({
|
||||
margin: 3.2,
|
||||
borderRadius: 6,
|
||||
},
|
||||
genre: {
|
||||
borderRadius: 50,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 10,
|
||||
marginHorizontal: 2,
|
||||
fontSize: 13,
|
||||
paddingVertical: 2,
|
||||
justifyContent: "center",
|
||||
flex: 1,
|
||||
},
|
||||
name: {
|
||||
fontWeight: "bold",
|
||||
fontSize: 18,
|
||||
@@ -371,16 +253,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
toggleFavourite: {
|
||||
height: 32,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: 16,
|
||||
marginRight: 4,
|
||||
paddingHorizontal: 4,
|
||||
borderWidth: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
|
||||
chapters: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 4,
|
||||
@@ -393,24 +266,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
paddingRight: 12,
|
||||
},
|
||||
summaryHeader: {
|
||||
marginTop: 5,
|
||||
paddingVertical: 5,
|
||||
fontSize: 15,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
moreBtn: {
|
||||
fontWeight: "bold",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 16,
|
||||
paddingLeft: 6,
|
||||
},
|
||||
startButton: {
|
||||
marginTop: 8,
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
|
||||
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.colorAccentDark}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlignVertical: "center",
|
||||
color: theme.colorAccentDark,
|
||||
}}
|
||||
>
|
||||
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.colorAccentDark,
|
||||
placeholder: theme.textColorHintDark,
|
||||
text: theme.textColorPrimary,
|
||||
background: "transparent",
|
||||
},
|
||||
}}
|
||||
underlineColor={theme.textColorHintDark}
|
||||
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 "../../../../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.colorAccentDark,
|
||||
text: theme.textColorPrimary,
|
||||
background: "transparent",
|
||||
},
|
||||
}}
|
||||
underlineColor={theme.textColorHintDark}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
color={theme.textColorSecondary}
|
||||
icon="close"
|
||||
onPress={() => setSearchText("")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ScrollView
|
||||
style={{ flexGrow: 1, maxHeight: 500, marginVertical: 8 }}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
color={theme.colorAccentDark}
|
||||
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.colorAccentDark,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccentDark } }}
|
||||
onPress={() => setSelectedNovel(null)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<Button
|
||||
labelStyle={{
|
||||
color: theme.colorAccentDark,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccentDark } }}
|
||||
onPress={() => setTrackSearchDialog(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
labelStyle={{
|
||||
color: theme.colorAccentDark,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccentDark } }}
|
||||
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 },
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { useFocusEffect } from "@react-navigation/native";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import EmptyView from "../../components/common/EmptyView";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
import { getUpdatesAction } from "../../redux/updates/updates.actions";
|
||||
import { Appbar } from "./components/Appbar";
|
||||
import UpdateCard from "./components/UpdateCard";
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
import { Provider, Portal } from "react-native-paper";
|
||||
|
||||
import NovelCover from "../../../components/common/NovelCover";
|
||||
import { SearchAppbar } from "../../../components/common/Appbar";
|
||||
import { BottomSheet } from "./filters/BottomSheet";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const BoxNovel = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
const itemsPerRow = useSelector(
|
||||
(state) => state.settingsReducer.itemsPerRow
|
||||
);
|
||||
|
||||
const library = useSelector((state) => state.libraryReducer.novels);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [novels, setNovels] = useState([]);
|
||||
const [sort, setSort] = useState("rating");
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
let bottomSheetRef = useRef(null);
|
||||
|
||||
const getNovels = () => {
|
||||
fetch(
|
||||
`https://lnreader-extensions.herokuapp.com/api/1/novels/1/?o=${sort}`
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getSearchResults = (searchText) => {
|
||||
setLoading(true);
|
||||
fetch(
|
||||
`https://lnreader-extensions.herokuapp.com/api/1/search/?s=${searchText}&?o=${sort}`
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const checkIFInLibrary = (id) => {
|
||||
return library.some((obj) => obj.novelUrl === id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getNovels();
|
||||
}, [sort]);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<SearchAppbar
|
||||
screen="Extension"
|
||||
placeholder="Search BoxNovel"
|
||||
searchText={searchText}
|
||||
setSearchText={setSearchText}
|
||||
getSearchResults={getSearchResults}
|
||||
getNovels={getNovels}
|
||||
setLoading={setLoading}
|
||||
onFilter={() =>
|
||||
bottomSheetRef.current.show({ velocity: -1.5 })
|
||||
}
|
||||
/>
|
||||
{loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator
|
||||
size={60}
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.list}
|
||||
numColumns={itemsPerRow}
|
||||
key={itemsPerRow}
|
||||
data={novels}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() => {
|
||||
navigation.navigate("NovelItem", {
|
||||
novelName: item.novelName,
|
||||
novelCover: item.novelCover,
|
||||
novelUrl: item.novelUrl,
|
||||
sourceId: item.extensionId,
|
||||
});
|
||||
}}
|
||||
libraryStatus={
|
||||
checkIFInLibrary(item.novelUrl)
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
novels.length === 0 && (
|
||||
<EmptyView
|
||||
icon="(; ̄Д ̄)"
|
||||
description="No results found"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Portal>
|
||||
<BottomSheet
|
||||
bottomSheetRef={bottomSheetRef}
|
||||
setSort={setSort}
|
||||
sort={sort}
|
||||
setLoading={setLoading}
|
||||
/>
|
||||
</Portal>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoxNovel;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
},
|
||||
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const BottomSheet = ({ bottomSheetRef, setSort, sort, setLoading }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
const sorting = [
|
||||
{ label: "Latest", sortFlag: "latest" },
|
||||
{ label: "A-Z", sortFlag: "aplhabet" },
|
||||
{ label: "Rating", sortFlag: "rating" },
|
||||
{ label: "Trending", sortFlag: "trending" },
|
||||
{ label: "Most Views", sortFlag: "views" },
|
||||
{ label: "New", sortFlag: "new-manga" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 370, bottom: 0 }}
|
||||
snappingPoints={[0, 370]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.bottomSheetHandle,
|
||||
{ backgroundColor: theme.textColorPrimary },
|
||||
]}
|
||||
/>
|
||||
<List.Section
|
||||
title="Sort"
|
||||
titleStyle={{
|
||||
color: theme.colorAccentDark,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
style={{ color: theme.colorAccentDark }}
|
||||
expanded
|
||||
>
|
||||
{sorting.map((item, index) => (
|
||||
<List.Item
|
||||
key={index}
|
||||
right={() =>
|
||||
sort === item.sortFlag && (
|
||||
<List.Icon
|
||||
icon="arrow-up"
|
||||
color={theme.colorAccentDark}
|
||||
style={styles.sortArrow}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title={item.label}
|
||||
titleStyle={{
|
||||
fontSize: 15,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
onPress={() => {
|
||||
setLoading(true);
|
||||
setSort(item.sortFlag);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</List.Section>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
borderTopRightRadius: 8,
|
||||
borderTopLeftRadius: 8,
|
||||
},
|
||||
bottomSheetHandle: {
|
||||
opacity: 0.5,
|
||||
height: 4,
|
||||
width: 32,
|
||||
borderRadius: 50,
|
||||
top: 8,
|
||||
alignSelf: "center",
|
||||
},
|
||||
sortArrow: {
|
||||
marginVertical: 0,
|
||||
paddingVertical: 0,
|
||||
},
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
import { Provider } from "react-native-paper";
|
||||
|
||||
import NovelCover from "../../../components/common/NovelCover";
|
||||
import { SearchAppbar } from "../../../components/common/Appbar";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import EmptyView from "../../../components/common/EmptyView";
|
||||
|
||||
const FastNovel = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
const library = useSelector((state) => state.libraryReducer.novels);
|
||||
const itemsPerRow = useSelector(
|
||||
(state) => state.settingsReducer.itemsPerRow
|
||||
);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [novels, setNovels] = useState();
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const getNovels = () => {
|
||||
fetch(`https://lnreader-extensions.herokuapp.com/api/3/novels/`)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getSearchResults = (searchText) => {
|
||||
setLoading(true);
|
||||
fetch(
|
||||
`https://lnreader-extensions.herokuapp.com/api/3/search/?s=${searchText}`
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const checkIFInLibrary = (id) => {
|
||||
return library.some((obj) => obj.novelUrl === id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getNovels();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<SearchAppbar
|
||||
screen="Extension"
|
||||
placeholder="Search FastNovel"
|
||||
searchText={searchText}
|
||||
setSearchText={setSearchText}
|
||||
getSearchResults={getSearchResults}
|
||||
getNovels={getNovels}
|
||||
setLoading={setLoading}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator
|
||||
size={60}
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
numColumns={itemsPerRow}
|
||||
key={itemsPerRow}
|
||||
data={novels}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() =>
|
||||
navigation.navigate("NovelItem", {
|
||||
novelName: item.novelName,
|
||||
novelCover: item.novelCover,
|
||||
novelUrl: item.novelUrl,
|
||||
sourceId: item.extensionId,
|
||||
})
|
||||
}
|
||||
libraryStatus={
|
||||
checkIFInLibrary(item.novelUrl)
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
novels.length === 0 && (
|
||||
<EmptyView
|
||||
icon="(; ̄Д ̄)"
|
||||
description="No results found"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default FastNovel;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
import { Provider } from "react-native-paper";
|
||||
|
||||
import NovelCover from "../../../components/common/NovelCover";
|
||||
import { SearchAppbar } from "../../../components/common/Appbar";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import EmptyView from "../../../components/common/EmptyView";
|
||||
|
||||
const FastNovel = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
const library = useSelector((state) => state.libraryReducer.novels);
|
||||
const itemsPerRow = useSelector(
|
||||
(state) => state.settingsReducer.itemsPerRow
|
||||
);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [novels, setNovels] = useState();
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const getNovels = () => {
|
||||
fetch(`https://lnreader-extensions.herokuapp.com/api/4/novels/`)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getSearchResults = (searchText) => {
|
||||
setLoading(true);
|
||||
fetch(
|
||||
`https://lnreader-extensions.herokuapp.com/api/4/search/?s=${searchText}`
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const checkIFInLibrary = (id) => {
|
||||
return library.some((obj) => obj.novelUrl === id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getNovels();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<SearchAppbar
|
||||
screen="Extension"
|
||||
placeholder="Search ReadNovelFull"
|
||||
searchText={searchText}
|
||||
setSearchText={setSearchText}
|
||||
getSearchResults={getSearchResults}
|
||||
getNovels={getNovels}
|
||||
setLoading={setLoading}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator
|
||||
size={60}
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
numColumns={itemsPerRow}
|
||||
key={itemsPerRow}
|
||||
data={novels}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() =>
|
||||
navigation.navigate("NovelItem", {
|
||||
novelName: item.novelName,
|
||||
novelCover: item.novelCover,
|
||||
novelUrl: item.novelUrl,
|
||||
sourceId: item.extensionId,
|
||||
})
|
||||
}
|
||||
libraryStatus={
|
||||
checkIFInLibrary(item.novelUrl)
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
novels.length === 0 && (
|
||||
<EmptyView
|
||||
icon="(; ̄Д ̄)"
|
||||
description="No results found"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default FastNovel;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Fetch novel information from source
|
||||
*/
|
||||
|
||||
export const fetchNovelFromSource = async (extensionId, novelUrl) => {
|
||||
const url = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}`;
|
||||
|
||||
let data = await fetch(url);
|
||||
|
||||
let res = await data.json();
|
||||
|
||||
const novel = {
|
||||
novelUrl: res.novelUrl,
|
||||
novelName: res.novelName,
|
||||
novelCover: res.novelCover,
|
||||
novelSummary: res.novelSummary,
|
||||
"Author(s)": res["Author(s)"],
|
||||
"Genre(s)": res["Genre(s)"],
|
||||
Status: res.Status,
|
||||
sourceUrl: res.sourceUrl,
|
||||
source: res.sourceName,
|
||||
unread: 1,
|
||||
libraryStatus: 0,
|
||||
lastRead: res.novelChapters[0].chapterUrl,
|
||||
lastReadName: res.novelChapters[0].chapterName,
|
||||
extensionId: res.extensionId,
|
||||
novelChapters: res.novelChapters,
|
||||
};
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch novel chapters froms source
|
||||
*/
|
||||
|
||||
export const fetchChaptersFromSource = async (extensionId, novelUrl) => {
|
||||
const url = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}`;
|
||||
|
||||
const data = await fetch(url);
|
||||
let res = await data.json();
|
||||
|
||||
const chapters = res.novelChapters;
|
||||
|
||||
return chapters;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch chapter from source
|
||||
*/
|
||||
|
||||
export const fetchChapterFromSource = async (
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapterUrl
|
||||
) => {
|
||||
const url = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
|
||||
|
||||
let res = await fetch(url);
|
||||
let chapter = await res.json();
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch list of extensions
|
||||
*/
|
||||
|
||||
export const fetchExtensionList = async () => {
|
||||
const url = `https://lnreader-extensions.herokuapp.com/api/`;
|
||||
|
||||
let res = await fetch(url);
|
||||
let extensions = await res.json();
|
||||
|
||||
return extensions;
|
||||
};
|
||||
+63
-2
@@ -1,6 +1,6 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
import { getLibrary } from "../database/queries/LibraryQueries";
|
||||
import { fetchNovel } from "../source/Source";
|
||||
import { fetchChapters, fetchNovel } from "../source/Source";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
import { ToastAndroid } from "react-native";
|
||||
|
||||
@@ -62,6 +62,50 @@ export const updateNovel = async (extensionId, novelUrl, novelId) => {
|
||||
});
|
||||
};
|
||||
|
||||
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) => {
|
||||
// console.log(
|
||||
// "Inserted Chapter Id -> " +
|
||||
// insertId +
|
||||
// " Novel Id " +
|
||||
// novelId
|
||||
// );
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
}
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAllNovels = async () => {
|
||||
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
|
||||
|
||||
@@ -69,8 +113,25 @@ export const updateAllNovels = async () => {
|
||||
|
||||
libraryNovels.map((novel, index) =>
|
||||
setTimeout(async () => {
|
||||
updateNovel(novel.sourceId, novel.novelUrl, novel.novelId);
|
||||
updateNovelChapters(novel.sourceId, novel.novelUrl, novel.novelId);
|
||||
console.log(novel.novelName + " Updated");
|
||||
}, 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;
|
||||
}
|
||||
};
|
||||
|
||||
+16
-3
@@ -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,23 @@ 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}`;
|
||||
|
||||
// console.log(url);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -15,6 +15,8 @@ export const theme = {
|
||||
* Light Theme
|
||||
*/
|
||||
export const lightTheme = {
|
||||
id: 1,
|
||||
name: "Light",
|
||||
colorPrimary: "#FFFFFF",
|
||||
colorPrimaryDark: "#FAFAFA",
|
||||
colorAccentDark: "#2979FF",
|
||||
@@ -23,12 +25,18 @@ export const lightTheme = {
|
||||
textColorHintDark: "rgba(0,0,0,0.38)",
|
||||
rippleColor: "rgba(0,0,0,0.12)",
|
||||
searchBarColor: "#FFFFFF",
|
||||
menuColor: "#FFFFFF",
|
||||
dividerColor: "rgba(0,0,0,0.1)",
|
||||
colorDisabled: "rgba(220,220,220,255)",
|
||||
statusBar: "dark",
|
||||
};
|
||||
|
||||
/**
|
||||
* Dark Theme
|
||||
*/
|
||||
export const darkTheme = {
|
||||
id: 2,
|
||||
name: "Dark",
|
||||
colorPrimary: "#242529",
|
||||
colorPrimaryDark: "#202125",
|
||||
colorAccentDark: "#3399FF",
|
||||
@@ -37,12 +45,18 @@ export const darkTheme = {
|
||||
textColorHintDark: "rgba(255,255,255,0.5)",
|
||||
rippleColor: "rgba(255,255,255,0.2)",
|
||||
searchBarColor: "#303135",
|
||||
menuColor: "#242529",
|
||||
dividerColor: "rgba(255,255,255,0.1)",
|
||||
colorDisabled: "rgba(57,57,57,255)",
|
||||
statusBar: "light",
|
||||
};
|
||||
|
||||
/**
|
||||
* Amoled Dark Theme
|
||||
*/
|
||||
export const amoledDarkTheme = {
|
||||
id: 0,
|
||||
name: "AMOLED Dark",
|
||||
colorPrimary: "#000000",
|
||||
colorPrimaryDark: "#000000",
|
||||
colorAccentDark: "#3399FF",
|
||||
@@ -51,12 +65,18 @@ export const amoledDarkTheme = {
|
||||
textColorHintDark: "rgba(255,255,255,0.5)",
|
||||
rippleColor: "rgba(255,255,255,0.2)",
|
||||
searchBarColor: "#1F1F1F",
|
||||
menuColor: "#242529",
|
||||
dividerColor: "rgba(255,255,255,0.15)",
|
||||
colorDisabled: "rgba(33,33,33,255)",
|
||||
statusBar: "light",
|
||||
};
|
||||
|
||||
/**
|
||||
* Midnight Dusk Theme
|
||||
*/
|
||||
export const midnightDuskTheme = {
|
||||
id: 3,
|
||||
name: "Midnight Dusk",
|
||||
colorPrimary: "#201F27",
|
||||
colorPrimaryDark: "#16151D",
|
||||
colorAccentDark: "#F02475",
|
||||
@@ -65,4 +85,8 @@ export const midnightDuskTheme = {
|
||||
textColorHintDark: "rgba(255,255,255,0.5)",
|
||||
rippleColor: "rgba(255,255,255,0.2)",
|
||||
searchBarColor: "#201F27",
|
||||
menuColor: "#201F27",
|
||||
dividerColor: "rgba(255,255,255,0.1)",
|
||||
colorDisabled: "rgba(57,57,57,255)",
|
||||
statusBar: "light",
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
const fonts = [
|
||||
{ fontFamily: "", name: "Original" },
|
||||
{ fontFamily: "arbutus-slab", name: "Arbutus Slab" },
|
||||
{ fontFamily: "domine", name: "Domine" },
|
||||
{ fontFamily: "lato", name: "Lato" },
|
||||
{ fontFamily: "noto-sans", name: "Noto Sans" },
|
||||
{ fontFamily: "open-sans", name: "Open Sans" },
|
||||
{ fontFamily: "pt-serif", name: "PT Serif" },
|
||||
];
|
||||
|
||||
export { fonts };
|
||||
Reference in New Issue
Block a user