Compare commits

...

57 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 381c779c36 Release v1.0.16 2021-05-03 22:06:08 +05:30
Rajarshee Chatterjee 90df6b5aca Refactor 2021-05-03 20:49:06 +05:30
Rajarshee Chatterjee 87ff17d5e5 Use Expo StorageAccessFramework for backups 2021-05-01 17:51:50 +05:30
Rajarshee Chatterjee 913d319222 Check github for new release (closes #9) 2021-05-01 17:24:33 +05:30
Rajarshee Chatterjee cca143383e Upgrade Expo SDK to 41 (closes #7) 2021-05-01 08:38:06 +05:30
Rajarshee Chatterjee 4a874b30be Move useTheme hook to Hooks folder 2021-05-01 08:11:50 +05:30
Rajarshee Chatterjee e958b0fa09 Refactor 2021-05-01 07:57:16 +05:30
Rajarshee Chatterjee a484d550a8 Fix navigation to novel from history item 2021-04-30 23:35:48 +05:30
Rajarshee Chatterjee 4acafdb741 Add backup/restore (closes #6) 2021-04-30 23:18:09 +05:30
Rajarshee Chatterjee 1e6c4758ff Check github for new release on launch 2021-04-30 20:52:05 +05:30
Rajarshee Chatterjee 1ed0e8b4c4 Fix tracker crashes and update app version 2021-04-30 12:01:34 +05:30
Rajarshee Chatterjee 458cdb2c2a Add update chapter progress after reading 2021-04-30 11:16:14 +05:30
Rajarshee Chatterjee 0a476ba6b4 Update api url 2021-04-30 11:15:58 +05:30
Rajarshee Chatterjee 6e85909f6f Finish mal tracking and refactor 2021-04-29 20:36:32 +05:30
Rajarshee Chatterjee 5642122a5e Add updating mal list logic 2021-04-28 22:33:41 +05:30
Rajarshee Chatterjee 77481dfc06 Add MAL tracking logic 2021-04-27 20:33:22 +05:30
Rajarshee Chatterjee a870442022 Remove unused dependencies 2021-04-26 20:43:48 +05:30
Rajarshee Chatterjee a9a176297e Use single screen for all extensions 2021-04-26 18:34:12 +05:30
Rajarshee Chatterjee f4c0a647fb Add fonts in reader 2021-04-26 17:02:36 +05:30
Rajarshee Chatterjee 06e7e45df0 Fix theme name 2021-04-26 12:07:19 +05:30
Rajarshee Chatterjee c538bf4956 Add logo in more screen 2021-04-26 12:06:59 +05:30
Rajarshee Chatterjee e1679a41f1 Fix navigating to chapter from history screen 2021-04-26 12:05:35 +05:30
Rajarshee Chatterjee 3c2f4a59b7 Refactor settings screen 2021-04-26 11:34:04 +05:30
Rajarshee Chatterjee 8c13b1ff12 Add discord link 2021-04-25 19:59:48 +05:30
Rajarshee Chatterjee 45083e5fd1 Fix menu color in light theme 2021-04-25 19:48:49 +05:30
Rajarshee Chatterjee 3b37c1cb33 Disable OTA updates 2021-04-25 08:25:53 +05:30
Rajarshee Chatterjee 48187d7d54 Fix reader padding and update version 2021-04-24 21:55:19 +05:30
Rajarshee Chatterjee 3af0b1afbc Merge branch 'main' of https://github.com/rajarsheechatterjee/LNReader into main 2021-04-24 20:47:48 +05:30
Rajarshee Chatterjee b9695e7f56 Revert git rebase 2021-04-24 20:46:41 +05:30
Rajarshee Chatterjee 7c068aa7d5 Add download all chapters option (fixes #4) 2021-04-24 20:32:56 +05:30
Rajarshee Chatterjee 8b182f1e31 Add download all chapters option (fixes #4) 2021-04-24 20:31:38 +05:30
Rajarshee Chatterjee 5029513424 closes #4; Add download all chapters 2021-04-24 20:16:10 +05:30
Rajarshee Chatterjee f4c6a3b4a1 Fix download button animation 2021-04-24 18:03:59 +05:30
Rajarshee Chatterjee 27f5e20107 Minor fixes 2021-04-24 11:45:43 +05:30
Rajarshee Chatterjee c249b52304 Add textAlign and padding in reader settings 2021-04-24 11:45:32 +05:30
Rajarshee Chatterjee c179e0e5bd Fix sort and filter chapters 2021-04-24 10:25:39 +05:30
Rajarshee Chatterjee b78382cc03 Allow navigate to chapter from updates tab 2021-04-24 10:03:22 +05:30
Rajarshee Chatterjee 9df3694c04 Fix start reading button 2021-04-24 09:08:34 +05:30
Rajarshee Chatterjee dfd300c358 Fix library search 2021-04-07 09:21:22 +05:30
Rajarshee Chatterjee 76c108597d Add updates tab and updating library 2021-04-05 11:31:07 +05:30
Rajarshee Chatterjee 23ff02674e Fix crash on updating novel 2021-04-03 14:30:06 +05:30
Rajarshee Chatterjee 55e2371f5d Fix start reading button 2021-04-03 11:36:54 +05:30
Rajarshee Chatterjee 4eb225cdf3 Rewrite database 2021-03-31 09:43:07 +05:30
Rajarshee Chatterjee 3cb41c8bfb Fix history table 2021-03-31 09:35:21 +05:30
Rajarshee Chatterjee 05511e073e Update redux folder structure 2021-03-30 23:39:55 +05:30
Rajarshee Chatterjee 085b9b9eb3 Fix updating novels 2021-03-30 09:17:20 +05:30
Rajarshee Chatterjee 9290d59f77 Create history queries 2021-03-29 15:59:45 +05:30
Rajarshee Chatterjee c0345ae9a9 Add downloaded and unread badges 2021-03-29 12:17:37 +05:30
Rajarshee Chatterjee c12a7f378a Move out database logic 2021-03-29 08:56:24 +05:30
Rajarshee Chatterjee d3c2d130e1 Create table and index queries 2021-03-26 09:34:25 +05:30
Rajarshee Chatterjee 134c72a683 Create database version 2 2021-03-26 08:39:39 +05:30
Rajarshee Chatterjee 4c326e35c0 Add updating a novel 2021-03-23 21:11:57 +05:30
Rajarshee Chatterjee 419420efd1 Add items per row settings 2021-03-23 18:34:58 +05:30
Rajarshee Chatterjee 871fc905ce Fix sorting and filter options 2021-03-23 07:59:08 +05:30
Rajarshee Chatterjee 5a8c652f68 Move delete chapter logic into redux 2021-03-22 21:34:04 +05:30
Rajarshee Chatterjee 97f2f84fa5 Move download chapter logic into redux 2021-03-22 20:52:39 +05:30
Rajarshee Chatterjee 65a3e8fd2d Fix statusbar 2021-03-22 14:23:32 +05:30
127 changed files with 7653 additions and 4485 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

+30 -46
View File
@@ -1,58 +1,42 @@
import React, { useState, useEffect } from "react";
import { StatusBar } from "expo-status-bar";
import React, { useEffect } from "react";
import { NavigationContainer } from "@react-navigation/native";
import * as Font from "expo-font";
import AppLoading from "expo-app-loading";
import { useFonts } from "expo-font";
import { Provider as PaperProvider } from "react-native-paper";
import Main from "./src/Navigators/Main";
import Router from "./src/navigation/Router";
import { createTables, createIndexes } from "./src/services/db";
import { setStatusBar } from "./src/services/asyncStorage";
// Redux
import { Provider } from "react-redux";
import { persistor, store } from "./src/redux/store/configureStore";
import { persistor, store } from "./src/redux/store";
import { PersistGate } from "redux-persist/lib/integration/react";
const getFonts = () =>
Font.loadAsync({
"pt-sans-bold": require("./assets/fonts/PTSansNarrow-Bold.ttf"),
});
import { createDB, deleteDb } from "./src/database/DBHelper";
const App = () => {
const [fontsLoaded, setFontsLoaded] = useState(false);
const [loaded] = useFonts({
"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"),
});
setStatusBar();
useEffect(() => createDB(), []);
const createDb = () => {
createTables();
createIndexes();
};
useEffect(() => {
createDb();
}, []);
if (fontsLoaded) {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<StatusBar />
<NavigationContainer>
<Router />
</NavigationContainer>
</PersistGate>
</Provider>
);
} else {
return (
<AppLoading
startAsync={getFonts}
onError={() => warn(error)}
onFinish={() => setFontsLoaded(true)}
/>
);
if (!loaded) {
return null;
}
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<PaperProvider>
<NavigationContainer>
<Main />
</NavigationContainer>
</PaperProvider>
</PersistGate>
</Provider>
);
};
export default App;
+4 -2
View File
@@ -2,7 +2,8 @@
"expo": {
"name": "LNReader",
"slug": "LNReader",
"version": "1.0.0",
"scheme": "lnreader",
"version": "1.0.16",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
@@ -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.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+2118 -1883
View File
File diff suppressed because it is too large Load Diff
+56 -50
View File
@@ -1,52 +1,58 @@
{
"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-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",
"rn-sliding-up-panel": "^2.4.4"
},
"devDependencies": {
"@babel/core": "~7.9.0"
},
"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.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/slider": "3.0.3",
"@react-navigation/material-bottom-tabs": "^5.3.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "^41.0.1",
"expo-app-loading": "^1.0.1",
"expo-document-picker": "^9.1.2",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-media-library": "~12.0.2",
"expo-permissions": "^12.0.1",
"expo-sharing": "~9.1.2",
"expo-sqlite": "~9.1.0",
"expo-status-bar": "~1.0.4",
"expo-web-browser": "~9.1.0",
"moment": "^2.29.1",
"qs": "^6.10.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
"react-native-collapsible-header-views": "^1.1.2",
"react-native-gesture-handler": "~1.10.2",
"react-native-offline": "^5.7.0",
"react-native-paper": "^4.5.0",
"react-native-picker-scrollview": "^1.0.1",
"react-native-reanimated": "~2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "~0.13.12",
"react-native-webview": "11.2.3",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4"
},
"devDependencies": {
"@babel/core": "~7.9.0"
},
"private": true
}
+46
View File
@@ -0,0 +1,46 @@
import { useState, useEffect } from "react";
export const githubUpdateChecker = () => {
const latestReleaseUrl =
"https://api.github.com/repos/rajarsheechatterjee/lnreader/releases/latest";
const [checking, setChecking] = useState(true);
const [latestRelease, setLatestRelease] = useState();
const checkForRelease = async () => {
let res = await fetch(latestReleaseUrl);
res = await res.json();
const release = {
tag_name: res.tag_name,
body: res.body,
downloadUrl: res.assets[0].browser_download_url,
};
setLatestRelease(release);
setChecking(false);
};
const isNewVersion = (versionTag) => {
let currentVersion = "v1.0.16";
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, "");
currentVersion = currentVersion.replace(regex, "");
return newVersion !== currentVersion;
};
useEffect(() => {
checkForRelease();
}, []);
if (!checking) {
const data = {
latestRelease,
isNewVersion: isNewVersion(latestRelease.tag_name),
};
return data;
}
};
+7
View File
@@ -0,0 +1,7 @@
import { setStatusBarStyle as setExpoStatusBarStyle } from "expo-status-bar";
import { useTheme } from "../Hooks/useTheme";
export const setStatusBarStyle = () => {
const theme = useTheme();
setExpoStatusBarStyle(theme.id === 1 ? "dark" : "light");
};
+5
View File
@@ -0,0 +1,5 @@
import { ToastAndroid } from "react-native";
export const showToast = (message) => {
ToastAndroid.show(message, ToastAndroid.SHORT);
};
+7
View File
@@ -0,0 +1,7 @@
import { useSelector } from "react-redux";
export const useLibrary = () => {
const library = useSelector((state) => state.libraryReducer.novels);
return library;
};
+7
View File
@@ -0,0 +1,7 @@
import { useSelector } from "react-redux";
export const useReaderSettings = () => {
const readerSettings = useSelector((state) => state.settingsReducer.reader);
return readerSettings;
};
+7
View File
@@ -0,0 +1,7 @@
import { useSelector } from "react-redux";
export const useTheme = () => {
const currentTheme = useSelector((state) => state.themeReducer.theme);
return currentTheme;
};
+92
View File
@@ -0,0 +1,92 @@
import React from "react";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { useTheme } from "../Hooks/useTheme";
import Library from "../Screens/Library/Library";
import Updates from "../Screens/Updates/Updates";
import History from "../Screens/History/History";
import Browse from "../Screens/Browse/Browse";
import More from "../Screens/More/More";
const Tab = createMaterialBottomTabNavigator();
const BottomNavigator = () => {
const theme = useTheme();
return (
<Tab.Navigator
shifting={false}
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccentDark}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="History"
component={History}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="history"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
/>
</Tab.Navigator>
);
};
export default BottomNavigator;
+61
View File
@@ -0,0 +1,61 @@
import React from "react";
import {
createStackNavigator,
TransitionPresets,
} from "@react-navigation/stack";
import BottomNavigator from "./BottomNavigator";
import Novel from "../Screens/Novel/Novel";
import Chapter from "../Screens/Chapter/Chapter";
import Extension from "../Screens/Extension/Extension";
import MoreStack from "./More";
import { setStatusBarStyle } from "../Hooks/setStatusBarStyle";
import { View } from "react-native";
import { useTheme } from "../Hooks/useTheme";
import { githubUpdateChecker } from "../Hooks/githubUpdateChecker";
import NewUpdateDialog from "../Components/NewUpdateDialog";
const Stack = createStackNavigator();
const stackNavigatorConfig = {
headerShown: false,
};
const MainNavigator = () => {
const theme = useTheme();
setStatusBarStyle();
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimary }}>
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen
name="BottomNavigator"
component={BottomNavigator}
/>
<Stack.Screen
name="Novel"
component={Novel}
options={{
headerTitle: "",
headerShown: true,
headerTransparent: true,
headerTintColor: "white",
...TransitionPresets.RevealFromBottomAndroid,
}}
/>
<Stack.Screen
name="Chapter"
component={Chapter}
options={{ ...TransitionPresets.SlideFromRightIOS }}
/>
<Stack.Screen name="MoreStack" component={MoreStack} />
<Stack.Screen name="Extension" component={Extension} />
</Stack.Navigator>
</View>
);
};
export default MainNavigator;
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import About from "../Screens/More/About";
import Settings from "../Screens/More/Settings";
import Trackers from "../Screens/More/Trackers";
import { View } from "react-native";
import { useTheme } from "../Hooks/useTheme";
const Stack = createStackNavigator();
const stackNavigatorConfig = { headerShown: false };
const MoreStack = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimary }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen name="Trackers" component={Trackers} />
<Stack.Screen name="About" component={About} />
</Stack.Navigator>
</View>
);
};
export default MoreStack;
@@ -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);
@@ -59,17 +61,22 @@ export const SearchAppbar = ({
}}
>
<View style={{ flex: 1, flexDirection: "row" }}>
<IconButton
icon={screen === "Library" ? "magnify" : "arrow-left"}
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
onPress={() => {
if (screen === "Extension") {
navigation.goBack();
}
}}
/>
{screen === "Library" ? (
<IconButton
icon="magnify"
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
/>
) : (
<IconButton
icon="arrow-left"
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
onPress={() => navigation.goBack()}
/>
)}
<TextInput
ref={searchRef}
style={{
@@ -105,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}
+83
View File
@@ -0,0 +1,83 @@
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,
}}
/>
);
const InfoItem = ({ title, description, icon, onPress, theme, right }) => (
<List.Item
title={title}
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
titleNumberOfLines={5}
left={() =>
icon && (
<List.Icon
color={theme.textColorSecondary}
icon={icon}
style={{ marginVertical: 0 }}
/>
)
}
/>
);
export { ListSection, ListSubHeader, ListItem, Divider, InfoItem };
const styles = StyleSheet.create({
listSection: {
flex: 1,
marginVertical: 0,
},
listSubHeader: {
paddingBottom: 5,
},
});
+80
View File
@@ -0,0 +1,80 @@
import React, { useState } from "react";
import { StyleSheet, View, Text } from "react-native";
import { Portal, Modal, Button } from "react-native-paper";
import * as Linking from "expo-linking";
import { useTheme } from "../Hooks/useTheme";
const NewUpdateDialog = ({ newVersion }) => {
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
const theme = useTheme();
return (
<Portal>
<Modal
visible={newUpdateDialog}
onDismiss={() => showNewUpdateDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Text
style={[
styles.modalHeader,
{ color: theme.textColorPrimary },
]}
>
{`New update available ${newVersion.tag_name}`}
</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 16 }}>
{newVersion.body}
</Text>
<View
style={{
flexDirection: "row",
marginTop: 30,
justifyContent: "flex-end",
}}
>
<Button
labelStyle={{
color: theme.colorAccentDark,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccentDark } }}
onPress={() => showNewUpdateDialog(false)}
>
Cancel
</Button>
<Button
labelStyle={{
color: theme.colorAccentDark,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccentDark } }}
onPress={() => Linking.openURL(newVersion.downloadUrl)}
>
Install
</Button>
</View>
</Modal>
</Portal>
);
};
export default NewUpdateDialog;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
modalHeader: {
fontSize: 20,
marginBottom: 10,
},
});
+274
View File
@@ -0,0 +1,274 @@
import React from "react";
import { StyleSheet, View, Text, ImageBackground, Image } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { TouchableRipple } from "react-native-paper";
import { useSelector } from "react-redux";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const displayMode = useSelector(
(state) => state.settingsReducer.displayMode
);
const theme = useSelector((state) => state.themeReducer.theme);
const itemsPerRow = useSelector(
(state) => state.settingsReducer.itemsPerRow
);
const height = {
1: 550,
2: 280,
3: 180,
4: 140,
5: 100,
6: 80,
7: 70,
8: 58,
9: 55,
10: 50,
};
return displayMode !== 2 ? (
<View style={{ flex: 1 / itemsPerRow }}>
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.opac}
onPress={onPress}
>
<>
{item.novelCover && (
<ImageBackground
source={{ uri: item.novelCover }}
style={{ height: height[itemsPerRow] }}
imageStyle={[
{ borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
]}
progressiveRenderingEnabled={true}
>
<View style={{ flexDirection: "row", margin: 4 }}>
{item.chaptersDownloaded && (
<Text
style={[
{
flex: 0,
backgroundColor: "#47a84a",
color: "#FFFFFF",
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
alignSelf: "flex-start",
fontSize: 12,
},
!item.chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
]}
>
{item.chaptersDownloaded}
</Text>
)}
{item.chaptersUnread && (
<Text
style={[
{
flex: 0,
backgroundColor: "#2979FF",
color: "#FFFFFF",
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
alignSelf: "flex-start",
fontSize: 12,
},
!item.chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
]}
>
{item.chaptersUnread}
</Text>
)}
</View>
{displayMode === 0 && (
<View style={styles.titleContainer}>
<LinearGradient
colors={[
"transparent",
"rgba(0,0,0,0.6)",
]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[
styles.title,
{
color:
"rgba(255,255,255,1)",
},
]}
>
{item.novelName}
</Text>
</LinearGradient>
</View>
)}
</ImageBackground>
)}
{displayMode === 1 && (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{item.novelName}
</Text>
)}
</>
</TouchableRipple>
</View>
) : (
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.listView}
onPress={onPress}
>
<View
style={{
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
<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>
);
};
export default NovelCover;
const styles = StyleSheet.create({
// logo: {
// height: 180,
// },
titleContainer: {
flex: 1,
justifyContent: "flex-end",
borderRadius: 4,
},
title: {
fontFamily: "pt-sans-bold",
fontSize: 14,
padding: 8,
},
linearGradient: {
bottom: -1,
borderRadius: 4,
},
opac: {
paddingHorizontal: 4.5,
paddingVertical: 4,
borderRadius: 4,
},
extensionIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
listView: {
flex: 1,
paddingVertical: 8,
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
borderRadius: 4,
},
});
+36
View File
@@ -0,0 +1,36 @@
import React from "react";
import { StyleSheet, View, Text } from "react-native";
import { RadioButton as MaterialRadioButton } from "react-native-paper";
export const RadioButtonGroup = ({ children, value, onValueChange }) => (
<MaterialRadioButton.Group onValueChange={onValueChange} value={value}>
{children}
</MaterialRadioButton.Group>
);
export const RadioButton = ({ value, label, theme }) => (
<View style={styles.radioButtonContainer}>
<MaterialRadioButton
value={value}
color={theme.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,
},
});
+93
View File
@@ -0,0 +1,93 @@
import React, { useRef } from "react";
import { View, TextInput, StyleSheet } from "react-native";
import { TouchableRipple, IconButton } from "react-native-paper";
import Constants from "expo-constants";
export const Searchbar = ({
theme,
placeholder,
searchText,
left,
onPressLeft,
right,
onPressRight,
clearSearchbar,
onChangeText,
onSubmitEditing,
}) => {
const searchRef = useRef(null);
return (
<TouchableRipple
borderless
onPress={() => searchRef.current.focus()}
style={[
styles.searchAppbarContainer,
{ backgroundColor: theme.searchBarColor },
]}
>
<View style={styles.container}>
<View style={{ flex: 1, flexDirection: "row" }}>
{left && (
<IconButton
icon={left}
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
onPress={onPressLeft}
/>
)}
<TextInput
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorSecondary,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
defaultValue={searchText}
/>
</View>
{searchText !== "" && (
<IconButton
icon="close"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={clearSearchbar}
/>
)}
{right && (
<IconButton
icon="earth"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={onPressRight}
/>
)}
</View>
</TouchableRipple>
);
};
const styles = StyleSheet.create({
searchAppbarContainer: {
marginTop: Constants.statusBarHeight + 8,
height: 48,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
marginBottom: 12,
borderRadius: 8,
marginHorizontal: 12,
elevation: 2,
},
container: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
},
});
-111
View File
@@ -1,111 +0,0 @@
import React from "react";
import { StyleSheet, View, Text, ImageBackground } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { TouchableRipple } from "react-native-paper";
import { useSelector } from "react-redux";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const displayMode = useSelector(
(state) => state.settingsReducer.displayMode
);
const theme = useSelector((state) => state.themeReducer.theme);
// const itemsPerRow = useSelector(
// (state) => state.settingsReducer.itemsPerRow
// );
return (
<View style={{ flex: 1 / 3 }}>
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.opac}
onPress={onPress}
>
<>
{item.novelCover && (
<ImageBackground
source={{ uri: item.novelCover }}
style={styles.logo}
imageStyle={[
{ borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
]}
progressiveRenderingEnabled={true}
>
{displayMode === 0 && (
<View style={styles.titleContainer}>
<LinearGradient
colors={[
"transparent",
"rgba(0,0,0,0.6)",
]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[
styles.title,
{
color:
"rgba(255,255,255,1)",
},
]}
>
{item.novelName}
</Text>
</LinearGradient>
</View>
)}
</ImageBackground>
)}
{displayMode === 1 && (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{item.novelName}
</Text>
)}
</>
</TouchableRipple>
</View>
);
};
export default NovelCover;
const styles = StyleSheet.create({
logo: {
height: 180,
},
titleContainer: {
flex: 1,
justifyContent: "flex-end",
borderRadius: 4,
},
title: {
fontFamily: "pt-sans-bold",
fontSize: 14,
padding: 8,
},
linearGradient: {
bottom: -1,
borderRadius: 4,
},
opac: {
paddingHorizontal: 4.5,
paddingVertical: 4,
borderRadius: 4,
},
});
-40
View File
@@ -1,40 +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",
1: "Comfortable",
};
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}
/>
);
};
+55
View File
@@ -0,0 +1,55 @@
import * as SQLite from "expo-sqlite";
import {
createNovelTableQuery,
createUrlIndexQuery,
createLibraryIndexQuery,
} from "./tables/NovelTable";
import {
createChapterTableQuery,
createNovelIdIndexQuery,
createUnreadChaptersIndexQuery,
} from "./tables/ChapterTable";
import {
createHistoryTableQuery,
createChapterIdIndexQuery,
} from "./tables/HistoryTable";
import { createDownloadTableQuery } from "./tables/DownloadTable";
import {
createUpdatesTableQuery,
updatesSeedDataQuery,
} from "./tables/UpdateTable";
/**
* Database Version = 2
*/
const dbName = "lnreader.db";
const db = SQLite.openDatabase(dbName);
export const createDB = () => {
db.transaction((tx) => {
tx.executeSql(createNovelTableQuery);
tx.executeSql(createChapterTableQuery);
tx.executeSql(createHistoryTableQuery);
tx.executeSql(createDownloadTableQuery);
tx.executeSql(createUpdatesTableQuery);
//Db indexes
tx.executeSql(createUrlIndexQuery);
tx.executeSql(createLibraryIndexQuery);
tx.executeSql(createNovelIdIndexQuery);
tx.executeSql(createUnreadChaptersIndexQuery);
tx.executeSql(createChapterIdIndexQuery);
});
};
export const deleteDb = () => {
db.transaction((tx) => {
tx.executeSql("DROP TABLE novels");
tx.executeSql("DROP TABLE chapters");
tx.executeSql("DROP TABLE history");
tx.executeSql("DROP TABLE downloads");
tx.executeSql("DROP TABLE updates");
});
};
+203
View File
@@ -0,0 +1,203 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const insertChaptersQuery = `INSERT INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)`;
export const insertChapters = async (novelId, chapters) => {
db.transaction((tx) => {
chapters.map(
(chapter) =>
tx.executeSql(
insertChaptersQuery,
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
novelId,
],
(txObj, res) => {},
(txObj, error) =>
console.log(
"Error ",
error,
" Chapter URL: ",
chapter.chapterUrl,
" Novel ID ",
novelId
)
),
(txObj, res) => {
console.log("Success");
},
(txObj, error) => console.log("Error ", error)
);
});
};
const getChaptersQuery = (sort, filter) =>
`SELECT * FROM chapters WHERE novelId = ? ${filter ?? ""} ${
sort ?? "ORDER BY chapterId ASC"
}`;
export const getChapters = (novelId, sort, filter) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getChaptersQuery(sort, filter),
[novelId],
(txObj, { rows: { _array } }) => {
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
const getChapterQuery = `SELECT * FROM downloads WHERE downloadChapterId = ?`;
export const getChapter = async (chapterId) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getChapterQuery,
[chapterId],
(tx, results) => {
resolve(results.rows.item(0));
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
const markChapterReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE chapterId = ?`;
export const markChapterRead = async (chapterId) => {
db.transaction((tx) => {
tx.executeSql(
markChapterReadQuery,
[chapterId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
};
const isChapterDownloadedQuery = `SELECT * FROM downloads WHERE downloadChapterId=?`;
export const isChapterDownloaded = async (chapterId) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
isChapterDownloadedQuery,
[chapterId],
(txObj, res) => {
if (res.rows.length !== 0) {
resolve(true);
} else {
resolve(false);
}
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?)`;
export const downloadChapter = async (
extensionId,
novelUrl,
chapterUrl,
chapterId
) => {
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
// console.log(downloadUrl);
const response = await fetch(downloadUrl);
const chapter = await response.json();
db.transaction((tx) => {
tx.executeSql(
`UPDATE chapters SET downloaded = 1 WHERE chapterId = ?`,
[chapterId]
);
tx.executeSql(
downloadChapterQuery,
[
chapterId,
chapter.chapterName,
chapter.chapterText,
chapter.prevChapter,
chapter.nextChapter,
],
(tx, res) =>
console.log(`Downloaded Chapter ${chapter.chapterUrl}`),
(txObj, error) => console.log("Error ", error)
);
});
};
export const deleteChapter = async (chapterId) => {
const updateIsDownloadedQuery = `UPDATE chapters SET downloaded = 0 WHERE chapterId=?`;
const deleteChapterQuery = `DELETE FROM downloads WHERE downloadChapterId=?`;
db.transaction((tx) => {
tx.executeSql(
updateIsDownloadedQuery,
[chapterId],
(tx, res) => console.log(`Chaptermarked not downapdeod deleted`),
(txObj, error) => console.log("Error ", error)
);
tx.executeSql(
deleteChapterQuery,
[chapterId],
(tx, res) => console.log(`Chapter deleted`),
(txObj, error) => console.log("Error ", error)
);
});
};
// export const deleteAllChapters = async (novelId) => {
// const updateIsDownloadedQuery = `UPDATE chapters SET downloaded = 0 WHERE novelId=?`;
// const deleteChapterQuery = `DELETE FROM downloads WHERE downloadChapterId=?`;
// db.transaction((tx) => {
// tx.executeSql(
// updateIsDownloadedQuery,
// [chapterId],
// (tx, res) => console.log(`Chaptermarked not downapdeod deleted`),
// (txObj, error) => console.log("Error ", error)
// );
// tx.executeSql(
// deleteChapterQuery,
// [chapterId],
// (tx, res) => console.log(`Chapter deleted`),
// (txObj, error) => console.log("Error ", error)
// );
// });
// };
const getLastReadChapterQuery = `
SELECT chapters.*
FROM history
JOIN chapters
ON history.historyChapterId = chapters.chapterId
WHERE history.historyNovelId = ?
`;
export const getLastReadChapter = async (novelId) => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
getLastReadChapterQuery,
[novelId],
(tx, { rows }) => resolve(rows.item(0)),
(txObj, error) => console.log("Error ", error)
);
});
});
};
+56
View File
@@ -0,0 +1,56 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const getHistoryQuery = `
SELECT history.*, chapters.*, novels.*
FROM history
JOIN chapters
ON history.historyChapterId = chapters.chapterId
JOIN novels
ON history.historyNovelId = novels.novelId
ORDER BY history.historyTimeRead DESC`;
export const getHistory = async () => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
getHistoryQuery,
null,
(txObj, { rows: { _array } }) => {
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
});
});
};
// const insertHistoryQuery =
export const insertHistory = async (novelId, chapterId) => {
db.transaction((tx) => {
tx.executeSql(
"INSERT OR REPLACE INTO history (historyNovelId, historyChapterId, historyTimeRead) VALUES (?, ?, (datetime('now','localtime')))",
[novelId, chapterId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
tx.executeSql("UPDATE novels SET unread = 0 WHERE novelId = ?", [
novelId,
]);
});
};
export const deleteHistory = async (novelId) => {
db.transaction((tx) => {
tx.executeSql("DELETE FROM history WHERE historyNovelId = ?", [
novelId,
]);
});
};
export const deleteAllHistory = () => {
db.transaction((tx) => {
tx.executeSql("DELETE FROM history; VACCUM;");
});
};
+73
View File
@@ -0,0 +1,73 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const getLibraryQuery = `
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
FROM chapters
WHERE chapters.read = 0
GROUP BY chapters.novelId
) AS C
ON novels.novelId = C.novelId
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
FROM chapters
WHERE chapters.downloaded = 1
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1
GROUP BY novels.novelId
`;
export const getLibrary = () => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getLibraryQuery,
null,
(txObj, { rows: { _array } }) => {
// console.log(_array);
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
// const searchLibraryQuery = ``;
export const searchLibrary = (searchText) => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
`
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
FROM chapters
WHERE chapters.read = 0
GROUP BY chapters.novelId
) AS C
ON novels.novelId = C.novelId
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
FROM chapters
WHERE chapters.downloaded = 1
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%'
GROUP BY novels.novelId
`,
null,
(txObj, { rows: { _array } }) => resolve(_array),
(txObj, error) => console.log("Error ", error)
);
});
});
};
+125
View File
@@ -0,0 +1,125 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import { fetchChapters } from "../../Services/Source/source";
import { insertChapters } from "./ChapterQueries";
const insertNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
export const insertNovel = async (novel) => {
return new Promise((resolve, reject) =>
db.transaction((tx) =>
tx.executeSql(
insertNovelQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
],
(txObj, { insertId }) => resolve(insertId),
(txObj, error) => console.log("Error ", error)
)
)
);
};
export const followNovel = async (followed, novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET followed = ? WHERE novelId = ?",
[!followed, novelId],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
const checkNovelInCacheQuery = `SELECT * FROM novels WHERE novelUrl=? LIMIT 1`;
export const checkNovelInCache = (novelUrl) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
checkNovelInCacheQuery,
[novelUrl],
(txObj, res) => {
if (res.rows.length !== 0) {
resolve(true);
} else {
resolve(false);
}
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
export const getNovel = async (novelUrl) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
`SELECT * FROM novels WHERE novelUrl = ?`,
[novelUrl],
(txObj, { rows }) => resolve(rows.item(0)),
(txObj, error) => console.log("Error ", error)
);
})
);
};
export const deleteNovelCache = () => {
db.transaction((tx) => {
tx.executeSql(
"DELETE FROM novels WHERE followed = 0",
null,
(txObj, res) => {
console.log("Cleared Novel Cache");
},
(txObj, error) => console.log("Error ", error)
);
});
};
const restoreFromBackupQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed, unread) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
export const restoreLibrary = async (novel) => {
return new Promise((resolve, reject) =>
db.transaction((tx) =>
tx.executeSql(
restoreFromBackupQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
novel.followed,
novel.unread,
],
async (txObj, { insertId }) => {
const chapters = await fetchChapters(
novel.sourceId,
novel.novelUrl
);
await insertChapters(insertId, chapters);
},
(txObj, error) => console.log("Error ", error)
)
)
);
};
+37
View File
@@ -0,0 +1,37 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const getUpdatesQuery = `
SELECT chapters.chapterId, chapters.chapterUrl, chapters.chapterName, novels.novelUrl, novels.novelId, novels.novelCover, novels.novelName, novels.sourceId, updates.updateTime, updates.updateId
FROM updates
JOIN chapters
ON updates.chapterId = chapters.chapterId
JOIN novels
ON updates.novelId = novels.novelId
ORDER BY updates.updateTime DESC`;
export const getUpdates = async () => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
getUpdatesQuery,
null,
(txObj, { rows: { _array } }) => {
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
});
});
};
export const deleteNovelUpdates = (novelId) => {
db.transaction((tx) => {
tx.executeSql(
"DELETE FROM updates WHERE novelId = ?",
[novelId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
+18
View File
@@ -0,0 +1,18 @@
export const createChapterTableQuery = `
CREATE TABLE IF NOT EXISTS chapters(
chapterId INTEGER PRIMARY KEY AUTOINCREMENT,
chapterUrl TEXT,
novelId INTEGER NOT NULL,
chapterName TEXT NOT NULL,
releaseDate TEXT,
bookmark BOOLEAN NOT NULL DEFAULT 0,
\`read\` INTEGER NOT NULL DEFAULT 0,
downloaded INTEGER NOT NULL DEFAULT 0,
UNIQUE(novelId, chapterUrl),
FOREIGN KEY (novelId) REFERENCES novels(novelId)
ON DELETE CASCADE
)`;
export const createNovelIdIndexQuery = `CREATE INDEX IF NOT EXISTS chapterNovelIdIndex ON chapters(novelId)`;
export const createUnreadChaptersIndexQuery = `CREATE INDEX IF NOT EXISTS chapterUnreadByNovelIndex ON chapters(novelId, \`read\`)`;
+11
View File
@@ -0,0 +1,11 @@
export const createDownloadTableQuery = `
CREATE TABLE IF NOT EXISTS downloads(
downloadId INTEGER PRIMARY KEY AUTOINCREMENT,
downloadChapterId INTEGER NOT NULL,
chapterName TEXT,
chapterText TEXT,
nextChapter TEXT,
prevChapter TEXT,
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
ON DELETE CASCADE
)`;
+11
View File
@@ -0,0 +1,11 @@
export const createHistoryTableQuery = `
CREATE TABLE IF NOT EXISTS history(
historyId INTEGER PRIMARY KEY AUTOINCREMENT,
historyChapterId INTEGER NOT NULL,
historyNovelId INTEGER NOT NULL UNIQUE,
historyTimeRead DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (historyChapterId) REFERENCES chapters(chapterId)
ON DELETE CASCADE
)`;
export const createChapterIdIndexQuery = `CREATE INDEX IF NOT EXISTS historyChapterIdIndex ON history(historyChapterId)`;
+21
View File
@@ -0,0 +1,21 @@
export const createNovelTableQuery = `
CREATE TABLE IF NOT EXISTS novels(
novelId INTEGER PRIMARY KEY AUTOINCREMENT,
novelUrl TEXT NOT NULL,
sourceUrl TEXT UNIQUE NOT NULL,
sourceId INTEGER NOT NULL,
source TEXT NOT NULL,
novelName TEXT NOT NULL,
novelCover TEXT NOT NULL,
novelSummary TEXT,
author TEXT,
artist TEXT,
status TEXT,
genre TEXT,
followed INTEGER DEFAULT 0,
unread INTEGER DEFAULT 1
)`;
export const createUrlIndexQuery = `CREATE INDEX IF NOT EXISTS novelUrlIndex ON novels(novelUrl)`;
export const createLibraryIndexQuery = `CREATE INDEX IF NOT EXISTS novelFollowedIndex ON novels(followed)`;
+11
View File
@@ -0,0 +1,11 @@
export const createUpdatesTableQuery = `
CREATE TABLE IF NOT EXISTS updates(
updateId INTEGER PRIMARY KEY AUTOINCREMENT,
chapterId INTEGER NOT NULL,
novelId INTEGER NOT NULL,
updateTime DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(novelId, chapterId),
FOREIGN KEY (chapterId) REFERENCES chapters(chapterId),
FOREIGN KEY (novelId) REFERENCES novels(novelId)
ON DELETE CASCADE
)`;
-171
View File
@@ -1,171 +0,0 @@
import React from "react";
import {
createStackNavigator,
TransitionPresets,
} from "@react-navigation/stack";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import { Provider } from "react-native-paper";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import Library from "../screens/Library/Library";
import History from "../screens/History/History";
import Browse from "../screens/Browse/Browse";
import More from "../screens/More/More";
import About from "../screens/More/About";
import Settings from "../screens/More/Settings";
import NovelItem from "../screens/Novel/Novel";
import ChapterItem from "../screens/Chapter/Chapter";
// Extensions
import BoxNovel from "../screens/Extensions/boxnovel/BoxNovel";
import ReadLightNovel from "../screens/Extensions/readlightnovel/ReadLightNovel";
const Stack = createStackNavigator();
const Tab = createMaterialBottomTabNavigator();
const stackNavigatorConfig = {
headerShown: false,
...TransitionPresets.RevealFromBottomAndroid,
};
const MoreStack = () => {
return (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="More" component={More} />
<Stack.Screen name="About" component={About} />
<Stack.Screen name="Settings" component={Settings} />
</Stack.Navigator>
);
};
const BoxNovelStack = () => {
return (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="BoxNovel" component={BoxNovel} />
</Stack.Navigator>
);
};
const ReadLightNovelStack = () => {
return (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="ReadLightNovel" component={ReadLightNovel} />
</Stack.Navigator>
);
};
import { useSelector } from "react-redux";
const BottomNavigator = () => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<Tab.Navigator
shifting={false}
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccentDark}
>
<Tab.Screen
name="My Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
{/*
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/> */}
<Tab.Screen
name="History"
component={History}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="history"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="More"
component={MoreStack}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
/>
</Tab.Navigator>
);
};
const Router = () => {
return (
<Provider>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Router" component={BottomNavigator} />
<Stack.Screen
name="ChapterItem"
component={ChapterItem}
options={{ ...TransitionPresets.SlideFromRightIOS }}
/>
<Stack.Screen
name="NovelItem"
component={NovelItem}
options={{
headerTitle: "",
headerShown: true,
headerTransparent: true,
headerTintColor: "white",
}}
/>
<Stack.Screen name="BoxNovelStack" component={BoxNovelStack} />
<Stack.Screen
name="ReadLightNovelStack"
component={ReadLightNovelStack}
/>
</Stack.Navigator>
</Provider>
);
};
export default Router;
-11
View File
@@ -1,11 +0,0 @@
import { GET_EXTENSIONS } from "./types";
import { fetchExtensionList } from "../../services/api";
export const getExtensions = () => async (dispatch) => {
const res = await fetchExtensionList();
dispatch({
type: GET_EXTENSIONS,
payload: res,
});
};
-38
View File
@@ -1,38 +0,0 @@
import {
UPDATE_NOVEL_HISTORY,
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
} from "./types";
import {
getHistoryFromDb,
insertIntoHistory,
deleteChapterHistory,
} from "../../services/db";
export const getHistory = () => async (dispatch) => {
dispatch({ type: LOAD_HISTORY });
const history = await getHistoryFromDb();
dispatch({ type: GET_HISTORY, payload: history });
};
export const updateNovelHistory = (chapterUrl, chapterName, novelUrl) => async (
dispatch
) => {
await insertIntoHistory(chapterUrl, chapterName, novelUrl);
dispatch({
type: UPDATE_NOVEL_HISTORY,
});
};
export const deleteChapterFromHistory = (novelUrl) => async (dispatch) => {
await deleteChapterHistory(novelUrl);
dispatch({
type: CLEAR_NOVEL_HISTORY,
payload: { novelUrl },
});
};
-20
View File
@@ -1,20 +0,0 @@
import { GET_LIBRARY_NOVELS } from "./types";
import { getLibraryFromDb, searchInLibrary } from "../../services/db";
export const getLibraryNovels = () => async (dispatch) => {
const res = await getLibraryFromDb();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
export const searchLibraryNovels = (searchText) => async (dispatch) => {
const res = await searchInLibrary(searchText);
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
-172
View File
@@ -1,172 +0,0 @@
import { ToastAndroid } from "react-native";
import {
LOADING_NOVEL,
GET_NOVEL,
GET_CHAPTERS,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
} from "./types";
import {
checkNovelInDb,
getChaptersFromDb,
getNovelInfoFromDb,
insertChaptersInDb,
insertNovelInfoInDb,
toggleFavourite,
downloadChapterFromSource,
deleteChapterFromDb,
chapterRead,
isChapterDownloaded,
getChapterFromDb,
} from "../../services/db";
import {
fetchChapterFromSource,
fetchChaptersFromSource,
fetchNovelFromSource,
} from "../../services/api";
export const setNovel = (novel) => async (dispatch) => {
dispatch({ type: SET_NOVEL, payload: novel });
};
export const getNovel = (inLibrary, extensionId, novelUrl) => async (
dispatch
) => {
dispatch({ type: LOADING_NOVEL });
if (inLibrary === 1) {
const chapters = await getChaptersFromDb(novelUrl, "", "");
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({ type: FETCHING_NOVEL });
const inCache = await checkNovelInDb(novelUrl);
if (inCache) {
const novel = await getNovelInfoFromDb(novelUrl);
const chapters = await getChaptersFromDb(novelUrl, "", "");
dispatch({
type: GET_NOVEL,
payload: { novel, chapters },
});
} else {
const novel = await fetchNovelFromSource(extensionId, novelUrl);
const chapters = await fetchChaptersFromSource(
extensionId,
novelUrl
);
dispatch({
type: GET_NOVEL,
payload: { novel, chapters },
});
insertNovelInfoInDb(novel);
insertChaptersInDb(novelUrl, chapters);
}
}
};
export const sortAndFilterChapters = (novelUrl, filter, sort) => async (
dispatch
) => {
const chapters = await getChaptersFromDb(novelUrl, filter, sort);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
};
export const insertNovelInLibrary = (inLibrary, novelUrl) => async (
dispatch
) => {
toggleFavourite(inLibrary, novelUrl);
dispatch({
type: UPDATE_IN_LIBRARY,
payload: !inLibrary,
});
ToastAndroid.show(
!inLibrary ? "Added to library" : "Removed from library",
ToastAndroid.SHORT
);
};
export const getChapter = (extensionId, chapterUrl, novelUrl) => async (
dispatch
) => {
dispatch({ type: CHAPTER_LOADING });
const isDownloaded = await isChapterDownloaded(chapterUrl, novelUrl);
let chapter;
if (isDownloaded) {
chapter = await getChapterFromDb(chapterUrl, novelUrl);
} else {
chapter = await fetchChapterFromSource(
extensionId,
novelUrl,
chapterUrl
);
}
dispatch({ type: GET_CHAPTER, payload: chapter });
};
export const updateChapterRead = (chapterUrl, novelUrl) => async (dispatch) => {
await chapterRead(chapterUrl, novelUrl);
dispatch({ type: CHAPTER_READ, payload: { chapterUrl, novelUrl } });
};
// export const downloadChapter = (
// extensionId,
// novelUrl,
// chapterUrl,
// chapterName
// ) => async (dispatch) => {
// dispatch({
// type: CHAPTER_DOWNLOADING,
// payload: {
// extensionId,
// novelUrl,
// chapterUrl,
// },
// });
// await downloadChapterFromSource(extensionId, novelUrl, chapterUrl);
// dispatch({
// type: CHAPTER_DOWNLOADED,
// payload: { extensionId, novelUrl, chapterUrl },
// });
// ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
// };
// export const deleteChapter = (
// extensionId,
// novelUrl,
// chapterUrl,
// chapterName
// ) => (dispatch) => {
// deleteChapterFromDb(extensionId, novelUrl, chapterUrl);
// dispatch({
// type: CHAPTER_DELETED,
// payload: { extensionId, novelUrl, chapterUrl },
// });
// ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
// };
-15
View File
@@ -1,15 +0,0 @@
import { SWITCH_THEME } from "./types";
/**
* 0 -> Amoled Dark Theme
* 1 -> Light Theme
* 2 -> Dark Theme
* 3 -> Midnight Dusk Theme
*/
export const switchTheme = (val) => (dispatch) => {
dispatch({
type: SWITCH_THEME,
payload: val,
});
};
+79
View File
@@ -0,0 +1,79 @@
import { ToastAndroid } from "react-native";
import {
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
} from "./chapter.types";
import { fetchChapter } from "../../Services/Source/source";
import {
markChapterRead,
isChapterDownloaded,
getChapter,
downloadChapter,
deleteChapter,
} from "../../database/queries/ChapterQueries";
export const getChapterAction = (
extensionId,
chapterUrl,
novelUrl,
novelId
) => async (dispatch) => {
dispatch({ type: CHAPTER_LOADING });
let chapter = await getChapter(novelId);
if (!chapter) {
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
}
console.log(isDownloaded);
dispatch({ type: GET_CHAPTER, payload: chapter });
};
export const markChapterReadAction = (chapterId) => async (dispatch) => {
await markChapterRead(chapterId);
dispatch({ type: CHAPTER_READ, payload: { chapterId } });
};
export const downloadChapterAction = (
extensionId,
novelUrl,
chapterUrl,
chapterName,
chapterId
) => async (dispatch) => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: {
chapterId,
},
});
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
payload: { chapterId },
});
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
export const deleteChapterAction = (chapterId, chapterName) => async (
dispatch
) => {
await deleteChapter(chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: { chapterId },
});
ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
};
+65
View File
@@ -0,0 +1,65 @@
import {
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
} from "./chapter.types";
const initialState = {
chapter: null,
downloading: [],
loading: true,
};
const chapterReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case CHAPTER_READ:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...chapter, read: 1 }
: chapter
),
};
case CHAPTER_LOADING:
return {
...state,
chapterLoading: true,
};
case GET_CHAPTER:
return { ...state, chapter: payload, chapterLoading: false };
case CHAPTER_DOWNLOADING:
return {
...state,
downloading: payload.chapterId,
};
case CHAPTER_DOWNLOADED:
return {
...state,
downloading: null,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...chapter, downloaded: 1 }
: chapter
),
};
case CHAPTER_DELETED:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...chapter, downloaded: 0 }
: chapter
),
};
default:
return state;
}
};
export default chapterReducer;
+6
View File
@@ -0,0 +1,6 @@
export const CHAPTER_DOWNLOADED = "CHAPTER_DOWNLOADED";
export const CHAPTER_DELETED = "CHAPTER_DELETED";
export const CHAPTER_DOWNLOADING = "CHAPTER_DOWNLOADING";
export const CHAPTER_READ = "CHAPTER_READ";
export const CHAPTER_LOADING = "CHAPTER_LOADING";
export const GET_CHAPTER = "GET_CHAPTER";
+36
View File
@@ -0,0 +1,36 @@
import {
UPDATE_NOVEL_HISTORY,
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
} from "./history.types";
import {
getHistory,
insertHistory,
deleteHistory,
} from "../../database/queries/HistoryQueries";
export const getHistoryAction = () => async (dispatch) => {
dispatch({ type: LOAD_HISTORY });
const history = await getHistory();
dispatch({ type: GET_HISTORY, payload: history });
};
export const insertHistoryAction = (novelId, chapterId) => async (dispatch) => {
await insertHistory(novelId, chapterId);
dispatch({
type: UPDATE_NOVEL_HISTORY,
});
};
export const deleteHistoryAction = (novelId) => async (dispatch) => {
await deleteHistory(novelId);
dispatch({
type: CLEAR_NOVEL_HISTORY,
payload: { novelId },
});
};
@@ -3,7 +3,7 @@ import {
GET_HISTORY,
LOAD_HISTORY,
UPDATE_NOVEL_HISTORY,
} from "../actions/types";
} from "./history.types";
const initialState = {
history: [],
@@ -24,7 +24,7 @@ const historyReducer = (state = initialState, action) => {
return {
...state,
history: state.history.filter(
(item) => item.novelUrl !== payload.novelUrl
(item) => item.novelId !== payload.novelId
),
};
// return {
+4
View File
@@ -0,0 +1,4 @@
export const LOAD_HISTORY = "LOAD_HISTORY";
export const GET_HISTORY = "GET_HISTORY";
export const UPDATE_NOVEL_HISTORY = "UPDATE_NOVEL_HISTORY";
export const CLEAR_NOVEL_HISTORY = "CLEAR_NOVEL_HISTORY";
+26
View File
@@ -0,0 +1,26 @@
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
} from "./library.types";
import {
getLibrary,
searchLibrary,
} from "../../database/queries/LibraryQueries";
export const getLibraryAction = () => async (dispatch) => {
const res = await getLibrary();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
export const searchLibraryAction = (searchText) => async (dispatch) => {
const res = await searchLibrary(searchText);
dispatch({
type: GET_LIBRARY_SEARCH_RESULTS,
payload: res,
});
};
+47
View File
@@ -0,0 +1,47 @@
import {
FOLLOW_NOVEL,
UNFOLLOW_NOVEL,
UPDATE_IN_LIBRARY,
} from "../novel/novel.types";
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
} from "./library.types";
const initialState = {
novels: [],
searchResults: [],
loading: true,
};
const libraryReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_LIBRARY_NOVELS:
return { ...state, novels: payload, loading: false };
case GET_LIBRARY_SEARCH_RESULTS:
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, { novelUrl: payload.novelUrl }],
};
default:
return state;
}
};
export default libraryReducer;
+2
View File
@@ -0,0 +1,2 @@
export const GET_LIBRARY_NOVELS = "GET_LIBRARY_NOVELS";
export const GET_LIBRARY_SEARCH_RESULTS = "GET_LIBRARY_SEARCH_RESULTS";
+240
View File
@@ -0,0 +1,240 @@
import { ToastAndroid } from "react-native";
import {
LOADING_NOVEL,
GET_NOVEL,
GET_CHAPTERS,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
UPDATE_NOVEL,
} from "./novel.types";
import { updateNovel } from "../../Services/updates";
import { fetchChapter, fetchNovel } from "../../Services/Source/source";
import {
followNovel,
insertNovel,
getNovel,
} from "../../database/queries/NovelQueries";
import {
getChapters,
insertChapters,
markChapterRead,
isChapterDownloaded,
getChapter,
downloadChapter,
deleteChapter,
} from "../../database/queries/ChapterQueries";
import { deleteNovelUpdates } from "../../database/queries/UpdateQueries";
export const setNovel = (novel) => async (dispatch) => {
dispatch({ type: SET_NOVEL, payload: novel });
};
export const getNovelAction = (
followed,
sourceId,
novelUrl,
novelId,
sort,
filter
) => async (dispatch) => {
dispatch({ type: LOADING_NOVEL });
if (followed === 1) {
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({ type: FETCHING_NOVEL });
const novel = await getNovel(novelUrl);
if (novel) {
novel.chapters = await getChapters(novel.novelId, sort, filter);
dispatch({
type: GET_NOVEL,
payload: novel,
});
} else {
/**
* Fetch novel from source
*/
const fetchedNovel = await fetchNovel(sourceId, novelUrl);
/**
* Insert novel in db
*/
const fetchedNovelId = await insertNovel(fetchedNovel);
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
/**
* Get Novel from db
*/
const novel = await getNovel(novelUrl);
novel.chapters = await getChapters(novel.novelId);
dispatch({
type: GET_NOVEL,
payload: novel,
});
}
}
};
export const sortAndFilterChapters = (novelUrl, filter, sort) => async (
dispatch
) => {
// const chapters = await getChaptersFromDb(novelUrl, filter, sort);
// dispatch({
// type: GET_CHAPTERS,
// payload: chapters,
// });
};
export const followNovelAction = (novel) => async (dispatch) => {
await followNovel(novel.followed, novel.novelId);
if (!novel.followNovel) {
deleteNovelUpdates(novel.novelId);
}
dispatch({
type: UPDATE_IN_LIBRARY,
payload: { novelUrl: novel.novelUrl, followed: !novel.followed },
});
ToastAndroid.show(
!novel.followed ? "Added to library" : "Removed from library",
ToastAndroid.SHORT
);
};
export const getChapterAction = (
extensionId,
chapterUrl,
novelUrl,
novelId
) => async (dispatch) => {
dispatch({ type: CHAPTER_LOADING });
const isDownloaded = await isChapterDownloaded(novelId);
let chapter;
if (isDownloaded) {
chapter = await getChapter(novelId);
} else {
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
}
dispatch({ type: GET_CHAPTER, payload: chapter });
};
export const markChapterReadAction = (chapterId) => async (dispatch) => {
await markChapterRead(chapterId);
dispatch({ type: CHAPTER_READ, payload: { chapterId } });
};
export const downloadChapterAction = (
extensionId,
novelUrl,
chapterUrl,
chapterName,
chapterId
) => async (dispatch) => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapterId,
});
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapterId,
});
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
export const downloadAllChaptersAction = (
extensionId,
novelUrl,
chapters
) => async (dispatch) => {
await chapters.map((chapter, index) => {
setTimeout(async () => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapter.chapterId,
});
if (!chapter.downloaded) {
await downloadChapter(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterId
);
}
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapter.chapterId,
});
}, 1000 * index);
});
ToastAndroid.show(`All chapters downloaded`, ToastAndroid.SHORT);
};
export const deleteChapterAction = (chapterId, chapterName) => async (
dispatch
) => {
await deleteChapter(chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: chapterId,
});
ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
};
export const deleteAllChaptersAction = (chapters) => async (dispatch) => {
await chapters.map((chapter) => {
deleteChapter(chapter.chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: chapter.chapterId,
});
});
ToastAndroid.show(`Deleted all chapters`, ToastAndroid.SHORT);
};
export const updateNovelAction = (extensionId, novelUrl, novelId) => async (
dispatch
) => {
dispatch({ type: FETCHING_NOVEL });
await updateNovel(extensionId, novelUrl, novelId);
let novel = await getNovel(novelUrl);
let chapters = await getChapters(novel.novelId);
dispatch({
type: UPDATE_NOVEL,
payload: { novel, chapters },
});
};
@@ -11,12 +11,15 @@ import {
UPDATE_IN_LIBRARY,
CHAPTER_READ,
CHAPTER_LOADING,
} from "../actions/types";
UPDATE_NOVEL,
UPDATE_LAST_READ,
} from "./novel.types";
const initialState = {
novel: null,
chapters: [],
chapter: null,
downloading: [],
chapterLoading: true,
loading: true,
fetching: false,
@@ -29,13 +32,13 @@ const novelReducer = (state = initialState, action) => {
case LOADING_NOVEL:
return { ...state, loading: true };
case FETCHING_NOVEL:
return { ...state, loading: true, fetching: true };
return { ...state, fetching: true };
case SET_NOVEL:
return { ...state, novel: payload };
case GET_NOVEL:
return {
...state,
novel: payload.novel,
novel: payload,
chapters: payload.chapters,
loading: false,
fetching: false,
@@ -50,15 +53,21 @@ const novelReducer = (state = initialState, action) => {
case UPDATE_IN_LIBRARY:
return {
...state,
novel: { ...state.novel, libraryStatus: payload },
novel: { ...state.novel, followed: payload.followed },
};
case UPDATE_NOVEL:
return {
...state,
novel: payload.novel,
chapters: payload.chapters,
loading: false,
fetching: false,
};
case CHAPTER_READ:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterUrl === payload.chapterUrl &&
chapter.novelUrl === payload.novelUrl
chapter.chapterId === payload.chapterId
? { ...chapter, read: 1 }
: chapter
),
@@ -70,36 +79,40 @@ const novelReducer = (state = initialState, action) => {
};
case GET_CHAPTER:
return { ...state, chapter: payload, chapterLoading: false };
// case CHAPTER_DOWNLOADING:
// return {
// ...state,
// chapters: [
// state.chapters.map((chap) =>
// chap.novelUrl === payload.novelUrl &&
// chap.chapterUrl === payload.novelUrl
// ? { ...chap, downloading: true, downloaded: false }
// : chap
// ),
// ],
// };
// case CHAPTER_DOWNLOADED:
// return {
// ...state,
// downloads: state.downloads.map((chap) =>
// chap.chapterUrl === payload.chapterUrl &&
// chap.novelUrl === payload.novelUrl
// ? { ...chap, downloading: false, downloaded: true }
// : chap
// ),
// };
// case CHAPTER_DELETED:
// return {
// downloads: state.downloads.filter(
// (chapter) =>
// chapter.chapterUrl === payload.chapterUrl &&
// chapter.novelUrl === payload.novelUrl
// ),
// };
case CHAPTER_DOWNLOADING:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload
? { ...chapter, downloaded: 3 }
: chapter
),
};
case CHAPTER_DOWNLOADED:
return {
...state,
// downloading: state.downloading.filter((id) => id === payload),
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload
? { ...chapter, downloaded: 1 }
: chapter
),
};
case CHAPTER_DELETED:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload
? { ...chapter, downloaded: 0 }
: chapter
),
};
case UPDATE_LAST_READ:
return {
...state,
novel: { ...state.novel, lastRead: payload },
};
default:
return state;
}
@@ -1,47 +1,19 @@
export const SWITCH_THEME = "SWITCH_THEME";
export const GET_LIBRARY_NOVELS = "GET_LIBRARY_NOVELS";
export const SET_DISPLAY_MODE = "SET_DISPLAY_MODE";
export const SET_ITEMS_PER_ROW = "SET_ITEMS_PER_ROW";
export const GET_EXTENSIONS = "GET_EXTENSIONS";
export const LOADING_NOVEL = "LOADING_NOVEL";
export const FETCHING_NOVEL = "FETCHING_NOVEL";
export const LOADING_CHAPTERS = "LOADING_CHAPTERS";
export const SET_NOVEL = "SET_NOVEL";
export const GET_NOVEL = "GET_NOVEL";
export const SET_NOVEL = "SET_NOVEL";
export const UPDATE_NOVEL = "UPDATE_NOVEL";
export const UPDATE_IN_LIBRARY = "UPDATE_IN_LIBRARY";
export const GET_CHAPTERS = "GET_CHAPTERS";
export const UPDATE_LAST_READ = "UPDATE_LAST_READ";
export const CHAPTER_DOWNLOADED = "CHAPTER_DOWNLOADED";
export const CHAPTER_DELETED = "CHAPTER_DELETED";
export const CHAPTER_DOWNLOADING = "CHAPTER_DOWNLOADING";
export const LOAD_HISTORY = "LOAD_HISTORY";
export const UPDATE_NOVEL_HISTORY = "UPDATE_NOVEL_HISTORY";
export const CLEAR_NOVEL_HISTORY = "CLEAR_NOVEL_HISTORY";
export const GET_HISTORY = "GET_HISTORY";
export const CHAPTER_READ = "CHAPTER_READ";
export const CHAPTER_LOADING = "CHAPTER_LOADING";
export const GET_CHAPTER = "GET_CHAPTER";
export const CHAPTER_LOADING = "CHAPTER_LOADING";
export const UPDATE_READER_THEME = "UPDATE_READER_THEME";
export const UPDATE_READER_TEXT_SIZE = "UPDATE_READER_TEXT_SIZE";
export const FOLLOW_NOVEL = "FOLLOW_NOVEL";
export const UNFOLLOW_NOVEL = "UNFOLLOW_NOVEL";
-17
View File
@@ -1,17 +0,0 @@
const initialState = {
novels: [],
loading: true,
};
const libraryReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case "GET_LIBRARY_NOVELS":
return { novels: payload, loading: false };
default:
return state;
}
};
export default libraryReducer;
-49
View File
@@ -1,49 +0,0 @@
import {
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
} from "../actions/types";
const initialState = {
displayMode: 0,
itemsPerRow: 3,
reader: {
theme: 1,
textSize: 16,
},
};
/**
* Display Mode
* 0 -> Compact
* 1 -> Comfortable
* 2 -> List
*/
/**
* Reader Settings
* theme
* 1 -> Dark
* 2 -> White
* 3 -> Sepia
*/
const settingsReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case SET_DISPLAY_MODE:
return { ...state, displayMode: payload };
case SET_ITEMS_PER_ROW:
return { ...state, itemsPerRow: payload };
case UPDATE_READER_THEME:
return { ...state, reader: { ...state.reader, theme: payload } };
case UPDATE_READER_TEXT_SIZE:
return { ...state, reader: { ...state.reader, textSize: payload } };
default:
return state;
}
};
export default settingsReducer;
@@ -3,7 +3,10 @@ import {
SET_ITEMS_PER_ROW,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
} from "./types";
UPDATE_READER_TEXT_ALIGN,
UPDATE_READER_PADDING,
SET_READER_FONT,
} from "./settings.types";
export const setDisplayMode = (val) => (dispatch) => {
dispatch({
@@ -32,3 +35,24 @@ export const updateReaderTextSize = (val) => (dispatch) => {
payload: val,
});
};
export const updateReaderTextAlign = (val) => (dispatch) => {
dispatch({
type: UPDATE_READER_TEXT_ALIGN,
payload: val,
});
};
export const updateReaderPadding = (val) => (dispatch) => {
dispatch({
type: UPDATE_READER_PADDING,
payload: val,
});
};
export const setReaderFont = (val) => (dispatch) => {
dispatch({
type: SET_READER_FONT,
payload: val,
});
};
+77
View File
@@ -0,0 +1,77 @@
import {
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
SET_READER_FONT,
UPDATE_READER_PADDING,
UPDATE_READER_TEXT_ALIGN,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
} from "./settings.types";
const initialState = {
displayMode: 0,
itemsPerRow: 3,
reader: {
theme: 1,
textSize: 16,
textAlign: "left",
padding: 5,
fontFamily: null,
},
};
/**
* Display Mode
* 0 -> Compact
* 1 -> Comfortable
* 2 -> List
*/
/**
* Reader Settings
* theme
* 1 -> Dark
* 2 -> White
* 3 -> Sepia
*/
const settingsReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case SET_DISPLAY_MODE:
return {
...state,
displayMode: payload,
itemsPerRow: payload === 2 ? 1 : state.itemsPerRow,
};
case SET_ITEMS_PER_ROW:
return {
...state,
itemsPerRow: state.displayMode !== 2 ? payload : 1,
};
case UPDATE_READER_THEME:
return { ...state, reader: { ...state.reader, theme: payload } };
case UPDATE_READER_TEXT_SIZE:
return { ...state, reader: { ...state.reader, textSize: payload } };
case UPDATE_READER_TEXT_ALIGN:
return {
...state,
reader: { ...state.reader, textAlign: payload },
};
case UPDATE_READER_PADDING:
return {
...state,
reader: { ...state.reader, padding: payload },
};
case SET_READER_FONT:
return {
...state,
reader: { ...state.reader, fontFamily: payload },
};
default:
return state;
}
};
export default settingsReducer;
+7
View File
@@ -0,0 +1,7 @@
export const SET_DISPLAY_MODE = "SET_DISPLAY_MODE";
export const SET_ITEMS_PER_ROW = "SET_ITEMS_PER_ROW";
export const UPDATE_READER_THEME = "UPDATE_READER_THEME";
export const UPDATE_READER_TEXT_SIZE = "UPDATE_READER_TEXT_SIZE";
export const UPDATE_READER_TEXT_ALIGN = "UPDATE_READER_TEXT_ALIGN";
export const UPDATE_READER_PADDING = "UPDATE_READER_PADDING";
export const SET_READER_FONT = "SET_READER_FONT";
+11
View File
@@ -0,0 +1,11 @@
import { GET_SOURCES } from "./source.types";
import { fetchSources } from "../../Services/Source/source";
export const getSourcesAction = () => async (dispatch) => {
const res = await fetchSources();
dispatch({
type: GET_SOURCES,
payload: res,
});
};
@@ -1,4 +1,4 @@
import { GET_EXTENSIONS } from "../actions/types";
import { GET_SOURCES } from "./source.types";
const initialState = {
extensions: [],
@@ -9,7 +9,7 @@ const libraryReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_EXTENSIONS:
case GET_SOURCES:
return { extensions: payload, loading: false };
default:
return state;
+1
View File
@@ -0,0 +1 @@
export const GET_SOURCES = "GET_SOURCES";
@@ -3,12 +3,14 @@ import { persistStore, persistReducer } from "redux-persist";
import AsyncStorage from "@react-native-async-storage/async-storage";
import thunk from "redux-thunk";
import themeReducer from "../reducers/theme";
import settingsReducer from "../reducers/settings";
import libraryReducer from "../reducers/library";
import extensionReducer from "../reducers/extension";
import novelReducer from "../reducers/novel";
import historyReducer from "../reducers/history";
import themeReducer from "./theme/theme.reducer";
import settingsReducer from "./settings/settings.reducer";
import libraryReducer from "./library/library.reducer";
import extensionReducer from "./source/source.reducers";
import 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",
@@ -25,6 +27,8 @@ const persistedReducer = persistReducer(
extensionReducer,
novelReducer,
historyReducer,
updatesReducer,
trackerReducer,
})
);
+8
View File
@@ -0,0 +1,8 @@
import { SWITCH_THEME } from "./theme.types";
export const switchTheme = (val) => (dispatch) => {
dispatch({
type: SWITCH_THEME,
payload: val,
});
};
@@ -3,7 +3,7 @@ import {
lightTheme,
darkTheme,
midnightDuskTheme,
} from "../../theme/theme";
} from "../../Theme/theme";
const themes = {
0: amoledDarkTheme,
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
export const SWITCH_THEME = "SWITCH_THEME";
+57
View File
@@ -0,0 +1,57 @@
import { findListItem, updateItem } from "../../Services/Trackers/myAnimeList";
import {
REMOVE_TRACKER,
SET_TRACKER,
TRACK_NOVEL,
UNTRACK_NOVEL,
UPDATE_TRACKER,
UPDATE_CHAPTERS_READ,
} from "./tracker.types";
import { updateMalChaptersRead } from "../../Services/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 },
});
};
+81
View File
@@ -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;
+9
View File
@@ -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";
+20
View File
@@ -0,0 +1,20 @@
import { GET_UPDATES } from "./updates.types";
import { getUpdates } from "../../database/queries/UpdateQueries";
import { updateAllNovels } from "../../Services/updates";
import { showToast } from "../../Hooks/showToast";
export const getUpdatesAction = () => async (dispatch) => {
const updates = await getUpdates();
dispatch({ type: GET_UPDATES, payload: updates });
};
export const updateLibraryAction = () => async (dispatch) => {
showToast("Updating library");
await updateAllNovels();
const updates = await getUpdates();
dispatch({ type: GET_UPDATES, payload: updates });
};
+21
View File
@@ -0,0 +1,21 @@
import { LOAD_UPDATES, GET_UPDATES } from "./updates.types";
const initialState = {
updates: [],
loading: true,
};
const updateReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case LOAD_UPDATES:
return { ...state, loading: true };
case GET_UPDATES:
return { ...state, updates: payload, loading: false };
default:
return state;
}
};
export default updateReducer;
+2
View File
@@ -0,0 +1,2 @@
export const GET_UPDATES = "GET_UPDATES";
export const LOAD_UPDATES = "LOAD_UPDATES";
+41 -14
View File
@@ -1,16 +1,39 @@
import React, { useEffect } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar } from "../../components/common/Appbar";
import React, { useEffect, useState } from "react";
import {
StyleSheet,
View,
FlatList,
ActivityIndicator,
RefreshControl,
} from "react-native";
import { Appbar } from "../../Components/Appbar";
import { connect } from "react-redux";
import { useTheme } from "../../Hooks/useTheme";
import { getExtensions } from "../../redux/actions/extension";
import { getSourcesAction } from "../../redux/source/source.actions";
import ExtensionCard from "./components/ExtensionCard";
import { showToast } from "../../Hooks/showToast";
const Browse = ({ extensions, loading, getSourcesAction }) => {
const [refreshing, setRefreshing] = useState(false);
const theme = useTheme();
const Browse = ({ theme, extensions, loading, getExtensions }) => {
useEffect(() => {
getExtensions();
}, []);
getSourcesAction();
}, [getSourcesAction]);
const onRefresh = () => {
showToast("Updating extension list");
setRefreshing(true);
getSourcesAction();
setRefreshing(false);
};
const renderExtensionCard = ({ item }) => (
<ExtensionCard item={item} theme={theme} />
);
return (
<>
@@ -24,7 +47,7 @@ const Browse = ({ theme, extensions, loading, getExtensions }) => {
<FlatList
data={extensions}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={({ item }) => <ExtensionCard item={item} />}
renderItem={renderExtensionCard}
ListEmptyComponent={
loading && (
<ActivityIndicator
@@ -34,6 +57,14 @@ const Browse = ({ theme, extensions, loading, getExtensions }) => {
/>
)
}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["white"]}
progressBackgroundColor={theme.colorAccentDark}
/>
}
/>
</View>
</>
@@ -41,16 +72,12 @@ const Browse = ({ theme, extensions, loading, getExtensions }) => {
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
extensions: state.extensionReducer.extensions,
loading: state.extensionReducer.loading,
});
export default connect(mapStateToProps, { getExtensions })(Browse);
export default connect(mapStateToProps, { getSourcesAction })(Browse);
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 4,
},
container: { flex: 1 },
});
+30 -24
View File
@@ -1,28 +1,29 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple, Button } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
const ExtensionCard = ({ item }) => {
const { sourceName, sourceCover, sourceLanguage } = item;
const ExtensionCard = ({ item, theme }) => {
const { sourceName, sourceCover, sourceLanguage, status } = item;
const navigation = useNavigation();
const theme = useSelector((state) => state.themeReducer.theme);
const navigateToExtension = () =>
navigation.navigate("Extension", {
sourceId: item.sourceId,
sourceName: item.sourceName,
sourceUrl: item.sourceUrl,
});
return (
<TouchableRipple
style={styles.extensionCard}
onPress={() => navigation.navigate(sourceName + "Stack")}
onPress={navigateToExtension}
rippleColor={theme.rippleColor}
>
<>
<Image
source={{
uri: sourceCover,
}}
source={{ uri: sourceCover }}
style={styles.extensionIcon}
resizeMode="contain"
/>
@@ -36,25 +37,26 @@ const ExtensionCard = ({ item }) => {
>
{sourceName}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
<View style={{ flexDirection: "row" }}>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
<Text style={styles.sourceStatus}>
{status === 0 && "• Down"}
</Text>
</View>
</View>
<View>
<Button
labelStyle={{
letterSpacing: 0,
}}
labelStyle={{ letterSpacing: 0 }}
uppercase={false}
color={theme.colorAccentDark}
onPress={() =>
navigation.navigate(sourceName + "Stack")
}
onPress={navigateToExtension}
>
Browse
</Button>
@@ -74,7 +76,6 @@ const styles = StyleSheet.create({
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
borderRadius: 6,
},
extensionIcon: {
width: 40,
@@ -88,4 +89,9 @@ const styles = StyleSheet.create({
alignItems: "center",
marginLeft: 16,
},
sourceStatus: {
color: "#C14033",
fontSize: 12,
marginLeft: 5,
},
});
+68 -18
View File
@@ -6,29 +6,53 @@ import { CollapsibleHeaderScrollView } from "react-native-collapsible-header-vie
import BottomSheet from "./components/BottomSheet";
import { updateNovelHistory } from "../../redux/actions/history";
import { getChapter, updateChapterRead } from "../../redux/actions/novel";
import { insertHistoryAction } from "../../redux/history/history.actions";
import {
getChapterAction,
markChapterReadAction,
} from "../../redux/novel/novel.actions";
import { connect } from "react-redux";
import { connect, useSelector, useDispatch } from "react-redux";
import { parseChapterNumber } from "../../Services/updates";
import { updateChaptersRead } from "../../redux/tracker/tracker.actions";
const ChapterItem = ({
route,
navigation,
theme,
updateNovelHistory,
updateChapterRead,
getChapter,
insertHistoryAction,
markChapterReadAction,
getChapterAction,
chapter,
loading,
reader,
}) => {
const { extensionId, chapterUrl, novelUrl, chapterName } = route.params;
const {
chapterId,
extensionId,
chapterUrl,
novelUrl,
novelId,
} = route.params;
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(() => {
getChapter(extensionId, chapterUrl, novelUrl);
updateNovelHistory(chapterUrl, chapterName, novelUrl);
getChapterAction(extensionId, chapterUrl, novelUrl, chapterId);
insertHistoryAction(novelId, chapterId);
}, []);
const readerBackground = (val) => {
@@ -42,15 +66,19 @@ const ChapterItem = ({
};
const readerTextColor = (val) => {
const textColor = val === 1 ? theme.textColorSecondary : "#000000";
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "#000000";
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];
@@ -71,7 +99,7 @@ const ChapterItem = ({
return (
<Provider>
<CollapsibleHeaderScrollView
headerContainerBackgroundColor={"rgba(0,0,0,0.4)"}
headerContainerBackgroundColor="rgba(0,0,0,0.4)"
CollapsibleHeaderComponent={
<Appbar.Header
style={{
@@ -96,10 +124,12 @@ const ChapterItem = ({
size={26}
disabled={!chapter.prevChapter}
onPress={() => {
navigation.push("ChapterItem", {
navigation.replace("Chapter", {
chapterUrl: chapter.prevChapter,
chapterId: chapterId - 1,
extensionId,
novelUrl,
novelId,
chapterName: chapter.chapterName,
});
}}
@@ -110,10 +140,12 @@ const ChapterItem = ({
size={26}
disabled={!chapter.nextChapter}
onPress={() => {
navigation.push("ChapterItem", {
navigation.replace("Chapter", {
chapterUrl: chapter.nextChapter,
extensionId,
novelUrl,
novelId,
chapterId: chapterId + 1,
chapterName: chapter.chapterName,
});
}}
@@ -138,7 +170,21 @@ const ChapterItem = ({
]}
onScroll={({ nativeEvent }) => {
if (isCloseToBottom(nativeEvent)) {
updateChapterRead(chapterUrl, novelUrl);
markChapterReadAction(chapterId);
if (
isTracked &&
chapterNumber &&
chapterNumber >
isTracked.my_list_status.num_chapters_read
) {
dispatch(
updateChaptersRead(
isTracked.id,
tracker.access_token,
chapterNumber
)
);
}
}
}}
>
@@ -157,6 +203,11 @@ const ChapterItem = ({
fontSize: reader.textSize,
color: readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
},
]}
>
@@ -179,15 +230,14 @@ const mapStateToProps = (state) => ({
});
export default connect(mapStateToProps, {
updateNovelHistory,
updateChapterRead,
getChapter,
insertHistoryAction,
markChapterReadAction,
getChapterAction,
})(ChapterItem);
const styles = StyleSheet.create({
container: {
flexGrow: 1,
paddingHorizontal: 10,
paddingVertical: 10,
},
});
+200 -47
View File
@@ -1,17 +1,20 @@
import React from "react";
import { Animated, StyleSheet, View, Text, Dimensions } from "react-native";
import { ToggleButton } 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 "../../../Services/utils/constants";
import { connect } from "react-redux";
import {
setReaderFont,
updateReaderPadding,
updateReaderTextAlign,
updateReaderTextSize,
updateReaderTheme,
} from "../../../redux/actions/settings";
import BottomSheetHandle from "../../../components/common/BottomSheetHandle";
} from "../../../redux/settings/settings.actions";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
const ChapterBottomSheet = ({
bottomSheetRef,
@@ -19,26 +22,24 @@ const ChapterBottomSheet = ({
reader,
updateReaderTextSize,
updateReaderTheme,
updateReaderTextAlign,
updateReaderPadding,
setReaderFont,
}) => {
return (
<Bottomsheet
animatedValue={new Animated.Value(0)}
ref={bottomSheetRef}
draggableRange={{ top: 200, bottom: 0 }}
snappingPoints={[0, 200]}
draggableRange={{ top: 400, bottom: 0 }}
snappingPoints={[0, 400]}
showBackdrop={false}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: "rgba(0,0,0,0.4)" },
]}
>
<View style={styles.contentContainer}>
<BottomSheetHandle />
<View style={styles.readerSettingsContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
fontWeight: "bold",
}}
>
@@ -52,53 +53,199 @@ const ChapterBottomSheet = ({
value={reader.textSize}
minimumValue={12}
maximumValue={20}
step={4}
step={2}
minimumTrackTintColor={theme.colorAccentDark}
maximumTrackTintColor="#000000"
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 }}
<View
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<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={{
justifyContent: "center",
alignItems: "center",
}}
>
<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: "#FFFFFF",
fontWeight: "bold",
marginTop: 10,
}}
>
<ToggleButton
icon="format-text"
color="#FFFFFF"
value={1}
style={{
backgroundColor: "#000000",
marginHorizontal: 10,
}}
Padding
</Text>
<View
style={{
width: "100%",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
}}
>
<IconButton
icon="minus"
color={theme.colorAccentDark}
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
updateReaderPadding(reader.padding - 1)
}
/>
<ToggleButton
icon="format-text"
color="#000000"
value={2}
<Text
style={{
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
color: "#FFFFFF",
paddingHorizontal: 50,
}}
>
{`${reader.padding}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccentDark}
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
updateReaderPadding(reader.padding + 1)
}
/>
<ToggleButton
icon="format-text"
color="#000000"
value={3}
style={{
backgroundColor: "#F4ECD8",
marginHorizontal: 10,
}}
/>
</ToggleButton.Row>
</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>
@@ -113,11 +260,17 @@ const mapStateToProps = (state) => ({
export default connect(mapStateToProps, {
updateReaderTextSize,
updateReaderTheme,
updateReaderTextAlign,
updateReaderPadding,
setReaderFont,
})(ChapterBottomSheet);
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
backgroundColor: "rgba(0,0,0,0.4)",
},
readerSettingsContainer: { flex: 1, alignItems: "center", paddingTop: 30 },
});
@@ -1,25 +1,48 @@
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 { SearchAppbar } from "../../../components/common/Appbar";
import NovelCover from "../../Components/NovelCover";
import EmptyView from "../../Components/EmptyView";
import { useSelector } from "react-redux";
import EmptyView from "../../../components/common/EmptyView";
import { Searchbar } from "../../Components/Searchbar";
import { useTheme } from "../../Hooks/useTheme";
import { useLibrary } from "../../Hooks/useLibrary";
const ReadLightNovel = ({ navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const library = useSelector((state) => state.libraryReducer.novels);
const Extension = ({ navigation, route }) => {
const { sourceId, sourceName, sourceUrl } = route.params;
const theme = useTheme();
const library = useLibrary();
const itemsPerRow = useSelector(
(state) => state.settingsReducer.itemsPerRow
);
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);
@@ -27,11 +50,9 @@ const ReadLightNovel = ({ navigation }) => {
});
};
const getSearchResults = (searchText) => {
const getSearchResults = () => {
setLoading(true);
fetch(
`https://lnreader-extensions.herokuapp.com/api/2/search/?s=${searchText}`
)
fetch(getSearchUrl())
.then((response) => response.json())
.then((json) => {
setNovels(json);
@@ -55,14 +76,16 @@ const ReadLightNovel = ({ navigation }) => {
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<SearchAppbar
screen="Extension"
placeholder="Search ReadLightNovel"
<Searchbar
theme={theme}
placeholder={`Search ${sourceName}`}
left="arrow-left"
onPressLeft={() => navigation.goBack()}
searchText={searchText}
setSearchText={setSearchText}
getSearchResults={getSearchResults}
getNovels={getNovels}
setLoading={setLoading}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchResults}
right="earth"
onPressRight={() => WebBrowser.openBrowserAsync(sourceUrl)}
/>
{loading ? (
@@ -75,7 +98,8 @@ const ReadLightNovel = ({ navigation }) => {
) : (
<FlatList
contentContainerStyle={{ flexGrow: 1 }}
numColumns={3}
numColumns={itemsPerRow}
key={itemsPerRow}
data={novels}
showsVerticalScrollIndicator={false}
keyExtractor={(item) => item.novelUrl}
@@ -83,17 +107,17 @@ const ReadLightNovel = ({ navigation }) => {
<NovelCover
item={item}
onPress={() =>
navigation.navigate("NovelItem", item)
}
libraryStatus={
checkIFInLibrary(item.novelUrl)
? true
: false
navigation.navigate("Novel", {
novelName: item.novelName,
novelCover: item.novelCover,
novelUrl: item.novelUrl,
sourceId: item.extensionId,
})
}
libraryStatus={checkIFInLibrary(item.novelUrl)}
/>
)}
ListEmptyComponent={
searchText !== "" &&
novels.length === 0 && (
<EmptyView
icon="(; ̄Д ̄)"
@@ -108,7 +132,7 @@ const ReadLightNovel = ({ navigation }) => {
);
};
export default ReadLightNovel;
export default Extension;
const styles = StyleSheet.create({
container: {
+12 -12
View File
@@ -3,35 +3,35 @@ 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 {
getHistory,
deleteChapterFromHistory,
} from "../../redux/actions/history";
getHistoryAction,
deleteHistoryAction,
} from "../../redux/history/history.actions";
const History = ({
navigation,
theme,
history,
loading,
getHistory,
deleteChapterFromHistory,
getHistoryAction,
deleteHistoryAction,
}) => {
useFocusEffect(
useCallback(() => {
getHistory();
}, [])
getHistoryAction();
}, [getHistoryAction])
);
const renderHistoryCard = ({ item }) => (
<HistoryCard
item={item}
deleteHistory={deleteChapterFromHistory}
deleteHistoryAction={deleteHistoryAction}
navigation={navigation}
/>
);
@@ -79,8 +79,8 @@ const mapStateToProps = (state) => ({
});
export default connect(mapStateToProps, {
getHistory,
deleteChapterFromHistory,
getHistoryAction,
deleteHistoryAction,
})(History);
const styles = StyleSheet.create({
@@ -7,7 +7,7 @@ import moment from "moment";
import { useSelector } from "react-redux";
const HistoryCard = ({ item, deleteHistory, navigation }) => {
const HistoryCard = ({ item, deleteHistoryAction, navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
@@ -16,12 +16,12 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
rippleColor={theme.rippleColor}
borderless
onPress={() =>
navigation.navigate("NovelItem", {
navigation.navigate("Novel", {
novelName: item.novelName,
novelCover: item.novelCover,
extensionId: item.extensionId,
novelUrl: item.novelUrl,
libraryStatus: item.libraryStatus,
sourceId: item.sourceId,
})
}
>
@@ -50,7 +50,7 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
numberOfLines={1}
>
{`${item.chapterName} - ${moment(
item.lastRead
item.historyTimeRead
).calendar()}`}
</Text>
</View>
@@ -67,7 +67,7 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
style={{
marginRight: 0,
}}
onPress={() => deleteHistory(item.novelUrl)}
onPress={() => deleteHistoryAction(item.novelId)}
/>
<IconButton
icon="play"
@@ -75,12 +75,12 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
color={theme.textColorPrimary}
style={{ marginRight: 10 }}
onPress={() =>
navigation.navigate("ChapterItem", {
navigation.navigate("Chapter", {
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,
})
}
+44 -46
View File
@@ -5,55 +5,61 @@ import {
View,
RefreshControl,
ActivityIndicator,
ToastAndroid,
} from "react-native";
import { FlatList } from "react-native-gesture-handler";
import { useFocusEffect } from "@react-navigation/native";
import { connect } from "react-redux";
import { connect, useDispatch } 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 {
getLibraryNovels,
searchLibraryNovels,
} from "../../redux/actions/library";
import { SearchAppbar } from "../../components/common/Appbar";
import { setNovel } from "../../redux/actions/novel";
getLibraryAction,
searchLibraryAction,
} from "../../redux/library/library.actions";
import { SearchAppbar } from "../../Components/Appbar";
import { setNovel } from "../../redux/novel/novel.actions";
import { updateLibraryAction } from "../../redux/updates/updates.actions";
import { useTheme } from "../../Hooks/useTheme";
import { Searchbar } from "../../Components/Searchbar";
const LibraryScreen = ({ navigation, novels, loading, itemsPerRow }) => {
const theme = useTheme();
const dispatch = useDispatch();
const LibraryScreen = ({
navigation,
theme,
novels,
loading,
getLibraryNovels,
searchLibraryNovels,
setNovel,
}) => {
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
useFocusEffect(
useCallback(() => {
setSearchText("");
getLibraryNovels();
}, [])
dispatch(getLibraryAction());
}, [getLibraryAction])
);
/**
* TODO: fix refreshing
*/
const onRefresh = () => {
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
setRefreshing(true);
getLibraryNovels();
dispatch(updateLibraryAction());
setRefreshing(false);
};
const redirectToNovel = (item) => {
navigation.navigate("NovelItem", item);
setNovel(item);
navigation.navigate("Novel", item);
dispatch(setNovel(item));
};
const renderNovels = ({ item }) => (
<NovelCover item={item} onPress={() => redirectToNovel(item)} />
);
const clearSearchbar = () => {
dispatch(getLibraryAction());
setSearchText("");
};
const onChangeText = (text) => {
setSearchText(text);
dispatch(searchLibraryAction(text));
};
return (
@@ -64,13 +70,13 @@ const LibraryScreen = ({
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<SearchAppbar
screen="Library"
<Searchbar
placeholder="Search Library"
getNovels={getLibraryNovels}
getSearchResults={searchLibraryNovels}
searchText={searchText}
setSearchText={setSearchText}
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
left="magnify"
theme={theme}
/>
{loading ? (
<ActivityIndicator
@@ -83,15 +89,11 @@ const LibraryScreen = ({
flexGrow: 1,
paddingBottom: 8,
}}
numColumns={3}
numColumns={itemsPerRow}
key={itemsPerRow}
data={novels}
keyExtractor={(item) => item.novelUrl}
renderItem={({ item }) => (
<NovelCover
item={item}
onPress={() => redirectToNovel(item)}
/>
)}
renderItem={renderNovels}
refreshControl={
<RefreshControl
refreshing={refreshing}
@@ -125,16 +127,12 @@ const LibraryScreen = ({
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
itemsPerRow: state.settingsReducer.itemsPerRow,
novels: state.libraryReducer.novels,
loading: state.libraryReducer.loading,
});
export default connect(mapStateToProps, {
getLibraryNovels,
searchLibraryNovels,
setNovel,
})(LibraryScreen);
export default connect(mapStateToProps)(LibraryScreen);
const styles = StyleSheet.create({
container: {
-86
View File
@@ -1,86 +0,0 @@
import React from "react";
import { StyleSheet, TextInput } from "react-native";
import { Appbar } from "react-native-paper";
import { useSelector } from "react-redux";
const LibraryAppbar = ({
search,
setSearch,
getLibraryNovels,
searchLibraryNovels,
}) => {
const theme = useSelector((state) => state.themeReducer.theme);
const { searching, searchText } = search;
return (
<Appbar.Header style={{ backgroundColor: theme.colorPrimary }}>
{!searching ? (
<>
<Appbar.Content
title="Library"
titleStyle={{ color: theme.textColorPrimary }}
/>
<Appbar.Action
icon="magnify"
onPress={() =>
setSearch({ ...search, searching: true })
}
color={theme.textColorPrimary}
/>
<Appbar.Action
color={theme.textColorPrimary}
icon="filter-variant"
disabled
// onPress={() => _panel.show({ velocity: -1.5 })}
/>
</>
) : (
<>
<Appbar.BackAction
onPress={() => {
setSearch({ searching: false, searchText: "" });
getLibraryNovels();
}}
color={theme.textColorPrimary}
size={24}
/>
<TextInput
placeholder="Search..."
defaultValue={searchText}
style={[
styles.searchBar,
{
color: theme.textColorPrimary,
},
]}
autoFocus
placeholderTextColor={theme.textColorHintDark}
blurOnSubmit={true}
onChangeText={(text) => {
setSearch({ ...search, searchText: text });
searchLibraryNovels(text);
}}
/>
{searchText !== "" && (
<Appbar.Action
icon="close"
onPress={() => {
setSearch({ ...search, searchText: "" });
getLibraryNovels();
}}
color={theme.textColorPrimary}
/>
)}
</>
)}
</Appbar.Header>
);
};
export default LibraryAppbar;
const styles = StyleSheet.create({
searchBar: { fontSize: 18, flex: 1, marginLeft: 15 },
});
+55 -61
View File
@@ -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.6"
/>
<List.Item
titleStyle={{ color: theme.textColorPrimary }}
title="Build Time"
descriptionStyle={{ color: theme.textColorSecondary }}
description="21-03-21 11:10 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.16"
theme={theme}
/>
<ListItem
title="Build Time"
description="03-05-21 20:45 PM"
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 -37
View File
@@ -1,49 +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("Settings")}
/>
<List.Item
titleStyle={{ color: theme.textColorPrimary }}
title="About"
left={() => (
<List.Icon
color={theme.colorAccentDark}
icon="information-outline"
/>
)}
onPress={() => navigation.navigate("About")}
/>
</List.Section>
</View>
</View>
<ListSection>
<ListItem
title="Settings"
icon="cog-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "Settings",
})
}
theme={theme}
/>
<ListItem
title="Tracking"
icon="sync"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "Trackers",
})
}
theme={theme}
/>
<ListItem
title="About"
icon="information-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "About",
})
}
theme={theme}
/>
</ListSection>
</View>
</>
);
};
+110 -158
View File
@@ -1,208 +1,160 @@
import React, { useState } from "react";
import { View } from "react-native";
import { setStatusBarStyle } from "expo-status-bar";
import { List, Modal, Portal } from "react-native-paper";
import { connect } from "react-redux";
import { switchTheme } from "../../redux/actions/theme";
import { setDisplayMode, setItemsPerRow } from "../../redux/actions/settings";
import { Appbar } from "../../components/common/Appbar";
import { switchTheme } from "../../redux/theme/theme.actions";
import {
DisplayCheckbox,
ThemeCheckbox,
} from "../../components/settings/Checkbox";
setDisplayMode,
setItemsPerRow,
} from "../../redux/settings/settings.actions";
import { deleteAllHistory } from "../../database/queries/HistoryQueries";
import { deleteNovelCache } from "../../database/queries/NovelQueries";
import { Appbar } from "../../Components/Appbar";
import {
deleteHistory,
deleteNovelsNotInLibrary,
deleteDatabase,
} from "../../services/db";
Divider,
ListItem,
ListSection,
ListSubHeader,
InfoItem,
} from "../../Components/List";
import DisplayModeModal from "./components/DisplayModeModal";
import GridSizeModal from "./components/GridSizeModal";
import ThemeModal from "./components/ThemeModal";
import { createBackup, restoreBackup } from "../../Services/backup";
import { useTheme } from "../../Hooks/useTheme";
const SettingsScreen = ({
navigation,
theme,
themeCode,
displayMode,
itemsPerRow,
switchTheme,
setDisplayMode,
setItemsPerRow,
}) => {
const display = {
0: "Compact",
1: "Comfortable",
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 theme = useTheme();
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);
// 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={() => deleteNovelsNotInLibrary()}
rippleColor={theme.rippleColor}
onPress={() => deleteNovelCache()}
theme={theme}
/>
<List.Item
titleStyle={titleStyles}
<ListItem
title="Clear history"
descriptionStyle={desciptionStyles}
description="Delete reading history for all novels"
onPress={() => deleteHistory()}
rippleColor={theme.rippleColor}
onPress={() => deleteAllHistory()}
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)}
/>
</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={() => deleteDatabase()}
/> */}
<List.Subheader
style={{
color: theme.colorAccentDark,
paddingBottom: 5,
}}
>
Theme
</List.Subheader>
<List.Item
titleStyle={titleStyles}
<ListItem
title="Items per row in library"
description={`${itemsPerRow} items per row`}
onPress={showGridSizeModal}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Theme</ListSubHeader>
<ListItem
title="Theme"
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>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Backup</ListSubHeader>
<ListItem
title="Create backup"
description="Can be used to restore current library"
onPress={createBackup}
theme={theme}
/>
<ListItem
title="Restore backup"
description="Restore library from backup file"
onPress={restoreBackup}
theme={theme}
/>
<InfoItem
title="Create backup may not work on devices with Android 9 or lower."
icon="information-outline"
description="Restore library from backup file"
onPress={restoreBackup}
theme={theme}
/>
</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,
});
export default connect(mapStateToProps, {
+158
View File
@@ -0,0 +1,158 @@
import React, { useState } from "react";
import { View } from "react-native";
import { Modal, Portal, Text, Button, Provider } from "react-native-paper";
import * as WebBrowser from "expo-web-browser";
import {
myAnimeListConfig,
getAccessToken,
malTokenWatcher,
} from "../../Services/Trackers/myAnimeList";
import { Appbar } from "../../Components/Appbar";
import { useSelector, useDispatch } from "react-redux";
import {
Divider,
ListItem,
ListSection,
ListSubHeader,
} from "../../Components/List";
import { removeTracker, setTracker } from "../../redux/tracker/tracker.actions";
const TrackerScreen = ({ navigation }) => {
const theme = useSelector((state) => state.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,
},
});
+60
View File
@@ -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,
},
});
+59 -79
View File
@@ -1,94 +1,63 @@
import React, { useState, useEffect, useRef } from "react";
import {
StyleSheet,
View,
FlatList,
RefreshControl,
ToastAndroid,
} from "react-native";
import React, { useEffect, useRef, useState } from "react";
import { StyleSheet, View, FlatList, RefreshControl } from "react-native";
import { Provider, Portal } from "react-native-paper";
import { useTheme } from "../../Hooks/useTheme";
import ChapterCard from "./components/ChapterCard";
import NovelInfoHeader from "./components/NovelHeader";
import { BottomSheet } from "./components/BottomSheet";
import { downloadOrDeleteChapter } from "../../services/db";
import ChaptersSettingsSheet from "./components/ChaptersSettingsSheet";
import { connect } from "react-redux";
import {
getNovel,
insertNovelInLibrary,
getNovelAction,
sortAndFilterChapters,
} from "../../redux/actions/novel";
updateNovelAction,
} from "../../redux/novel/novel.actions";
import TrackSheet from "./components/Tracker/TrackSheet";
import { showToast } from "../../Hooks/showToast";
const Novel = ({
route,
theme,
novel,
chapters,
loading,
getNovel,
getNovelAction,
fetching,
insertNovelInLibrary,
sortAndFilterChapters,
updateNovelAction,
}) => {
const {
extensionId,
sourceId,
novelUrl,
novelName,
novelCover,
libraryStatus,
followed,
novelId,
} = route.params;
const [downloading, setDownloading] = useState({
downloading: false,
chapterUrl: "",
});
const theme = useTheme();
let _panel = useRef(null); // Bottomsheet ref
let chaptersSettingsSheetRef = useRef(null);
let trackerSheetRef = useRef(null);
/**
* Download chapter from source
*
* If downloaded then delete from db
*/
const downloadChapter = (
downloadStatus,
extensionId,
novelUrl,
chapterUrl,
chapterName
) => {
setDownloading({ downloading: true, chapterUrl: chapterUrl });
downloadOrDeleteChapter(
downloadStatus,
extensionId,
novelUrl,
chapterUrl
).then((res) => {
setDownloading({ downloading: false });
ToastAndroid.show(
!downloadStatus
? `Downloaded ${chapterName}`
: `Deleted ${chapterName}`,
ToastAndroid.SHORT
);
});
};
const [sort, setSort] = useState("ORDER BY chapterId ASC");
const [filter, setFilter] = useState("");
useEffect(() => {
getNovel(libraryStatus, extensionId, novelUrl);
}, []);
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter);
}, [getNovelAction, sort, filter]);
const onRefresh = async () => {
await updateNovelAction(sourceId, novelUrl, novelId);
showToast(`Updated ${novelName}`);
};
const renderChapterCard = ({ item }) => (
<ChapterCard
novelUrl={novelUrl}
extensionId={extensionId}
extensionId={sourceId}
chapter={item}
downloadChapter={downloadChapter}
downloading={downloading}
/>
);
@@ -102,7 +71,7 @@ const Novel = ({
>
<FlatList
data={!loading && chapters}
keyExtractor={(item) => item.chapterUrl}
keyExtractor={(item) => item.chapterId.toString()}
removeClippedSubviews={true}
maxToRenderPerBatch={5}
windowSize={15}
@@ -110,37 +79,49 @@ const Novel = ({
renderItem={renderChapterCard}
ListHeaderComponent={
<NovelInfoHeader
theme={theme}
item={{ novelName, novelCover }}
novel={novel}
noOfChapters={chapters.length}
insertNovelInLibrary={insertNovelInLibrary}
loading={loading}
bottomSheetRef={_panel}
novelName={novelName}
novelCover={novelCover}
novel={novel}
chapters={chapters}
trackerSheetRef={trackerSheetRef}
chaptersSettingsSheetRef={chaptersSettingsSheetRef}
theme={theme}
/>
}
refreshControl={
<RefreshControl
refreshing={fetching}
// onRefresh={onRefresh}
onRefresh={onRefresh}
colors={[theme.textColorPrimary]}
progressBackgroundColor={theme.colorPrimary}
/>
}
/>
<Portal>
<BottomSheet
novelUrl={novelUrl}
bottomSheetRef={_panel}
sortAndFilterChapters={sortAndFilterChapters}
/>
</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>
);
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
novel: state.novelReducer.novel,
chapters: state.novelReducer.chapters,
loading: state.novelReducer.loading,
@@ -148,13 +129,12 @@ const mapStateToProps = (state) => ({
});
export default connect(mapStateToProps, {
getNovel,
insertNovelInLibrary,
getNovelAction,
sortAndFilterChapters,
updateNovelAction,
})(Novel);
const styles = StyleSheet.create({
container: {
flex: 1,
},
container: { flex: 1 },
});
+88 -46
View File
@@ -1,32 +1,101 @@
import React, { useState } from "react";
import { View, Text, ActivityIndicator, StyleSheet } from "react-native";
import { TouchableRipple, IconButton } from "react-native-paper";
import { TouchableRipple, IconButton, Menu } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
import {
deleteChapterAction,
downloadChapterAction,
} from "../../../redux/novel/novel.actions";
import { connect } from "react-redux";
const ChapterCard = ({
novelUrl,
chapter,
downloadChapter,
extensionId,
downloading,
theme,
// downloading,
downloadChapterAction,
deleteChapterAction,
}) => {
const [downloaded, setDownloaded] = useState(chapter.downloaded);
const theme = useSelector((state) => state.themeReducer.theme);
const navigation = useNavigation();
const [visible, setVisible] = useState(false);
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
const displayDownloadButton = () => {
if (chapter.downloaded === 3) {
return (
<ActivityIndicator
color={theme.textColorHintDark}
size={25}
style={{ margin: 3.5, padding: 5 }}
/>
);
} else if (chapter.downloaded === 1) {
return (
<Menu
visible={visible}
onDismiss={closeMenu}
anchor={
<IconButton
icon="check-circle"
animated
color={theme.textColorPrimary}
size={25}
onPress={openMenu}
style={{ margin: 2 }}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
>
<Menu.Item
onPress={() =>
deleteChapterAction(
chapter.chapterId,
chapter.chapterName
)
}
title="Delete"
titleStyle={{ color: theme.textColorPrimary }}
/>
</Menu>
);
} else {
return (
<IconButton
icon="arrow-down-circle-outline"
animated
color={theme.textColorHintDark}
size={25}
onPress={() => {
downloadChapterAction(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId
);
// console.log(downloading);
}}
style={{ margin: 2 }}
/>
);
}
};
return (
<TouchableRipple
style={styles.chapterCardContainer}
onPress={() =>
navigation.navigate("ChapterItem", {
navigation.navigate("Chapter", {
chapterId: chapter.chapterId,
chapterUrl: chapter.chapterUrl,
extensionId,
novelUrl: novelUrl,
chapterName: chapter.chapterName,
novelId: chapter.novelId,
})
}
rippleColor={theme.rippleColor}
@@ -64,48 +133,21 @@ const ChapterCard = ({
</Text>
)}
</View>
<View>
{downloading.downloading &&
downloading.chapterUrl === chapter.chapterUrl ? (
<ActivityIndicator
color={theme.textColorHintDark}
size={25}
style={{ margin: 3.5, padding: 5 }}
/>
) : (
<IconButton
icon={
downloaded
? "check-circle"
: "arrow-down-circle-outline"
}
animated
color={
downloaded
? theme.textColorPrimary
: theme.textColorHintDark
}
size={25}
onPress={() => {
downloadChapter(
downloaded ? 1 : 0,
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName
);
setDownloaded(!downloaded);
}}
style={{ margin: 2 }}
/>
)}
</View>
<View>{displayDownloadButton()}</View>
</>
</TouchableRipple>
);
};
export default ChapterCard;
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
// downloading: state.novelReducer.downloading,
});
export default connect(mapStateToProps, {
downloadChapterAction,
deleteChapterAction,
})(ChapterCard);
const styles = StyleSheet.create({
chapterCardContainer: {
@@ -1,33 +1,27 @@
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,
sortAndFilterChapters,
sort,
filter,
setSort,
setFilter,
novelUrl,
}) => {
const checkBoxStyle = { flexDirection: "row", alignItems: "center" };
const [chapterFlags, setChapterFlags] = useState({ sort: "", filter: "" });
const sortChapters = (sort) => {
setChapterFlags({ ...chapterFlags, sort: sort });
sortAndFilterChapters(novelUrl, chapterFlags.filter, chapterFlags.sort);
};
const filterChapters = (filter) => {
setChapterFlags({ ...chapterFlags, filter: filter });
sortAndFilterChapters(novelUrl, chapterFlags.filter, chapterFlags.sort);
};
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]}
@@ -51,9 +45,8 @@ export const BottomSheet = ({
</List.Subheader>
<View>
<RadioButton.Group
onValueChange={(newValue) => sortChapters(newValue)}
value={chapterFlags.sort}
c
onValueChange={(newValue) => setSort(newValue)}
value={sort}
>
<View
style={{
@@ -89,7 +82,7 @@ export const BottomSheet = ({
<RadioButton.Item
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
value=""
value="ORDER BY chapterId ASC"
/>
<Text
style={{
@@ -114,12 +107,10 @@ export const BottomSheet = ({
</List.Subheader>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
chapterFlags.filter === "" ? "checked" : "unchecked"
}
status={filter === "" ? "checked" : "unchecked"}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => filterChapters("")}
onPress={() => setFilter("")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show all
@@ -128,13 +119,11 @@ export const BottomSheet = ({
<View style={checkBoxStyle}>
<Checkbox.Item
status={
chapterFlags.filter === "AND `read`=1"
? "checked"
: "unchecked"
filter === "AND `read`=1" ? "checked" : "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => filterChapters("AND `read`=1")}
onPress={() => setFilter("AND `read`=1")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show read chapters
@@ -143,13 +132,11 @@ export const BottomSheet = ({
<View style={checkBoxStyle}>
<Checkbox.Item
status={
chapterFlags.filter === "AND `read`=0"
? "checked"
: "unchecked"
filter === "AND `read`=0" ? "checked" : "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => filterChapters("AND `read`=0")}
onPress={() => setFilter("AND `read`=0")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show unread chapters
@@ -158,13 +145,13 @@ export const BottomSheet = ({
<View style={checkBoxStyle}>
<Checkbox.Item
status={
chapterFlags.filter === "AND downloaded=1"
filter === "AND downloaded=1"
? "checked"
: "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => filterChapters("AND downloaded=1")}
onPress={() => setFilter("AND downloaded=1")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show downloaded chapters
@@ -175,6 +162,8 @@ export const BottomSheet = ({
);
};
export default ChaptersSettingsSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,

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