Compare commits

...

32 Commits

Author SHA1 Message Date
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
89 changed files with 3508 additions and 2119 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

+7 -1
View File
@@ -10,11 +10,17 @@ import Router from "./src/navigation/Router";
import { Provider } from "react-redux";
import { persistor, store } from "./src/redux/store";
import { PersistGate } from "redux-persist/lib/integration/react";
import { createDB } from "./src/database/DBHelper";
import { createDB, deleteDb } from "./src/database/DBHelper";
const getFonts = () =>
Font.loadAsync({
"pt-sans-bold": require("./assets/fonts/PTSansNarrow-Bold.ttf"),
"arbutus-slab": require("./assets/fonts/ArbutusSlab-Regular.ttf"),
domine: require("./assets/fonts/Domine-VariableFont_wght.ttf"),
lato: require("./assets/fonts/Lato-Regular.ttf"),
"noto-sans": require("./assets/fonts/NotoSans-Regular.ttf"),
"open-sans": require("./assets/fonts/OpenSans-Regular.ttf"),
"pt-serif": require("./assets/fonts/PTSerif-Regular.ttf"),
});
const App = () => {
+4 -2
View File
@@ -2,7 +2,8 @@
"expo": {
"name": "LNReader",
"slug": "LNReader",
"version": "1.0.0",
"scheme": "lnreader",
"version": "1.0.15",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
@@ -11,7 +12,8 @@
"backgroundColor": "#000000"
},
"updates": {
"fallbackToCacheTimeout": 0
"fallbackToCacheTimeout": 0,
"enabled": false
},
"assetBundlePatterns": ["**/*"],
"ios": {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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

+34 -778
View File
File diff suppressed because it is too large Load Diff
+54 -54
View File
@@ -1,56 +1,56 @@
{
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.13.2",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "^5.9.7",
"@react-native-community/slider": "^3.0.3",
"@react-navigation/material-bottom-tabs": "^5.3.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "~40.0.0",
"expo-app-loading": "^1.0.1",
"expo-font": "^8.4.0",
"expo-linear-gradient": "^8.4.0",
"expo-sharing": "~8.5.0",
"expo-sqlite": "~8.5.0",
"expo-status-bar": "~1.0.3",
"expo-web-browser": "~8.6.0",
"moment": "^2.29.1",
"react": "16.13.1",
"react-color-extractor": "^1.1.2",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
"react-native-collapsible-header-views": "^1.1.2",
"react-native-gesture-handler": "^1.8.0",
"react-native-input-spinner": "^1.7.7",
"react-native-offline": "^5.7.0",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "^1.13.2",
"react-native-safe-area-context": "3.1.9",
"react-native-screens": "~2.15.0",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "~0.13.12",
"react-native-webview": "11.0.0",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"reflect-metadata": "^0.1.13",
"rn-sliding-up-panel": "^2.4.4",
"typeorm": "^0.2.31"
},
"devDependencies": {
"@babel/core": "~7.9.0",
"@types/node": "^14.14.35"
},
"private": true
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.13.2",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "^5.9.7",
"@react-native-community/slider": "^3.0.3",
"@react-navigation/material-bottom-tabs": "^5.3.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "~40.0.0",
"expo-app-loading": "^1.0.1",
"expo-font": "^8.4.0",
"expo-linear-gradient": "^8.4.0",
"expo-sharing": "~8.5.0",
"expo-sqlite": "~8.5.0",
"expo-status-bar": "~1.0.3",
"expo-web-browser": "~8.6.0",
"moment": "^2.29.1",
"qs": "^6.10.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
"react-native-collapsible-header-views": "^1.1.2",
"react-native-gesture-handler": "^1.8.0",
"react-native-inappbrowser-reborn": "^3.5.1",
"react-native-offline": "^5.7.0",
"react-native-paper": "^4.5.0",
"react-native-picker-scrollview": "^1.0.1",
"react-native-reanimated": "^1.13.2",
"react-native-safe-area-context": "3.1.9",
"react-native-screens": "~2.15.0",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "~0.13.12",
"react-native-webview": "11.0.0",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"reflect-metadata": "^0.1.13",
"rn-sliding-up-panel": "^2.4.4"
},
"devDependencies": {
"@babel/core": "~7.9.0",
"@types/node": "^14.14.35"
},
"private": true
}
@@ -10,11 +10,13 @@ import Constants from "expo-constants";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
export const Appbar = ({ title, onBackAction }) => {
export const Appbar = ({ title, onBackAction, style }) => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<MaterialAppbar.Header style={{ backgroundColor: theme.colorPrimary }}>
<MaterialAppbar.Header
style={[{ backgroundColor: theme.colorPrimary }, style]}
>
{onBackAction && (
<MaterialAppbar.BackAction onPress={onBackAction} />
)}
@@ -34,7 +36,7 @@ export const SearchAppbar = ({
getNovels,
setLoading,
screen,
onFilter,
openWebView,
}) => {
const searchRef = useRef(null);
@@ -110,17 +112,15 @@ export const SearchAppbar = ({
}}
/>
)}
<IconButton
icon="filter-variant"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
disabled={typeof onFilter === "function" ? false : true}
/**
* TODO
*/
onPress={() => onFilter?.()}
/>
{openWebView && (
<IconButton
icon="earth"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={openWebView}
/>
)}
</View>
</TouchableRipple>
);
@@ -10,9 +10,7 @@ const EmptyView = ({ icon, description }) => {
<Text
style={[
styles.emptyViewIcon,
{
color: theme.textColorSecondary,
},
{ color: theme.textColorSecondary },
]}
>
{icon}
@@ -20,9 +18,7 @@ const EmptyView = ({ icon, description }) => {
<Text
style={[
styles.emptyViewText,
{
color: theme.textColorSecondary,
},
{ color: theme.textColorSecondary },
]}
>
{description}
+66
View File
@@ -0,0 +1,66 @@
import React from "react";
import { StyleSheet } from "react-native";
import { List, Divider as PaperDivider } from "react-native-paper";
const ListSection = ({ children }) => (
<List.Section style={styles.listSection}>{children}</List.Section>
);
const ListSubHeader = ({ children, theme }) => (
<List.Subheader
style={[styles.listSubHeader, { color: theme.colorAccentDark }]}
>
{children}
</List.Subheader>
);
const ListItem = ({ title, description, icon, onPress, theme, right }) => (
<List.Item
title={title}
titleStyle={{ color: theme.textColorPrimary }}
description={description}
descriptionStyle={{ color: theme.textColorSecondary }}
descriptionNumberOfLines={1}
left={() =>
icon && (
<List.Icon
color={theme.colorAccentDark}
icon={icon}
style={{ marginVertical: 0 }}
/>
)
}
right={() =>
right && (
<List.Icon
color="#47a84a"
icon={right}
style={{ marginVertical: 0 }}
/>
)
}
onPress={onPress}
rippleColor={theme.rippleColor}
/>
);
const Divider = ({ theme }) => (
<PaperDivider
style={{
backgroundColor: theme.dividerColor,
height: 1,
}}
/>
);
export { ListSection, ListSubHeader, ListItem, Divider };
const styles = StyleSheet.create({
listSection: {
flex: 1,
marginVertical: 0,
},
listSubHeader: {
paddingBottom: 5,
},
});
@@ -148,24 +148,86 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
style={styles.listView}
onPress={onPress}
>
<>
<Image
source={{ uri: item.novelCover }}
style={styles.extensionIcon}
/>
<Text
<View
style={{
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<View
style={{
flex: 1,
color: theme.textColorPrimary,
marginLeft: 16,
fontSize: 15,
paddingRight: 8,
flexDirection: "row",
alignItems: "center",
}}
numberOfLines={1}
>
{item.novelName}
</Text>
</>
<Image
source={{ uri: item.novelCover }}
style={styles.extensionIcon}
/>
<Text
style={{
color: theme.textColorPrimary,
marginLeft: 16,
fontSize: 15,
paddingRight: 8,
}}
numberOfLines={1}
>
{item.novelName}
</Text>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
{item.chaptersDownloaded && (
<Text
style={[
{
backgroundColor: "#47a84a",
color: "#FFFFFF",
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
!item.chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
]}
>
{item.chaptersDownloaded}
</Text>
)}
{item.chaptersUnread && (
<Text
style={[
{
backgroundColor: "#2979FF",
color: "#FFFFFF",
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
!item.chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
]}
>
{item.chaptersUnread}
</Text>
)}
</View>
</View>
</TouchableRipple>
);
};
+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,
},
});
-41
View File
@@ -1,41 +0,0 @@
import React from "react";
import { Checkbox } from "react-native-paper";
import { useSelector } from "react-redux";
export const DisplayCheckbox = ({ displayMode, onPress, value }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const label = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
return (
<Checkbox.Item
label={label[value]}
labelStyle={{ color: theme.textColorPrimary }}
status={displayMode === value ? "checked" : "unchecked"}
mode="ios"
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccentDark}
onPress={onPress}
/>
);
};
export const ThemeCheckbox = ({ onPress, label, checked }) => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<Checkbox.Item
label={label}
labelStyle={{ color: theme.textColorPrimary }}
status={checked ? "checked" : "unchecked"}
mode="ios"
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccentDark}
onPress={onPress}
/>
);
};
+6
View File
@@ -14,6 +14,10 @@ import {
createChapterIdIndexQuery,
} from "./tables/HistoryTable";
import { createDownloadTableQuery } from "./tables/DownloadTable";
import {
createUpdatesTableQuery,
updatesSeedDataQuery,
} from "./tables/UpdateTable";
/**
* Database Version = 2
@@ -29,6 +33,7 @@ export const createDB = () => {
tx.executeSql(createChapterTableQuery);
tx.executeSql(createHistoryTableQuery);
tx.executeSql(createDownloadTableQuery);
tx.executeSql(createUpdatesTableQuery);
//Db indexes
tx.executeSql(createUrlIndexQuery);
@@ -45,5 +50,6 @@ export const deleteDb = () => {
tx.executeSql("DROP TABLE chapters");
tx.executeSql("DROP TABLE history");
tx.executeSql("DROP TABLE downloads");
tx.executeSql("DROP TABLE updates");
});
};
+54 -11
View File
@@ -5,24 +5,45 @@ const insertChaptersQuery = `INSERT INTO chapters (chapterUrl, chapterName, rele
export const insertChapters = async (novelId, chapters) => {
db.transaction((tx) => {
chapters.map((chapter) =>
tx.executeSql(insertChaptersQuery, [
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
novelId,
])
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 = `SELECT * FROM chapters WHERE novelId = ? ORDER BY chapterId ASC`;
const getChaptersQuery = (sort, filter) =>
`SELECT * FROM chapters WHERE novelId = ? ${filter ?? ""} ${
sort ?? "ORDER BY chapterId ASC"
}`;
export const getChapters = (novelId) => {
export const getChapters = (novelId, sort, filter) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getChaptersQuery,
getChaptersQuery(sort, filter),
[novelId],
(txObj, { rows: { _array } }) => {
resolve(_array);
@@ -92,7 +113,9 @@ export const downloadChapter = async (
chapterUrl,
chapterId
) => {
const downloadUrl = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
// console.log(downloadUrl);
const response = await fetch(downloadUrl);
const chapter = await response.json();
@@ -138,6 +161,26 @@ export const deleteChapter = async (chapterId) => {
});
};
// export const deleteAllChapters = async (novelId) => {
// const updateIsDownloadedQuery = `UPDATE chapters SET downloaded = 0 WHERE novelId=?`;
// const deleteChapterQuery = `DELETE FROM downloads WHERE downloadChapterId=?`;
// db.transaction((tx) => {
// tx.executeSql(
// updateIsDownloadedQuery,
// [chapterId],
// (tx, res) => console.log(`Chaptermarked not downapdeod deleted`),
// (txObj, error) => console.log("Error ", error)
// );
// tx.executeSql(
// deleteChapterQuery,
// [chapterId],
// (tx, res) => console.log(`Chapter deleted`),
// (txObj, error) => console.log("Error ", error)
// );
// });
// };
const getLastReadChapterQuery = `
SELECT chapters.*
FROM history
+1 -1
View File
@@ -30,7 +30,7 @@ export const getHistory = async () => {
export const insertHistory = async (novelId, chapterId) => {
db.transaction((tx) => {
tx.executeSql(
"INSERT OR REPLACE INTO history (historyNovelId, historyChapterId) VALUES (?, ?)",
"INSERT OR REPLACE INTO history (historyNovelId, historyChapterId, historyTimeRead) VALUES (?, ?, (datetime('now','localtime')))",
[novelId, chapterId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
+20 -1
View File
@@ -44,7 +44,26 @@ export const searchLibrary = (searchText) => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
`SELECT * FROM novels WHERE followed = 1 AND novelName LIKE '%${searchText}%'`,
`
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)
+1 -1
View File
@@ -79,7 +79,7 @@ export const deleteNovelCache = () => {
"DELETE FROM novels WHERE followed = 0",
null,
(txObj, res) => {
console.log("Deleted");
console.log("Cleared Novel Cache");
},
(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)
);
});
};
+1 -1
View File
@@ -6,6 +6,6 @@ export const createDownloadTableQuery = `
chapterText TEXT,
nextChapter TEXT,
prevChapter TEXT,
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapter_id)
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
ON DELETE CASCADE
)`;
+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
)`;
+16 -31
View File
@@ -10,19 +10,19 @@ import { setStatusBarStyle } from "expo-status-bar";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import Library from "../screens/Library/Library";
import Updates from "../screens/Updates/Updates";
import History from "../screens/History/History";
import Browse from "../screens/Browse/Browse";
import More from "../screens/More/More";
import About from "../screens/More/About";
import Settings from "../screens/More/Settings";
import Trackers from "../screens/More/Trackers";
import NovelItem from "../screens/Novel/Novel";
import ChapterItem from "../screens/Chapter/Chapter";
// Extensions
import BoxNovel from "../screens/Extensions/boxnovel/BoxNovel";
import ReadLightNovel from "../screens/Extensions/readlightnovel/ReadLightNovel";
import Extension from "../screens/Extension/Extension";
const Stack = createStackNavigator();
@@ -33,28 +33,12 @@ const stackNavigatorConfig = {
...TransitionPresets.RevealFromBottomAndroid,
};
const MoreStack = () => {
const SettingsStack = () => {
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.Screen name="Trackers" component={Trackers} />
</Stack.Navigator>
);
};
@@ -83,7 +67,7 @@ const BottomNavigator = () => {
),
}}
/>
{/*
<Tab.Screen
name="Updates"
component={Updates}
@@ -96,7 +80,7 @@ const BottomNavigator = () => {
/>
),
}}
/> */}
/>
<Tab.Screen
name="History"
component={History}
@@ -125,7 +109,7 @@ const BottomNavigator = () => {
/>
<Tab.Screen
name="More"
component={MoreStack}
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
@@ -141,9 +125,9 @@ const BottomNavigator = () => {
};
const Router = () => {
const themeCode = useSelector((state) => state.themeReducer.themeCode);
const theme = useSelector((state) => state.themeReducer.theme);
useEffect(() => setStatusBarStyle(themeCode === 1 ? "dark" : "light"), []);
useEffect(() => setStatusBarStyle(theme.id === 1 ? "dark" : "light"), []);
return (
<Provider>
@@ -164,11 +148,12 @@ const Router = () => {
headerTintColor: "white",
}}
/>
<Stack.Screen name="BoxNovelStack" component={BoxNovelStack} />
<Stack.Screen
name="ReadLightNovelStack"
component={ReadLightNovelStack}
/>
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="Extension" component={Extension} />
{/* <Stack.Screen
name="ExtensionStack"
component={ExtensionStack}
/> */}
</Stack.Navigator>
</Provider>
);
+3 -5
View File
@@ -27,14 +27,12 @@ export const getChapterAction = (
) => async (dispatch) => {
dispatch({ type: CHAPTER_LOADING });
const isDownloaded = await isChapterDownloaded(novelId);
let chapter;
let chapter = await getChapter(novelId);
if (isDownloaded) {
chapter = await getChapter(novelId);
} else {
if (!chapter) {
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
}
console.log(isDownloaded);
dispatch({ type: GET_CHAPTER, payload: chapter });
};
+5 -3
View File
@@ -1,4 +1,7 @@
import { GET_LIBRARY_NOVELS } from "./library.types";
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
} from "./library.types";
import {
getLibrary,
searchLibrary,
@@ -14,11 +17,10 @@ export const getLibraryAction = () => async (dispatch) => {
};
export const searchLibraryAction = (searchText) => async (dispatch) => {
console.log(searchText);
const res = await searchLibrary(searchText);
dispatch({
type: GET_LIBRARY_NOVELS,
type: GET_LIBRARY_SEARCH_RESULTS,
payload: res,
});
};
+27 -12
View File
@@ -1,8 +1,16 @@
import { FOLLOW_NOVEL, UNFOLLOW_NOVEL } from "../novel/novel.types";
import { GET_LIBRARY_NOVELS } from "./library.types";
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,
};
@@ -11,18 +19,25 @@ const libraryReducer = (state = initialState, action) => {
switch (type) {
case GET_LIBRARY_NOVELS:
return { novels: payload, loading: false };
case FOLLOW_NOVEL:
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, { ...payload, followed: 1 }],
};
case UNFOLLOW_NOVEL:
return {
...state,
novels: state.novels.filter(
(novel) => novel.novelId !== payload
),
novels: [...state.novels, { novelUrl: payload.novelUrl }],
};
default:
return state;
+1
View File
@@ -1 +1,2 @@
export const GET_LIBRARY_NOVELS = "GET_LIBRARY_NOVELS";
export const GET_LIBRARY_SEARCH_RESULTS = "GET_LIBRARY_SEARCH_RESULTS";
+68 -40
View File
@@ -14,9 +14,6 @@ import {
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
UPDATE_NOVEL,
UPDATE_LAST_READ,
FOLLOW_NOVEL,
UNFOLLOW_NOVEL,
} from "./novel.types";
import { updateNovel } from "../../services/updates";
@@ -24,7 +21,6 @@ import { fetchChapter, fetchNovel } from "../../source/Source";
import {
followNovel,
insertNovel,
checkNovelInCache,
getNovel,
} from "../../database/queries/NovelQueries";
import {
@@ -35,20 +31,25 @@ import {
getChapter,
downloadChapter,
deleteChapter,
getLastReadChapter,
} 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) => async (
dispatch
) => {
export const getNovelAction = (
followed,
sourceId,
novelUrl,
novelId,
sort,
filter
) => async (dispatch) => {
dispatch({ type: LOADING_NOVEL });
if (followed === 1) {
const chapters = await getChapters(novelId);
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
@@ -57,11 +58,10 @@ export const getNovelAction = (followed, sourceId, novelUrl, novelId) => async (
} else {
dispatch({ type: FETCHING_NOVEL });
const inCache = await checkNovelInCache(novelUrl);
const novel = await getNovel(novelUrl);
if (inCache) {
const novel = await getNovel(novelUrl);
novel.chapters = await getChapters(novel.novelId);
if (novel) {
novel.chapters = await getChapters(novel.novelId, sort, filter);
dispatch({
type: GET_NOVEL,
payload: novel,
@@ -105,21 +105,13 @@ export const sortAndFilterChapters = (novelUrl, filter, sort) => async (
export const followNovelAction = (novel) => async (dispatch) => {
await followNovel(novel.followed, novel.novelId);
if (!novel.followed) {
dispatch({
type: FOLLOW_NOVEL,
payload: novel,
});
} else {
dispatch({
type: UNFOLLOW_NOVEL,
payload: novel.novelId,
});
if (!novel.followNovel) {
deleteNovelUpdates(novel.novelId);
}
dispatch({
type: UPDATE_IN_LIBRARY,
payload: !novel.followed,
payload: { novelUrl: novel.novelUrl, followed: !novel.followed },
});
ToastAndroid.show(
@@ -161,21 +153,50 @@ export const downloadChapterAction = (
) => async (dispatch) => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: {
chapterId,
},
payload: chapterId,
});
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
payload: { chapterId },
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
) => {
@@ -183,30 +204,37 @@ export const deleteChapterAction = (chapterId, chapterName) => async (
dispatch({
type: CHAPTER_DELETED,
payload: { chapterId },
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 });
const updatedNovel = await updateNovel(extensionId, novelUrl, novelId);
await updateNovel(extensionId, novelUrl, novelId);
let novel = await getNovel(novelUrl);
let chapters = await getChapters(novel.novelId);
dispatch({
type: UPDATE_NOVEL,
payload: { novel: updatedNovel, chapters: updatedNovel.chapters },
});
};
export const getLastReadChapterAction = (novelId) => async (dispatch) => {
const lastReadChapter = await getLastReadChapter(novelId);
dispatch({
type: UPDATE_LAST_READ,
payload: lastReadChapter,
payload: { novel, chapters },
});
};
+10 -15
View File
@@ -53,21 +53,12 @@ const novelReducer = (state = initialState, action) => {
case UPDATE_IN_LIBRARY:
return {
...state,
novel: { ...state.novel, followed: payload },
novel: { ...state.novel, followed: payload.followed },
};
case UPDATE_NOVEL:
return {
...state,
novel: {
...state.novel,
novelName: payload.novel.novelName,
novelUrl: payload.novel.novelUrl,
novelSummary: payload.novel.novelSummary,
author: payload.novel.author,
artist: payload.novel.artist,
genre: payload.novel.genre,
status: payload.novel.status,
},
novel: payload.novel,
chapters: payload.chapters,
loading: false,
fetching: false,
@@ -91,14 +82,18 @@ const novelReducer = (state = initialState, action) => {
case CHAPTER_DOWNLOADING:
return {
...state,
downloading: payload.chapterId,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload
? { ...chapter, downloaded: 3 }
: chapter
),
};
case CHAPTER_DOWNLOADED:
return {
...state,
downloading: null,
// downloading: state.downloading.filter((id) => id === payload),
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
chapter.chapterId === payload
? { ...chapter, downloaded: 1 }
: chapter
),
@@ -107,7 +102,7 @@ const novelReducer = (state = initialState, action) => {
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
chapter.chapterId === payload
? { ...chapter, downloaded: 0 }
: chapter
),
+24 -1
View File
@@ -3,6 +3,9 @@ import {
SET_ITEMS_PER_ROW,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
UPDATE_READER_TEXT_ALIGN,
UPDATE_READER_PADDING,
SET_READER_FONT,
} from "./settings.types";
export const setDisplayMode = (val) => (dispatch) => {
@@ -13,7 +16,6 @@ export const setDisplayMode = (val) => (dispatch) => {
};
export const setItemsPerRow = (val) => (dispatch) => {
console.log("called" + val);
dispatch({
type: SET_ITEMS_PER_ROW,
payload: val,
@@ -33,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,
});
};
+21
View File
@@ -1,6 +1,9 @@
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";
@@ -11,6 +14,9 @@ const initialState = {
reader: {
theme: 1,
textSize: 16,
textAlign: "left",
padding: 5,
fontFamily: null,
},
};
@@ -48,6 +54,21 @@ const settingsReducer = (state = initialState, action) => {
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;
}
+3
View File
@@ -2,3 +2,6 @@ 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";
+4
View File
@@ -9,6 +9,8 @@ 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,
})
);
-7
View File
@@ -1,12 +1,5 @@
import { SWITCH_THEME } from "./theme.types";
/**
* 0 -> Amoled Dark Theme
* 1 -> Light Theme
* 2 -> Dark Theme
* 3 -> Midnight Dusk Theme
*/
export const switchTheme = (val) => (dispatch) => {
dispatch({
type: SWITCH_THEME,
+1 -2
View File
@@ -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;
}
+57
View File
@@ -0,0 +1,57 @@
import { findListItem, updateItem } from "../../trackers/MyAnimeList";
import {
REMOVE_TRACKER,
SET_TRACKER,
TRACK_NOVEL,
UNTRACK_NOVEL,
UPDATE_TRACKER,
UPDATE_CHAPTERS_READ,
} from "./tracker.types";
import { updateMalChaptersRead } from "../../trackers/MyAnimeList";
export const setTracker = (res) => async (dispatch) => {
dispatch({
type: SET_TRACKER,
payload: res,
});
};
export const removeTracker = () => async (dispatch) => {
dispatch({ type: REMOVE_TRACKER });
};
export const trackNovel = (trackObj, accessToken) => async (dispatch) => {
let res = await findListItem(trackObj.id, accessToken);
trackObj.num_chapters = res.num_chapters;
trackObj.my_list_status = {
num_chapters_read: res.my_list_status.num_chapters_read,
score: res.my_list_status.score,
status: res.my_list_status.status,
};
dispatch({ type: TRACK_NOVEL, payload: trackObj });
};
export const updateTracker = (malId, accessToken, body) => async (dispatch) => {
let res = await updateItem(malId, accessToken, body);
dispatch({ type: UPDATE_TRACKER, payload: { malId, ...res } });
};
export const untrackNovel = (id) => async (dispatch) => {
dispatch({ type: UNTRACK_NOVEL, payload: id });
};
export const updateChaptersRead = (malId, accessToken, chaptersRead) => async (
dispatch
) => {
let res = await updateMalChaptersRead(malId, accessToken, {
num_chapters_read: chaptersRead,
});
dispatch({
type: UPDATE_CHAPTERS_READ,
payload: { malId, chaptersRead: res },
});
};
+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";
+9
View File
@@ -0,0 +1,9 @@
import { GET_UPDATES } from "./updates.types";
import { getUpdates } from "../../database/queries/UpdateQueries";
export const getUpdatesAction = () => async (dispatch) => {
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";
+2 -3
View File
@@ -1,6 +1,6 @@
import React, { useEffect } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar } from "../../components/common/Appbar";
import { Appbar } from "../../components/Appbar";
import { connect } from "react-redux";
import { getSourcesAction } from "../../redux/source/source.actions";
@@ -10,7 +10,7 @@ import ExtensionCard from "./components/ExtensionCard";
const Browse = ({ theme, extensions, loading, getSourcesAction }) => {
useEffect(() => {
getSourcesAction();
}, []);
}, [getSourcesAction]);
return (
<>
@@ -51,6 +51,5 @@ export default connect(mapStateToProps, { getSourcesAction })(Browse);
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 4,
},
});
+33 -15
View File
@@ -6,7 +6,7 @@ import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
const ExtensionCard = ({ item }) => {
const { sourceName, sourceCover, sourceLanguage } = item;
const { sourceName, sourceCover, sourceLanguage, status } = item;
const navigation = useNavigation();
@@ -15,15 +15,19 @@ const ExtensionCard = ({ item }) => {
return (
<TouchableRipple
style={styles.extensionCard}
onPress={() => navigation.navigate(sourceName + "Stack")}
onPress={() =>
navigation.navigate("Extension", {
sourceId: item.sourceId,
sourceName: item.sourceName,
sourceUrl: item.sourceUrl,
})
}
rippleColor={theme.rippleColor}
borderless
>
<>
<Image
source={{
uri: sourceCover,
}}
source={{ uri: sourceCover }}
style={styles.extensionIcon}
resizeMode="contain"
/>
@@ -37,14 +41,25 @@ 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={{
color: "#D63B2F",
fontSize: 12,
marginLeft: 5,
}}
>
{status === 0 && "(Not Working)"}
</Text>
</View>
</View>
<View>
<Button
@@ -54,7 +69,11 @@ const ExtensionCard = ({ item }) => {
uppercase={false}
color={theme.colorAccentDark}
onPress={() =>
navigation.navigate(sourceName + "Stack")
navigation.navigate("Extension", {
sourceId: item.sourceId,
sourceName: item.sourceName,
sourceUrl: item.sourceUrl,
})
}
>
Browse
@@ -75,7 +94,6 @@ const styles = StyleSheet.create({
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
borderRadius: 4,
},
extensionIcon: {
width: 40,
+47 -4
View File
@@ -12,7 +12,9 @@ import {
markChapterReadAction,
} from "../../redux/novel/novel.actions";
import { connect } from "react-redux";
import { connect, useSelector, useDispatch } from "react-redux";
import { parseChapterNumber } from "../../services/updates";
import { updateChaptersRead } from "../../redux/tracker/tracker.actions";
const ChapterItem = ({
route,
@@ -35,6 +37,19 @@ const ChapterItem = ({
let _panel = useRef(null);
const dispatch = useDispatch();
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
const tracker = useSelector((state) => state.trackerReducer.tracker);
let isTracked = false;
if (!loading) {
isTracked = trackedNovels.find((obj) => obj.novelId === novelId);
}
useEffect(() => {
getChapterAction(extensionId, chapterUrl, novelUrl, chapterId);
insertHistoryAction(novelId, chapterId);
@@ -56,10 +71,14 @@ const ChapterItem = ({
return textColor;
};
const chapterNumber = !loading && parseChapterNumber(chapter.chapterName);
const readerLineHeight = (val) => {
const lineHeight = {
12: 20,
14: 22,
16: 25,
18: 26,
20: 28,
};
return lineHeight[val];
@@ -77,6 +96,8 @@ const ChapterItem = ({
);
};
// !loading && parseChapterNumber(chapter.chapterName);
return (
<Provider>
<CollapsibleHeaderScrollView
@@ -105,10 +126,12 @@ const ChapterItem = ({
size={26}
disabled={!chapter.prevChapter}
onPress={() => {
navigation.push("ChapterItem", {
navigation.replace("ChapterItem", {
chapterUrl: chapter.prevChapter,
chapterId: chapterId - 1,
extensionId,
novelUrl,
novelId,
chapterName: chapter.chapterName,
});
}}
@@ -119,10 +142,12 @@ const ChapterItem = ({
size={26}
disabled={!chapter.nextChapter}
onPress={() => {
navigation.push("ChapterItem", {
navigation.replace("ChapterItem", {
chapterUrl: chapter.nextChapter,
extensionId,
novelUrl,
novelId,
chapterId: chapterId + 1,
chapterName: chapter.chapterName,
});
}}
@@ -148,6 +173,20 @@ const ChapterItem = ({
onScroll={({ nativeEvent }) => {
if (isCloseToBottom(nativeEvent)) {
markChapterReadAction(chapterId);
if (
isTracked &&
chapterNumber &&
chapterNumber >
isTracked.my_list_status.num_chapters_read
) {
dispatch(
updateChaptersRead(
isTracked.id,
tracker.access_token,
chapterNumber
)
);
}
}
}}
>
@@ -166,6 +205,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,
},
]}
>
@@ -196,7 +240,6 @@ export default connect(mapStateToProps, {
const styles = StyleSheet.create({
container: {
flexGrow: 1,
paddingHorizontal: 10,
paddingVertical: 10,
},
});
+195 -40
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 "../../../utils/constants";
import { connect } from "react-redux";
import {
setReaderFont,
updateReaderPadding,
updateReaderTextAlign,
updateReaderTextSize,
updateReaderTheme,
} from "../../../redux/settings/settings.actions";
import BottomSheetHandle from "../../../components/common/BottomSheetHandle";
import BottomSheetHandle from "../../../components/BottomSheetHandle";
const ChapterBottomSheet = ({
bottomSheetRef,
@@ -19,13 +22,16 @@ 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}>
@@ -33,7 +39,7 @@ const ChapterBottomSheet = ({
<View style={styles.readerSettingsContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
fontWeight: "bold",
}}
>
@@ -47,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>
@@ -108,6 +260,9 @@ const mapStateToProps = (state) => ({
export default connect(mapStateToProps, {
updateReaderTextSize,
updateReaderTheme,
updateReaderTextAlign,
updateReaderPadding,
setReaderFont,
})(ChapterBottomSheet);
const styles = StyleSheet.create({
@@ -1,14 +1,17 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Provider } from "react-native-paper";
import * as WebBrowser from "expo-web-browser";
import NovelCover from "../../../components/common/NovelCover";
import { SearchAppbar } from "../../../components/common/Appbar";
import NovelCover from "../../components/NovelCover";
import EmptyView from "../../components/EmptyView";
import { SearchAppbar } from "../../components/Appbar";
import { useSelector } from "react-redux";
import EmptyView from "../../../components/common/EmptyView";
const ReadLightNovel = ({ navigation }) => {
const Extension = ({ navigation, route }) => {
const { sourceId, sourceName, sourceUrl } = route.params;
const theme = useSelector((state) => state.themeReducer.theme);
const library = useSelector((state) => state.libraryReducer.novels);
const itemsPerRow = useSelector(
@@ -16,13 +19,28 @@ const ReadLightNovel = ({ navigation }) => {
);
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState();
const [searchText, setSearchText] = useState("");
const getSourceUrl = () => {
if (sourceId === 1) {
return `https://lnreader-extensions.vercel.app/api/1/novels/1/?o=rating`;
} else {
return `https://lnreader-extensions.vercel.app/api/${sourceId}/novels/`;
}
};
const getSearchUrl = () => {
if (sourceId === 1) {
return `https://lnreader-extensions.vercel.app/api/1/search/?s=${searchText}&?o=rating`;
} else {
return `https://lnreader-extensions.vercel.app/api/${sourceId}/search/?s=${searchText}`;
}
};
const getNovels = () => {
fetch(`https://lnreader-extensions.herokuapp.com/api/2/novels/`)
fetch(getSourceUrl())
.then((response) => response.json())
.then((json) => {
setNovels(json);
@@ -32,9 +50,7 @@ const ReadLightNovel = ({ navigation }) => {
const getSearchResults = (searchText) => {
setLoading(true);
fetch(
`https://lnreader-extensions.herokuapp.com/api/2/search/?s=${searchText}`
)
fetch(getSearchUrl())
.then((response) => response.json())
.then((json) => {
setNovels(json);
@@ -60,12 +76,13 @@ const ReadLightNovel = ({ navigation }) => {
>
<SearchAppbar
screen="Extension"
placeholder="Search ReadLightNovel"
placeholder={`Search ${sourceName}`}
searchText={searchText}
setSearchText={setSearchText}
getSearchResults={getSearchResults}
getNovels={getNovels}
setLoading={setLoading}
openWebView={() => WebBrowser.openBrowserAsync(sourceUrl)}
/>
{loading ? (
@@ -94,15 +111,10 @@ const ReadLightNovel = ({ navigation }) => {
sourceId: item.extensionId,
})
}
libraryStatus={
checkIFInLibrary(item.novelUrl)
? true
: false
}
libraryStatus={checkIFInLibrary(item.novelUrl)}
/>
)}
ListEmptyComponent={
searchText !== "" &&
novels.length === 0 && (
<EmptyView
icon="(; ̄Д ̄)"
@@ -117,7 +129,7 @@ const ReadLightNovel = ({ navigation }) => {
);
};
export default ReadLightNovel;
export default Extension;
const styles = StyleSheet.create({
container: {
+2 -2
View File
@@ -3,11 +3,11 @@ import { useFocusEffect } from "@react-navigation/native";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar } from "../../components/common/Appbar";
import { Appbar } from "../../components/Appbar";
import HistoryCard from "./components/HistoryCard";
import { connect } from "react-redux";
import EmptyView from "../../components/common/EmptyView";
import EmptyView from "../../components/EmptyView";
import {
getHistoryAction,
@@ -49,7 +49,7 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation }) => {
numberOfLines={1}
>
{`${item.chapterName} - ${moment(
item.lastRead
item.historyTimeRead
).calendar()}`}
</Text>
</View>
@@ -75,11 +75,11 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation }) => {
style={{ marginRight: 10 }}
onPress={() =>
navigation.navigate("ChapterItem", {
chapterId: item.historyChapterId,
chapterUrl: item.chapterUrl,
extensionId: item.extensionId,
extensionId: item.sourceId,
novelUrl: item.novelUrl,
novelName: item.novelName,
novelCover: item.novelCover,
novelId: item.novelId,
chapterName: item.chapterName,
})
}
+11 -7
View File
@@ -11,15 +11,16 @@ import { FlatList } from "react-native-gesture-handler";
import { useFocusEffect } from "@react-navigation/native";
import { connect } from "react-redux";
import NovelCover from "../../components/common/NovelCover";
import EmptyView from "../../components/common/EmptyView";
import NovelCover from "../../components/NovelCover";
import EmptyView from "../../components/EmptyView";
import {
getLibraryAction,
searchLibraryAction,
} from "../../redux/library/library.actions";
import { SearchAppbar } from "../../components/common/Appbar";
import { SearchAppbar } from "../../components/Appbar";
import { setNovel } from "../../redux/novel/novel.actions";
import { updateAllNovels } from "../../services/updates";
const LibraryScreen = ({
navigation,
@@ -35,9 +36,12 @@ const LibraryScreen = ({
const [searchText, setSearchText] = useState("");
useEffect(() => {
getLibraryAction();
}, [getLibraryAction]);
useFocusEffect(
useCallback(() => {
setSearchText("");
getLibraryAction();
}, [getLibraryAction])
);
/**
* TODO: fix refreshing
@@ -45,7 +49,7 @@ const LibraryScreen = ({
const onRefresh = () => {
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
setRefreshing(true);
getLibraryAction();
updateAllNovels();
setRefreshing(false);
};
+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.10"
/>
<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.15"
theme={theme}
/>
<ListItem
title="Build Time"
description="30-04-21 11:00 AM"
theme={theme}
/>
<ListItem
title="What's new"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader/commits/main"
)
}
theme={theme}
/>
<Divider theme={theme} />
<ListItem
title="Discord"
description="https://discord.gg/QdcWN4MD63"
onPress={() =>
Linking.openURL("https://discord.gg/QdcWN4MD63")
}
theme={theme}
/>
<ListItem
title="Github"
description="https://github.com/rajarsheechatterjee/lnreader"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader"
)
}
theme={theme}
/>
<ListItem
title="Extensions"
description="https://github.com/rajarsheechatterjee/lnreader-extensions"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader-extensions"
)
}
theme={theme}
/>
</ListSection>
</View>
</>
);
};
+58 -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("SettingsStack", {
screen: "Settings",
})
}
theme={theme}
/>
<ListItem
title="Tracking"
icon="sync"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "Trackers",
})
}
theme={theme}
/>
<ListItem
title="About"
icon="information-outline"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "About",
})
}
theme={theme}
/>
</ListSection>
</View>
</>
);
};
+75 -213
View File
@@ -1,25 +1,25 @@
import React, { useState } from "react";
import { View, Text } from "react-native";
import { setStatusBarStyle } from "expo-status-bar";
import { List, Modal, Portal } from "react-native-paper";
import InputSpinner from "react-native-input-spinner";
import { View } from "react-native";
import { connect } from "react-redux";
import { switchTheme } from "../../redux/theme/theme.actions";
import {
setDisplayMode,
setItemsPerRow,
} from "../../redux/settings/settings.actions";
import { Appbar } from "../../components/common/Appbar";
import {
DisplayCheckbox,
ThemeCheckbox,
} from "../../components/settings/Checkbox";
import { deleteAllHistory } from "../../database/queries/HistoryQueries";
import { deleteNovelCache } from "../../database/queries/NovelQueries";
// import { deleteDb } from "../../database/DBHelper";
import { Appbar } from "../../components/Appbar";
import {
Divider,
ListItem,
ListSection,
ListSubHeader,
} from "../../components/List";
import DisplayModeModal from "./components/DisplayModeModal";
import GridSizeModal from "./components/GridSizeModal";
import ThemeModal from "./components/ThemeModal";
const SettingsScreen = ({
navigation,
@@ -31,242 +31,104 @@ const SettingsScreen = ({
setDisplayMode,
setItemsPerRow,
}) => {
const display = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
const displayModeLabel = (displayMode) => {
const label = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
return label[displayMode];
};
const themes = [
{ themeCode: 0, label: "AMOLED Dark Theme", statusBar: "light" },
{ themeCode: 1, label: "Light Theme", statusBar: "dark" },
{ themeCode: 2, label: "Dark Theme", statusBar: "light" },
{ themeCode: 3, label: "Midnight Dusk Theme", statusBar: "light" },
];
const desciptionStyles = {
color: theme.textColorSecondary,
};
const titleStyles = { color: theme.textColorPrimary };
/**
* Display Mode Modal
*/
// Display Mode Modal
const [displayModalVisible, setDisplayModalVisible] = useState(false);
const showDisplayModal = () => setDisplayModalVisible(true);
const hideDisplayModal = () => setDisplayModalVisible(false);
/**
* Change Theme Modal
*/
// Theme Modal
const [themeModalVisible, setthemeModalVisible] = useState(false);
const showthemeModal = () => setthemeModalVisible(true);
const hidethemeModal = () => setthemeModalVisible(false);
const [itemsModalVisible, setitemsModalVisible] = useState(false);
const showitemsModal = () => setitemsModalVisible(true);
const hideitemsModal = () => setitemsModalVisible(false);
// Grid Size Modal
const [gridSizeModalVisible, setGridSizeModalVisible] = useState(false);
const showGridSizeModal = () => setGridSizeModalVisible(true);
const hideGridSizeModal = () => setGridSizeModalVisible(false);
return (
<>
<Appbar title="Settings" onBackAction={() => navigation.goBack()} />
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<List.Section
style={{
flex: 1,
backgroundColor: theme.colorPrimaryDark,
}}
>
<List.Subheader
style={{
color: theme.colorAccentDark,
paddingBottom: 5,
}}
>
Data
</List.Subheader>
<List.Item
titleStyle={titleStyles}
<View
style={{
flex: 1,
backgroundColor: theme.colorPrimaryDark,
paddingVertical: 8,
}}
>
<ListSection>
<ListSubHeader theme={theme}>Data</ListSubHeader>
<ListItem
title="Clear database"
descriptionStyle={desciptionStyles}
description="Delete history for novels not in your library"
onPress={() => deleteNovelCache()}
rippleColor={theme.rippleColor}
theme={theme}
/>
<List.Item
titleStyle={titleStyles}
<ListItem
title="Clear history"
descriptionStyle={desciptionStyles}
description="Delete reading history for all novels"
onPress={() => deleteAllHistory()}
rippleColor={theme.rippleColor}
theme={theme}
/>
<List.Subheader
style={{
color: theme.colorAccentDark,
paddingBottom: 5,
}}
>
Display
</List.Subheader>
<List.Item
titleStyle={titleStyles}
<Divider theme={theme} />
<ListSubHeader theme={theme}>Display</ListSubHeader>
<ListItem
title="Display Mode"
descriptionStyle={desciptionStyles}
description={display[displayMode]}
description={displayModeLabel(displayMode)}
onPress={showDisplayModal}
rippleColor={theme.rippleColor}
theme={theme}
/>
<Portal>
<Modal
visible={displayModalVisible}
onDismiss={hideDisplayModal}
contentContainerStyle={{
backgroundColor: theme.colorPrimaryDark,
padding: 20,
margin: 20,
borderRadius: 6,
}}
>
<DisplayCheckbox
value={1}
displayMode={displayMode}
onPress={() => setDisplayMode(1)}
/>
<DisplayCheckbox
value={0}
displayMode={displayMode}
onPress={() => setDisplayMode(0)}
/>
<DisplayCheckbox
value={2}
displayMode={displayMode}
onPress={() => setDisplayMode(2)}
/>
</Modal>
</Portal>
<List.Item
titleStyle={titleStyles}
<ListItem
title="Items per row in library"
descriptionStyle={desciptionStyles}
description={itemsPerRow}
onPress={showitemsModal}
rippleColor={theme.rippleColor}
description={`${itemsPerRow} items per row`}
onPress={showGridSizeModal}
theme={theme}
/>
<Portal>
<Modal
visible={itemsModalVisible}
onDismiss={hideitemsModal}
contentContainerStyle={{
backgroundColor: theme.colorPrimaryDark,
padding: 20,
margin: 20,
borderRadius: 6,
}}
>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 18,
marginBottom: 10,
}}
>
Items per row
</Text>
<InputSpinner
style={{
paddingHorizontal: 16,
paddingVertical: 16,
elevation: 0,
}}
max={10}
min={1}
skin="paper"
background={theme.colorPrimaryDark}
colorMax={theme.colorAccentDark}
colorLeft={theme.colorAccentDark}
colorRight={theme.colorAccentDark}
buttonTextColor={theme.textColorPrimary}
colorPress={"white"}
colorMin={theme.colorAccentDark}
value={itemsPerRow}
onChange={(num) => setItemsPerRow(num)}
inputStyle={{
color: theme.textColorPrimary,
}}
/>
</Modal>
</Portal>
{/* <List.Subheader
style={{
color: theme.colorAccentDark,
paddingBottom: 5,
}}
>
Devloper options
</List.Subheader>
<List.Item
titleStyle={titleStyles}
title="Delete database"
descriptionStyle={desciptionStyles}
description="Delete entire database"
rippleColor={theme.rippleColor}
onPress={() => deleteDb()}
/> */}
<List.Subheader
style={{
color: theme.colorAccentDark,
paddingBottom: 5,
}}
>
Theme
</List.Subheader>
<List.Item
titleStyle={titleStyles}
<Divider theme={theme} />
<ListSubHeader theme={theme}>Theme</ListSubHeader>
<ListItem
title="Theme"
descriptionStyle={desciptionStyles}
description={themes[themeCode].label}
description={theme.name}
onPress={showthemeModal}
rippleColor={theme.rippleColor}
theme={theme}
/>
<Portal>
<Modal
visible={themeModalVisible}
onDismiss={hidethemeModal}
contentContainerStyle={{
backgroundColor: theme.colorPrimaryDark,
padding: 20,
margin: 20,
borderRadius: 6,
}}
>
{themes.map((item, index) => (
<ThemeCheckbox
key={index}
label={themes[item.themeCode].label}
checked={
themeCode === item.themeCode
? true
: false
}
onPress={() => {
switchTheme(item.themeCode);
setStatusBarStyle(item.statusBar);
}}
/>
))}
</Modal>
</Portal>
</List.Section>
</ListSection>
<DisplayModeModal
displayMode={displayMode}
displayModalVisible={displayModalVisible}
hideDisplayModal={hideDisplayModal}
setDisplayMode={setDisplayMode}
theme={theme}
/>
<GridSizeModal
itemsPerRow={itemsPerRow}
gridSizeModalVisible={gridSizeModalVisible}
hideGridSizeModal={hideGridSizeModal}
setItemsPerRow={setItemsPerRow}
theme={theme}
/>
<ThemeModal
themeModalVisible={themeModalVisible}
hidethemeModal={hidethemeModal}
switchTheme={switchTheme}
theme={theme}
/>
</View>
</>
);
};
const mapStateToProps = (state) => ({
themeCode: state.themeReducer.themeCode,
theme: state.themeReducer.theme,
displayMode: state.settingsReducer.displayMode,
itemsPerRow: state.settingsReducer.itemsPerRow,
+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 "../../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,
},
});
+51 -31
View File
@@ -1,19 +1,25 @@
import React, { useEffect, useRef } from "react";
import { StyleSheet, View, FlatList, RefreshControl } from "react-native";
import React, { useEffect, useRef, useState } from "react";
import {
StyleSheet,
View,
FlatList,
RefreshControl,
ToastAndroid,
} from "react-native";
import { Provider, Portal } from "react-native-paper";
import ChapterCard from "./components/ChapterCard";
import NovelInfoHeader from "./components/NovelHeader";
import { BottomSheet } from "./components/BottomSheet";
import ChaptersSettingsSheet from "./components/ChaptersSettingsSheet";
import { connect } from "react-redux";
import {
getNovelAction,
followNovelAction,
sortAndFilterChapters,
updateNovelAction,
} from "../../redux/novel/novel.actions";
import TrackSheet from "./components/Tracker/TrackSheet";
const Novel = ({
route,
@@ -23,7 +29,6 @@ const Novel = ({
loading,
getNovelAction,
fetching,
followNovelAction,
sortAndFilterChapters,
updateNovelAction,
}) => {
@@ -36,11 +41,20 @@ const Novel = ({
novelId,
} = route.params;
let _panel = useRef(null); // Bottomsheet ref
let chaptersSettingsSheetRef = useRef(null);
let trackerSheetRef = useRef(null);
const [sort, setSort] = useState("ORDER BY chapterId ASC");
const [filter, setFilter] = useState("");
useEffect(() => {
getNovelAction(followed, sourceId, novelUrl, novelId);
}, []);
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter);
}, [getNovelAction, sort, filter]);
const onRefresh = async () => {
await updateNovelAction(sourceId, novelUrl, novelId);
ToastAndroid.show(`Updated ${novelName}`, ToastAndroid.SHORT);
};
const renderChapterCard = ({ item }) => (
<ChapterCard
@@ -50,10 +64,6 @@ const Novel = ({
/>
);
const onRefresh = () => {
updateNovelAction(sourceId, novelUrl, novelId);
};
return (
<Provider>
<View
@@ -64,7 +74,7 @@ const Novel = ({
>
<FlatList
data={!loading && chapters}
keyExtractor={(item) => item.chapterUrl}
keyExtractor={(item) => item.chapterId.toString()}
removeClippedSubviews={true}
maxToRenderPerBatch={5}
windowSize={15}
@@ -72,14 +82,14 @@ const Novel = ({
renderItem={renderChapterCard}
ListHeaderComponent={
<NovelInfoHeader
theme={theme}
item={{ novelName, novelCover }}
novel={novel}
noOfChapters={chapters?.length}
followNovelAction={followNovelAction}
loading={loading}
bottomSheetRef={_panel}
firstChapter={chapters[0]}
novelName={novelName}
novelCover={novelCover}
novel={novel}
chapters={chapters}
trackerSheetRef={trackerSheetRef}
chaptersSettingsSheetRef={chaptersSettingsSheetRef}
theme={theme}
/>
}
refreshControl={
@@ -91,13 +101,25 @@ const Novel = ({
/>
}
/>
<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>
);
@@ -112,13 +134,11 @@ const mapStateToProps = (state) => ({
export default connect(mapStateToProps, {
getNovelAction,
followNovelAction,
sortAndFilterChapters,
updateNovelAction,
})(Novel);
const styles = StyleSheet.create({
container: {
flex: 1,
},
container: { flex: 1 },
});
+70 -45
View File
@@ -1,6 +1,6 @@
import React from "react";
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 {
@@ -14,12 +14,77 @@ const ChapterCard = ({
chapter,
extensionId,
theme,
downloading,
// downloading,
downloadChapterAction,
deleteChapterAction,
}) => {
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}
@@ -68,47 +133,7 @@ const ChapterCard = ({
</Text>
)}
</View>
<View>
{downloading === chapter.chapterId ? (
<ActivityIndicator
color={theme.textColorHintDark}
size={25}
style={{ margin: 3.5, padding: 5 }}
/>
) : (
<IconButton
icon={
chapter.downloaded
? "check-circle"
: "arrow-down-circle-outline"
}
animated
color={
chapter.downloaded
? theme.textColorPrimary
: theme.textColorHintDark
}
size={25}
onPress={() => {
if (!chapter.downloaded) {
downloadChapterAction(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId
);
} else {
deleteChapterAction(
chapter.chapterId,
chapter.chapterName
);
}
}}
style={{ margin: 2 }}
/>
)}
</View>
<View>{displayDownloadButton()}</View>
</>
</TouchableRipple>
);
@@ -116,7 +141,7 @@ const ChapterCard = ({
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
downloading: state.novelReducer.downloading,
// downloading: state.novelReducer.downloading,
});
export default connect(mapStateToProps, {
@@ -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, sort);
};
const filterChapters = (filter) => {
setChapterFlags({ ...chapterFlags, filter: filter });
sortAndFilterChapters(novelUrl, 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,8 +45,8 @@ export const BottomSheet = ({
</List.Subheader>
<View>
<RadioButton.Group
onValueChange={(newValue) => sortChapters(newValue)}
value={chapterFlags.sort}
onValueChange={(newValue) => setSort(newValue)}
value={sort}
>
<View
style={{
@@ -88,7 +82,7 @@ export const BottomSheet = ({
<RadioButton.Item
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
value=""
value="ORDER BY chapterId ASC"
/>
<Text
style={{
@@ -113,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
@@ -127,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
@@ -142,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
@@ -157,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
@@ -174,6 +162,8 @@ export const BottomSheet = ({
);
};
export default ChaptersSettingsSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
@@ -0,0 +1,53 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Chip } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useDispatch } from "react-redux";
import { followNovelAction } from "../../../../redux/novel/novel.actions";
const FollowChip = ({ followed, novel, theme }) => {
const dispatch = useDispatch();
const getIconName = () => (followed ? "heart" : "heart-outline");
const getChipLabel = () => (followed ? "In Library" : "Add to library");
return (
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={getIconName()}
size={21}
color={theme.colorAccentDark}
/>
)}
onPress={() => dispatch(followNovelAction(novel))}
style={[
{ backgroundColor: theme.colorPrimaryDark },
styles.toggleFavourite,
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimary,
}}
>
{getChipLabel()}
</Chip>
);
};
export default FollowChip;
const styles = StyleSheet.create({
toggleFavourite: {
height: 32,
justifyContent: "center",
alignItems: "center",
marginLeft: 16,
paddingLeft: 4,
borderWidth: 0,
elevation: 0,
},
});
@@ -0,0 +1,30 @@
import React from "react";
import { Text, StyleSheet } from "react-native";
export const GenreChip = ({ children, theme }) => (
<Text
style={[
styles.genreChip,
{
color: theme.colorAccentDark,
borderColor: theme.colorAccentDark,
backgroundColor: theme.colorPrimary,
},
]}
>
{children}
</Text>
);
const styles = StyleSheet.create({
genreChip: {
flex: 1,
justifyContent: "center",
paddingVertical: 2,
paddingHorizontal: 10,
marginHorizontal: 2,
fontSize: 13,
borderWidth: 1,
borderRadius: 50,
},
});
@@ -0,0 +1,24 @@
import React from "react";
import { View, ImageBackground, StyleSheet } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
const NovelCoverImage = ({ children, source, theme }) => {
return (
<ImageBackground source={source} style={styles.background}>
<LinearGradient
colors={["rgba(0,0,0,0.2)", theme.colorPrimaryDark]}
style={styles.linearGradient}
>
{children}
</LinearGradient>
</ImageBackground>
);
};
export default NovelCoverImage;
const styles = StyleSheet.create({
background: { height: 285 },
linearGradient: { height: 286 },
});
@@ -0,0 +1,66 @@
import React, { useState } from "react";
import { View, Text, StyleSheet } from "react-native";
const NovelSummary = ({ summary, followed, theme }) => {
const [more, showMore] = useState(!followed);
return (
summary !== "" && (
<View style={styles.summaryContainer}>
<Text
style={[
{ color: theme.textColorPrimary },
styles.summaryHeader,
]}
>
About
</Text>
<Text
style={{
color: theme.textColorSecondary,
lineHeight: 20,
}}
numberOfLines={more ? 200 : 2}
onPress={() => showMore(!more)}
ellipsizeMode="clip"
>
{summary}
</Text>
<Text
style={[
{
color: theme.colorAccentDark,
backgroundColor: theme.colorPrimaryDark,
},
styles.showMoreButton,
]}
onPress={() => showMore(!more)}
>
{more ? "Less" : "More"}
</Text>
</View>
)
);
};
export default NovelSummary;
const styles = StyleSheet.create({
summaryContainer: {
paddingHorizontal: 16,
marginBottom: 8,
},
summaryHeader: {
marginTop: 5,
paddingVertical: 5,
fontSize: 15,
fontWeight: "bold",
},
showMoreButton: {
fontWeight: "bold",
position: "absolute",
bottom: 0,
right: 18,
paddingLeft: 6,
},
});
@@ -0,0 +1,57 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Button } from "react-native-paper";
const ReadButton = ({ navigation, novel, chapters, theme }) => {
const lastRead = chapters.find((obj) => obj.read === 0);
return (
chapters.length > 0 &&
(lastRead ? (
<Button
color="white"
style={[
{ backgroundColor: theme.colorAccentDark },
styles.startButton,
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
onPress={() => {
navigation.navigate("ChapterItem", {
chapterId: lastRead.chapterId,
chapterUrl: lastRead.chapterUrl,
novelUrl: novel.novelUrl,
novelId: lastRead.novelId,
extensionId: novel.sourceId,
chapterName: lastRead.chapterName,
});
}}
>
{novel.unread ? `Start reading ` : `Continue reading `}
{lastRead.chapterName}
</Button>
) : (
<Button
color={theme.textColorHintDark}
style={[
styles.startButton,
{ backgroundColor: theme.colorDisabled },
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
>
All chapters read
</Button>
))
);
};
export default ReadButton;
const styles = StyleSheet.create({
startButton: {
marginTop: 8,
marginBottom: 16,
marginHorizontal: 16,
},
});
@@ -0,0 +1,48 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Chip } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
const TrackerChip = ({ isTracked, trackerSheetRef, theme }) => {
const getIconName = () => (isTracked ? "check" : "sync");
const getChipLabel = () => (isTracked ? "Tracked" : "Tracking");
return (
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={getIconName(isTracked)}
size={21}
color={theme.colorAccentDark}
/>
)}
onPress={() => trackerSheetRef.current.show()}
style={[
styles.toggleFavourite,
{ backgroundColor: theme.colorPrimaryDark },
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimary,
}}
>
{getChipLabel()}
</Chip>
);
};
export default TrackerChip;
const styles = StyleSheet.create({
toggleFavourite: {
height: 32,
justifyContent: "center",
alignItems: "center",
paddingLeft: 4,
marginLeft: 0,
borderWidth: 0,
elevation: 0,
},
});
+156 -248
View File
@@ -1,151 +1,125 @@
import React, { useState } from "react";
import {
View,
Text,
ImageBackground,
StyleSheet,
Image,
FlatList,
Share,
} from "react-native";
import { TouchableRipple, IconButton, Button, Chip } from "react-native-paper";
import { View, Text, StyleSheet, Image, FlatList, Share } from "react-native";
import { TouchableRipple, IconButton, Button, Menu } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import * as WebBrowser from "expo-web-browser";
import { useSelector, useDispatch } from "react-redux";
import { LinearGradient } from "expo-linear-gradient";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import {
downloadAllChaptersAction,
deleteAllChaptersAction,
} from "../../../redux/novel/novel.actions";
import { GenreChip } from "./Info/GenreChip";
import NovelCoverImage from "./Info/NovelCoverImage";
import FollowChip from "./Info/FollowChip";
import TrackerChip from "./Info/TrackerChip";
import NovelSummary from "./Info/NovelSummary";
import ReadButton from "./Info/ReadButton";
const NovelInfoHeader = ({
item,
novel,
noOfChapters,
followNovelAction,
novelName,
novelCover,
loading,
bottomSheetRef,
novel,
chapters,
theme,
firstChapter,
chaptersSettingsSheetRef,
trackerSheetRef,
}) => {
const navigation = useNavigation();
const dispatch = useDispatch();
const [more, setMore] = useState(
!loading && novel.followed !== 1 ? true : false
const [downloadMenu, showDownloadMenu] = useState(false);
const tracker = useSelector((state) => state.trackerReducer.tracker);
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
const renderGenreChip = ({ item }) => (
<Text
style={[
styles.genre,
{
color: theme.colorAccentDark,
borderColor: theme.colorAccentDark,
backgroundColor: theme.colorPrimary,
},
]}
>
{item}
</Text>
);
let isTracked = false;
if (!loading) {
isTracked = trackedNovels.find((obj) => obj.novelId === novel.novelId);
}
const getGenres = ({ item }) => <GenreChip theme={theme}>{item}</GenreChip>;
return (
<View style={{ flexGrow: 1 }}>
<ImageBackground
source={{ uri: item.novelCover }}
style={styles.background}
>
<LinearGradient
colors={["rgba(0,0,0,0.2)", theme.colorPrimaryDark]}
style={styles.linearGradient}
>
<View style={styles.detailsContainer}>
<View>
<Image
source={{ uri: item.novelCover }}
style={styles.logo}
/>
</View>
<View style={styles.nameContainer}>
<Text
numberOfLines={2}
style={[
styles.name,
{ color: theme.textColorPrimary },
]}
>
{item.novelName}
</Text>
{!loading && (
<>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
fontWeight: "bold",
}}
numberOfLines={2}
>
{novel.author.replace(",", ", ")}
</Text>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
}}
numberOfLines={1}
>
{novel.status}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 14,
}}
numberOfLines={1}
>
{novel.source}
</Text>
</>
)}
</View>
<NovelCoverImage source={{ uri: novelCover }} theme={theme}>
<View style={styles.detailsContainer}>
<View>
<Image
source={{ uri: novelCover }}
style={styles.logo}
/>
</View>
</LinearGradient>
</ImageBackground>
<View style={styles.nameContainer}>
<Text
numberOfLines={2}
style={[
styles.name,
{ color: theme.textColorPrimary },
]}
>
{novelName}
</Text>
{!loading && (
<>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
fontWeight: "bold",
}}
numberOfLines={2}
>
{novel.author.replace(",", ", ")}
</Text>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
}}
numberOfLines={1}
>
{novel.status}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 14,
}}
numberOfLines={1}
>
{novel.source}
</Text>
</>
)}
</View>
</View>
</NovelCoverImage>
{!loading && (
<>
<View style={styles.buttonsContainer}>
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={
novel.followed
? "heart"
: "heart-outline"
}
size={21}
color={theme.colorAccentDark}
/>
)}
onPress={() => followNovelAction(novel)}
style={[
{ backgroundColor: theme.colorPrimaryDark },
styles.toggleFavourite,
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimary,
}}
>
{novel.followed ? "In Library" : "Add to library"}
</Chip>
<FollowChip
theme={theme}
followed={novel.followed}
novel={novel}
/>
{tracker && (
<TrackerChip
theme={theme}
isTracked={isTracked}
trackerSheetRef={trackerSheetRef}
/>
)}
<IconButton
onPress={() =>
WebBrowser.openBrowserAsync(
novel.sourceUrl && novel.sourceUrl
)
WebBrowser.openBrowserAsync(novel.sourceUrl)
}
icon="earth"
color={theme.colorAccentDark}
@@ -153,57 +127,54 @@ const NovelInfoHeader = ({
/>
<IconButton
onPress={() =>
Share.share({
message: novel.sourceUrl,
})
Share.share({ message: novel.sourceUrl })
}
icon="share-variant"
color={theme.colorAccentDark}
size={21}
/>
</View>
{novel.novelSummary !== "" && (
<View
style={{
paddingHorizontal: 16,
marginBottom: 8,
}}
<Menu
visible={downloadMenu}
onDismiss={() => showDownloadMenu(false)}
anchor={
<IconButton
icon="download"
color={theme.colorAccentDark}
size={21}
onPress={() => showDownloadMenu(true)}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
>
<Text
style={[
{ color: theme.textColorPrimary },
styles.summaryHeader,
]}
>
About
</Text>
<Text
style={{
color: theme.textColorSecondary,
lineHeight: 20,
}}
numberOfLines={more ? 100 : 2}
onPress={() => setMore(!more)}
ellipsizeMode="clip"
>
{novel.novelSummary}
</Text>
<Text
style={[
{
color: theme.colorAccentDark,
backgroundColor: theme.colorPrimaryDark,
},
styles.moreBtn,
]}
onPress={() => setMore(!more)}
>
{more ? "Less " : "More "}
</Text>
</View>
)}
<Menu.Item
title="Download all"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{ color: theme.textColorPrimary }}
onPress={() =>
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
)
)
}
/>
<Menu.Item
title="Delete downloads"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{ color: theme.textColorPrimary }}
onPress={() =>
dispatch(deleteAllChaptersAction(chapters))
}
/>
</Menu>
</View>
<NovelSummary
summary={novel.novelSummary}
followed={novel.followed}
theme={theme}
/>
<FlatList
style={
novel.novelSummary === ""
@@ -212,44 +183,23 @@ const NovelInfoHeader = ({
}
contentContainerStyle={styles.genreContainer}
horizontal
data={novel.genre && novel.genre.split(",")}
data={novel.genre.split(",")}
keyExtractor={(item) => item}
renderItem={renderGenreChip}
renderItem={getGenres}
showsHorizontalScrollIndicator={false}
/>
<Button
color="white"
style={[
{ backgroundColor: theme.colorAccentDark },
styles.startButton,
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
// onPress={() => {
// console.log(firstChapter.chapterName);
// navigation.navigate("ChapterItem", {
// chapterUrl:
// novel.lastRead.chapterUrl ??
// firstChapter.chapterUrl,
// novelUrl: novel.novelUrl,
// extensionId: novel.extensionId,
// chapterName:
// novel.lastRead.chapterUrl ??
// firstChapter.chapterName,
// });
// }}
>
{/* {novel.lastRead
? `Continue reading ${novel.lastRead.chapterName}`
: `Start reading ${firstChapter.chapterName}`} */}
Start reading
</Button>
<ReadButton
novel={novel}
chapters={chapters}
navigation={navigation}
theme={theme}
/>
<TouchableRipple
style={styles.bottomsheet}
onPress={() =>
bottomSheetRef.current.show({ velocity: -1.5 })
chaptersSettingsSheetRef.current.show({
velocity: -1.5,
})
}
rippleColor={theme.rippleColor}
>
@@ -260,7 +210,7 @@ const NovelInfoHeader = ({
styles.chapters,
]}
>
{noOfChapters + " Chapters"}
{`${chapters.length} Chapters`}
</Text>
<IconButton
icon="filter-variant"
@@ -283,12 +233,6 @@ const styles = StyleSheet.create({
marginHorizontal: 16,
paddingTop: 8,
},
background: {
height: 285,
},
linearGradient: {
height: 286,
},
detailsContainer: {
flex: 1,
flexDirection: "row",
@@ -301,16 +245,6 @@ const styles = StyleSheet.create({
margin: 3.2,
borderRadius: 6,
},
genre: {
borderRadius: 50,
borderWidth: 1,
paddingHorizontal: 10,
marginHorizontal: 2,
fontSize: 13,
paddingVertical: 2,
justifyContent: "center",
flex: 1,
},
name: {
fontWeight: "bold",
fontSize: 18,
@@ -319,16 +253,7 @@ const styles = StyleSheet.create({
flexDirection: "row",
alignItems: "center",
},
toggleFavourite: {
height: 32,
justifyContent: "center",
alignItems: "center",
marginLeft: 16,
marginRight: 4,
paddingHorizontal: 4,
borderWidth: 0,
elevation: 0,
},
chapters: {
paddingHorizontal: 16,
paddingVertical: 4,
@@ -341,24 +266,7 @@ const styles = StyleSheet.create({
alignItems: "center",
paddingRight: 12,
},
summaryHeader: {
marginTop: 5,
paddingVertical: 5,
fontSize: 15,
fontWeight: "bold",
},
moreBtn: {
fontWeight: "bold",
position: "absolute",
bottom: 0,
right: 16,
paddingLeft: 6,
},
startButton: {
marginTop: 8,
marginBottom: 16,
marginHorizontal: 16,
},
genreContainer: {
paddingHorizontal: 15,
paddingBottom: 15,
@@ -0,0 +1,194 @@
import React from "react";
import { StyleSheet, View, Text, Image } from "react-native";
import { TouchableRipple, IconButton } from "react-native-paper";
import { useDispatch } from "react-redux";
import { untrackNovel } from "../../../../redux/tracker/tracker.actions";
export const AddMalTrackingCard = ({ theme, setTrackSearchDialog }) => (
<View
style={[
styles.addCardContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<Image
source={require("../../../../../assets/mal.png")}
style={styles.trackerIcon}
/>
<TouchableRipple
style={styles.rippleContainer}
borderless
onPress={() => setTrackSearchDialog(true)}
rippleColor={theme.colorAccentDark}
>
<Text
style={{
flex: 1,
textAlignVertical: "center",
color: theme.colorAccentDark,
}}
>
Add Tracking
</Text>
</TouchableRipple>
</View>
);
export const MalTrackItemCard = ({
trackItem,
setTrackStatusDialog,
setTrackChaptersDialog,
setTrackScoreDialog,
getStatus,
theme,
}) => {
const dispatch = useDispatch();
return (
<View
style={[
styles.cardContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<View
style={[
styles.titleContainer,
{ borderBottomColor: theme.dividerColor },
]}
>
<Image
source={require("../../../../../assets/mal.png")}
style={[styles.trackerIcon, { borderBottomLeftRadius: 0 }]}
/>
<View style={styles.listItemContainer}>
<Text
style={[
styles.listItem,
{ color: theme.textColorSecondary },
]}
>
{trackItem.title}
</Text>
<IconButton
icon="close"
color={theme.textColorSecondary}
size={21}
onPress={() => dispatch(untrackNovel(trackItem.id))}
/>
</View>
</View>
<View style={{ height: 50, flexDirection: "row" }}>
<TouchableRipple
style={[
styles.listItemLeft,
{ borderRightColor: theme.dividerColor },
]}
borderless
onPress={() => setTrackStatusDialog(true)}
rippleColor={theme.rippleColor}
>
<Text
style={[
styles.listItem,
{ color: theme.textColorSecondary },
]}
>
{getStatus(trackItem.my_list_status.status)}
</Text>
</TouchableRipple>
<TouchableRipple
style={{ flex: 1 }}
borderless
onPress={() => setTrackChaptersDialog(true)}
rippleColor={theme.rippleColor}
>
<Text
style={[
styles.listItem,
{ color: theme.textColorSecondary },
]}
>
{`${trackItem.my_list_status.num_chapters_read}/${
trackItem.num_chapters !== 0
? trackItem.num_chapters
: "-"
}`}
</Text>
</TouchableRipple>
<TouchableRipple
style={[
styles.listItemRight,
{ borderLeftColor: theme.dividerColor },
]}
borderless
onPress={() => setTrackScoreDialog(true)}
rippleColor={theme.rippleColor}
>
<Text
style={[
styles.listItem,
{ color: theme.textColorSecondary },
]}
>
{trackItem.my_list_status.score === 0
? "-"
: trackItem.my_list_status.score}
</Text>
</TouchableRipple>
</View>
</View>
);
};
const styles = StyleSheet.create({
trackerIcon: {
height: 50,
width: 50,
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
rippleContainer: {
flex: 1,
alignItems: "center",
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
addCardContainer: {
flexDirection: "row",
alignItems: "center",
margin: 8,
borderRadius: 4,
elevation: 2,
},
cardContainer: {
margin: 8,
elevation: 2,
borderRadius: 4,
},
titleContainer: {
flexDirection: "row",
alignItems: "center",
borderBottomWidth: 1,
},
listItemContainer: {
flex: 1,
flexDirection: "row",
borderTopRightRadius: 4,
},
listItem: {
flex: 1,
textAlign: "center",
textAlignVertical: "center",
},
listItemLeft: {
flex: 1,
borderRightWidth: 1,
borderBottomLeftRadius: 4,
},
listItemRight: {
flex: 1,
borderLeftWidth: 1,
borderBottomRightRadius: 4,
},
});
@@ -0,0 +1,67 @@
import React, { useState } from "react";
import { StyleSheet, Text } from "react-native";
import { Modal, TextInput } from "react-native-paper";
import {
RadioButton,
RadioButtonGroup,
} from "../../../../components/RadioButton";
const SetTrackChaptersDialog = ({
trackItem,
trackChaptersDialog,
setTrackChaptersDialog,
updateTrackChapters,
theme,
}) => {
const [trackChapters, setTrackChapters] = useState(
trackItem.my_list_status.num_chapters_read
);
return (
<Modal
visible={trackChaptersDialog}
onDismiss={() => setTrackChaptersDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={[styles.dialogTitle, { color: theme.textColorPrimary }]}
>
Chapters
</Text>
<TextInput
value={trackChapters.toString()}
onChangeText={(text) => setTrackChapters(text ? +text : "")}
onSubmitEditing={() => updateTrackChapters(trackChapters)}
mode="outlined"
keyboardType="numeric"
theme={{
colors: {
primary: theme.colorAccentDark,
placeholder: theme.textColorHintDark,
text: theme.textColorPrimary,
background: "transparent",
},
}}
underlineColor={theme.textColorHintDark}
dense
/>
</Modal>
);
};
export default SetTrackChaptersDialog;
const styles = StyleSheet.create({
containerStyle: {
margin: 30,
padding: 20,
borderRadius: 8,
},
dialogTitle: {
fontSize: 18,
marginBottom: 16,
},
});
@@ -0,0 +1,78 @@
import React from "react";
import { StyleSheet, Text } from "react-native";
import { Modal } from "react-native-paper";
import {
RadioButton,
RadioButtonGroup,
} from "../../../../components/RadioButton";
const SetTrackScoreDialog = ({
trackItem,
trackScoreDialog,
setTrackScoreDialog,
updateTrackScore,
theme,
}) => {
const getScoreLabel = (score) => {
const myanimeListScores = {
0: "No Score",
1: "Apalling",
2: "Horrible",
3: "Very Bad",
4: "Bad",
5: "Average",
6: "Fine",
7: "Good",
8: "Very Good",
9: "Great",
10: "Masterpiece",
};
return `(${score}) ${myanimeListScores[score]}`;
};
return (
<Modal
visible={trackScoreDialog}
onDismiss={() => setTrackScoreDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
theme={{ colors: { backdrop: "rgba(0,0,0,0.25)" } }}
>
<Text
style={[styles.dialogTitle, { color: theme.textColorPrimary }]}
>
Score
</Text>
<RadioButtonGroup
onValueChange={updateTrackScore}
value={trackItem.my_list_status.score}
>
{[...Array(11).keys()].map((item, index) => (
<RadioButton
key={index}
value={item}
label={getScoreLabel(item)}
theme={theme}
/>
))}
</RadioButtonGroup>
</Modal>
);
};
export default SetTrackScoreDialog;
const styles = StyleSheet.create({
containerStyle: {
margin: 30,
padding: 20,
borderRadius: 8,
},
dialogTitle: {
fontSize: 18,
marginBottom: 16,
},
});
@@ -0,0 +1,67 @@
import React from "react";
import { StyleSheet, Text } from "react-native";
import { Modal } from "react-native-paper";
import {
RadioButton,
RadioButtonGroup,
} from "../../../../components/RadioButton";
const SetTrackStatusDialog = ({
trackItem,
trackStatusDialog,
setTrackStatusDialog,
updateTrackStatus,
theme,
}) => {
const myAnimeListStatus = {
reading: "Reading",
completed: "Completed",
on_hold: "On Hold",
dropped: "Dropped",
plan_to_read: "Plan to read",
};
return (
<Modal
visible={trackStatusDialog}
onDismiss={() => setTrackStatusDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={[styles.dialogTitle, { color: theme.textColorPrimary }]}
>
Status
</Text>
<RadioButtonGroup
onValueChange={updateTrackStatus}
value={trackItem.my_list_status.status}
>
{Object.keys(myAnimeListStatus).map((key, index) => (
<RadioButton
key={index}
value={key}
label={myAnimeListStatus[key]}
theme={theme}
/>
))}
</RadioButtonGroup>
</Modal>
);
};
export default SetTrackStatusDialog;
const styles = StyleSheet.create({
containerStyle: {
margin: 30,
padding: 20,
borderRadius: 8,
},
dialogTitle: {
fontSize: 18,
marginBottom: 16,
},
});
@@ -0,0 +1,184 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, Text, View, Image, ActivityIndicator } from "react-native";
import { Button, Modal, TextInput, TouchableRipple } from "react-native-paper";
import { searchNovels } from "../../../../trackers/MyAnimeList";
import { useSelector, useDispatch } from "react-redux";
import { ScrollView } from "react-native-gesture-handler";
import { trackNovel } from "../../../../redux/tracker/tracker.actions";
const TrackSearchDialog = ({
trackSearchDialog,
setTrackSearchDialog,
novelId,
novelName,
theme,
}) => {
const [loading, setLoading] = useState(true);
const [searchResults, setSearchResults] = useState([]);
const [searchText, setSearchText] = useState(novelName);
const [selectedNovel, setSelectedNovel] = useState();
const tracker = useSelector((state) => state.trackerReducer.tracker);
const dispatch = useDispatch();
const getSearchresults = async () => {
const res = await searchNovels(searchText, tracker.access_token);
setSearchResults(res);
setLoading(false);
};
useEffect(() => {
trackSearchDialog && getSearchresults();
}, [trackSearchDialog]);
const renderSearchResultCard = (item) => (
<TouchableRipple
style={[
{ flexDirection: "row", borderRadius: 4, margin: 8 },
selectedNovel &&
selectedNovel.id === item.node.id && {
backgroundColor: theme.rippleColor,
},
]}
key={item.node.id}
onPress={() => setSelectedNovel(item.node)}
rippleColor={theme.rippleColor}
borderless
>
<>
<Image
source={{ uri: item.node.main_picture.large }}
style={{ height: 150, width: 100, borderRadius: 4 }}
/>
<Text
style={{
flex: 1,
color: theme.textColorPrimary,
marginLeft: 20,
fontSize: 16,
flexWrap: "wrap",
padding: 8,
paddingLeft: 0,
}}
>
{item.node.title}
</Text>
</>
</TouchableRipple>
);
return (
<Modal
visible={trackSearchDialog}
onDismiss={() => setTrackSearchDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
theme={{ colors: { backdrop: "rgba(0,0,0,0.25)" } }}
>
<TextInput
value={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchresults}
theme={{
colors: {
primary: theme.colorAccentDark,
text: theme.textColorPrimary,
background: "transparent",
},
}}
underlineColor={theme.textColorHintDark}
right={
<TextInput.Icon
color={theme.textColorSecondary}
icon="close"
onPress={() => setSearchText("")}
/>
}
/>
<ScrollView
style={{ flexGrow: 1, maxHeight: 500, marginVertical: 8 }}
>
{loading ? (
<ActivityIndicator
color={theme.colorAccentDark}
size={45}
style={{ margin: 16 }}
/>
) : (
searchResults &&
searchResults.data &&
searchResults.data.map((result) =>
renderSearchResultCard(result)
)
)}
</ScrollView>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
marginTop: 30,
}}
>
<Button
labelStyle={{
color: theme.colorAccentDark,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccentDark } }}
onPress={() => setSelectedNovel(null)}
>
Remove
</Button>
<View style={{ flexDirection: "row" }}>
<Button
labelStyle={{
color: theme.colorAccentDark,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccentDark } }}
onPress={() => setTrackSearchDialog(false)}
>
Cancel
</Button>
<Button
labelStyle={{
color: theme.colorAccentDark,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccentDark } }}
onPress={() => {
if (selectedNovel) {
dispatch(
trackNovel(
{ ...selectedNovel, novelId },
tracker.access_token
)
);
}
setTrackSearchDialog(false);
}}
>
OK
</Button>
</View>
</View>
</Modal>
);
};
export default TrackSearchDialog;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
});
@@ -0,0 +1,180 @@
import React, { useState, useEffect } from "react";
import { StyleSheet, View, ToastAndroid } from "react-native";
import { Portal } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import { useSelector, useDispatch } from "react-redux";
import { updateTracker } from "../../../../redux/tracker/tracker.actions";
import TrackSearchDialog from "./TrackSearchDialog";
import SetTrackStatusDialog from "./SetTrackStatusDialog";
import SetTrackScoreDialog from "./SetTrackScoreDialog";
import SetTrackChaptersDialog from "./SetTrackChaptersDialog";
import { AddMalTrackingCard, MalTrackItemCard } from "./MyAnimeListCards";
const TrackSheet = ({ bottomSheetRef, novelId, novelName, theme }) => {
const tracker = useSelector((state) => state.trackerReducer.tracker);
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
const dispatch = useDispatch();
const [trackItem, setTrackItem] = useState();
useEffect(() => {
setTrackItem(trackedNovels.find((obj) => obj.novelId === novelId));
}, [trackedNovels, novelId]);
const [trackSearchDialog, setTrackSearchDialog] = useState(false);
const [trackStatusDialog, setTrackStatusDialog] = useState(false);
const [trackChaptersDialog, setTrackChaptersDialog] = useState(false);
const [trackScoreDialog, setTrackScoreDialog] = useState(false);
const getStatus = (status) => {
const myAnimeListStatus = {
reading: "Reading",
completed: "Completed",
on_hold: "On Hold",
dropped: "Dropped",
plan_to_read: "Plan to read",
};
return myAnimeListStatus[status];
};
const updateTrackChapters = (newChapters) => {
if (newChapters !== "") {
setTrackItem({
...trackItem,
my_list_status: {
...trackItem.my_list_status,
num_chapters_read: newChapters,
},
});
dispatch(
updateTracker(trackItem.id, tracker.access_token, {
...trackItem.my_list_status,
num_chapters_read: newChapters,
})
);
setTrackChaptersDialog(false);
} else {
ToastAndroid.show("Enter a valid number", ToastAndroid.SHORT);
}
};
const updateTrackStatus = (newStatus) => {
setTrackItem({
...trackItem,
my_list_status: {
...trackItem.my_list_status,
status: newStatus,
},
});
dispatch(
updateTracker(trackItem.id, tracker.access_token, {
...trackItem.my_list_status,
status: newStatus,
})
);
setTrackStatusDialog(false);
};
const updateTrackScore = (newScore) => {
setTrackItem({
...trackItem,
my_list_status: {
...trackItem.my_list_status,
score: newScore,
},
});
dispatch(
updateTracker(trackItem.id, tracker.access_token, {
...trackItem.my_list_status,
score: newScore,
})
);
setTrackScoreDialog(false);
};
return (
<>
<Bottomsheet
ref={bottomSheetRef}
draggableRange={{ top: 120, bottom: 0 }}
snappingPoints={[0, 120]}
backdropOpacity={0.25}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
{!trackItem ? (
<AddMalTrackingCard
theme={theme}
setTrackSearchDialog={setTrackSearchDialog}
/>
) : (
<MalTrackItemCard
trackItem={trackItem}
setTrackStatusDialog={setTrackStatusDialog}
setTrackChaptersDialog={setTrackChaptersDialog}
setTrackScoreDialog={setTrackScoreDialog}
theme={theme}
getStatus={getStatus}
/>
)}
</View>
</Bottomsheet>
<Portal>
{trackItem ? (
<>
<SetTrackStatusDialog
trackItem={trackItem}
trackStatusDialog={trackStatusDialog}
setTrackStatusDialog={setTrackStatusDialog}
updateTrackStatus={updateTrackStatus}
theme={theme}
/>
<SetTrackChaptersDialog
trackItem={trackItem}
trackChaptersDialog={trackChaptersDialog}
setTrackChaptersDialog={setTrackChaptersDialog}
updateTrackChapters={updateTrackChapters}
theme={theme}
/>
<SetTrackScoreDialog
trackItem={trackItem}
trackScoreDialog={trackScoreDialog}
setTrackScoreDialog={setTrackScoreDialog}
updateTrackScore={updateTrackScore}
theme={theme}
/>
</>
) : (
<TrackSearchDialog
trackSearchDialog={trackSearchDialog}
setTrackSearchDialog={setTrackSearchDialog}
novelId={novelId}
novelName={novelName}
theme={theme}
/>
)}
</Portal>
</>
);
};
export default TrackSheet;
const styles = StyleSheet.create({
contentContainer: { flex: 1 },
});
+67 -3
View File
@@ -1,3 +1,67 @@
/**
* Updates Screen
*/
import React, { useState, useEffect, useCallback } from "react";
import { useFocusEffect } from "@react-navigation/native";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { connect } from "react-redux";
import EmptyView from "../../components/EmptyView";
import { getUpdatesAction } from "../../redux/updates/updates.actions";
import { Appbar } from "./components/Appbar";
import UpdateCard from "./components/UpdateCard";
const Updates = ({ theme, updates, loading, getUpdatesAction }) => {
useFocusEffect(
useCallback(() => {
getUpdatesAction();
}, [getUpdatesAction])
);
const renderUpdateCard = ({ item }) => <UpdateCard item={item} />;
return (
<>
<Appbar />
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
contentContainerStyle={{ flexGrow: 1, paddingVertical: 8 }}
data={updates}
keyExtractor={(item) => item.updateId.toString()}
renderItem={renderUpdateCard}
ListFooterComponent={
loading && (
<ActivityIndicator
size="small"
color={theme.colorAccentDark}
/>
)
}
ListEmptyComponent={
!loading && (
<EmptyView
icon="(˘・_・˘)"
description="No recent updates"
/>
)
}
/>
</View>
</>
);
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
updates: state.updatesReducer.updates,
loading: state.updatesReducer.loading,
});
export default connect(mapStateToProps, { getUpdatesAction })(Updates);
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
+19
View File
@@ -0,0 +1,19 @@
import React from "react";
import { Appbar as MaterialAppbar } from "react-native-paper";
import { useSelector } from "react-redux";
import { updateAllNovels } from "../../../services/updates";
export const Appbar = () => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<MaterialAppbar.Header style={{ backgroundColor: theme.colorPrimary }}>
<MaterialAppbar.Content
title="Updates"
titleStyle={{ color: theme.textColorPrimary }}
/>
<MaterialAppbar.Action icon="reload" onPress={updateAllNovels} />
</MaterialAppbar.Header>
);
};
@@ -0,0 +1,88 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple, Button } from "react-native-paper";
import moment from "moment";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
const UpdateCard = ({ item }) => {
const navigation = useNavigation();
const theme = useSelector((state) => state.themeReducer.theme);
return (
<TouchableRipple
style={styles.updateCard}
onPress={() =>
// console.log(item);
navigation.navigate("ChapterItem", {
chapterId: item.chapterId,
chapterUrl: item.chapterUrl,
extensionId: item.sourceId,
novelUrl: item.novelUrl,
chapterName: item.chapterName,
novelId: item.novelId,
})
}
rippleColor={theme.rippleColor}
borderless
>
<>
<Image
source={{ uri: item.novelCover }}
style={styles.updateIcon}
/>
<View style={styles.extensionDetails}>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={{ flexDirection: "row" }}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 12,
}}
>
{`${item.chapterName} · ${moment(
item.updateTime
).calendar()}`}
</Text>
</View>
</View>
</View>
</>
</TouchableRipple>
);
};
export default UpdateCard;
const styles = StyleSheet.create({
updateCard: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 12,
// borderRadius: 4,
},
updateIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
extensionDetails: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
});
-138
View File
@@ -1,138 +0,0 @@
import React, { useEffect, useState, useRef } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Provider, Portal } from "react-native-paper";
import NovelCover from "../../../components/common/NovelCover";
import { SearchAppbar } from "../../../components/common/Appbar";
import { BottomSheet } from "./filters/BottomSheet";
import { useSelector } from "react-redux";
const BoxNovel = ({ navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const itemsPerRow = useSelector(
(state) => state.settingsReducer.itemsPerRow
);
const library = useSelector((state) => state.libraryReducer.novels);
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState([]);
const [sort, setSort] = useState("rating");
const [searchText, setSearchText] = useState("");
let bottomSheetRef = useRef(null);
const getNovels = () => {
fetch(
`https://lnreader-extensions.herokuapp.com/api/1/novels/1/?o=${sort}`
)
.then((response) => response.json())
.then((json) => {
setNovels(json);
setLoading(false);
});
};
const getSearchResults = (searchText) => {
setLoading(true);
fetch(
`https://lnreader-extensions.herokuapp.com/api/1/search/?s=${searchText}&?o=${sort}`
)
.then((response) => response.json())
.then((json) => {
setNovels(json);
setLoading(false);
});
};
const checkIFInLibrary = (id) => {
return library.some((obj) => obj.novelUrl === id);
};
useEffect(() => {
getNovels();
}, [sort]);
return (
<Provider>
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<SearchAppbar
screen="Extension"
placeholder="Search BoxNovel"
searchText={searchText}
setSearchText={setSearchText}
getSearchResults={getSearchResults}
getNovels={getNovels}
setLoading={setLoading}
onFilter={() =>
bottomSheetRef.current.show({ velocity: -1.5 })
}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
size={60}
color={theme.colorAccentDark}
/>
</View>
) : (
<FlatList
contentContainerStyle={styles.list}
numColumns={itemsPerRow}
key={itemsPerRow}
data={novels}
showsVerticalScrollIndicator={false}
keyExtractor={(item) => item.novelUrl}
renderItem={({ item }) => (
<NovelCover
item={item}
onPress={() => {
navigation.navigate("NovelItem", {
novelName: item.novelName,
novelCover: item.novelCover,
novelUrl: item.novelUrl,
sourceId: item.extensionId,
});
}}
libraryStatus={
checkIFInLibrary(item.novelUrl)
? true
: false
}
/>
)}
/>
)}
</View>
<Portal>
<BottomSheet
bottomSheetRef={bottomSheetRef}
setSort={setSort}
sort={sort}
setLoading={setLoading}
/>
</Portal>
</Provider>
);
};
export default BoxNovel;
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 4,
},
contentContainer: {
flex: 1,
},
});
@@ -1,94 +0,0 @@
import React from "react";
import { StyleSheet, View } from "react-native";
import { List } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import { useSelector } from "react-redux";
export const BottomSheet = ({ bottomSheetRef, setSort, sort, setLoading }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const sorting = [
{ label: "Latest", sortFlag: "latest" },
{ label: "A-Z", sortFlag: "aplhabet" },
{ label: "Rating", sortFlag: "rating" },
{ label: "Trending", sortFlag: "trending" },
{ label: "Most Views", sortFlag: "views" },
{ label: "New", sortFlag: "new-manga" },
];
return (
<Bottomsheet
ref={bottomSheetRef}
draggableRange={{ top: 370, bottom: 0 }}
snappingPoints={[0, 370]}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<View
style={[
styles.bottomSheetHandle,
{ backgroundColor: theme.textColorPrimary },
]}
/>
<List.Section
title="Sort"
titleStyle={{
color: theme.colorAccentDark,
fontWeight: "bold",
}}
style={{ color: theme.colorAccentDark }}
expanded
>
{sorting.map((item, index) => (
<List.Item
key={index}
right={() =>
sort === item.sortFlag && (
<List.Icon
icon="arrow-up"
color={theme.colorAccentDark}
style={styles.sortArrow}
/>
)
}
title={item.label}
titleStyle={{
fontSize: 15,
color: theme.textColorPrimary,
}}
onPress={() => {
setLoading(true);
setSort(item.sortFlag);
}}
/>
))}
</List.Section>
</View>
</Bottomsheet>
);
};
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
bottomSheetHandle: {
opacity: 0.5,
height: 4,
width: 32,
borderRadius: 50,
top: 8,
alignSelf: "center",
},
sortArrow: {
marginVertical: 0,
paddingVertical: 0,
},
});
-76
View File
@@ -1,76 +0,0 @@
/**
* Fetch novel information from source
*/
export const fetchNovelFromSource = async (extensionId, novelUrl) => {
const url = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}`;
let data = await fetch(url);
let res = await data.json();
const novel = {
novelUrl: res.novelUrl,
novelName: res.novelName,
novelCover: res.novelCover,
novelSummary: res.novelSummary,
"Author(s)": res["Author(s)"],
"Genre(s)": res["Genre(s)"],
Status: res.Status,
sourceUrl: res.sourceUrl,
source: res.sourceName,
unread: 1,
libraryStatus: 0,
lastRead: res.novelChapters[0].chapterUrl,
lastReadName: res.novelChapters[0].chapterName,
extensionId: res.extensionId,
novelChapters: res.novelChapters,
};
return novel;
};
/**
* Fetch novel chapters froms source
*/
export const fetchChaptersFromSource = async (extensionId, novelUrl) => {
const url = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}`;
const data = await fetch(url);
let res = await data.json();
const chapters = res.novelChapters;
return chapters;
};
/**
* Fetch chapter from source
*/
export const fetchChapterFromSource = async (
extensionId,
novelUrl,
chapterUrl
) => {
const url = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
let res = await fetch(url);
let chapter = await res.json();
return chapter;
};
/**
* Fetch list of extensions
*/
export const fetchExtensionList = async () => {
const url = `https://lnreader-extensions.herokuapp.com/api/`;
let res = await fetch(url);
let extensions = await res.json();
return extensions;
};
+94 -4
View File
@@ -1,6 +1,8 @@
import * as SQLite from "expo-sqlite";
import { fetchNovel } from "../source/Source";
import { getLibrary } from "../database/queries/LibraryQueries";
import { fetchChapters, fetchNovel } from "../source/Source";
const db = SQLite.openDatabase("lnreader.db");
import { ToastAndroid } from "react-native";
const updateNovelDetails = async (novel, novelId) => {
db.transaction((tx) => {
@@ -37,11 +39,99 @@ export const updateNovel = async (extensionId, novelUrl, novelId) => {
chapter.releaseDate,
novelId,
],
(txObj, res) => {},
(txObj, { insertId }) => {
if (insertId !== -1) {
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
(txObj, res) => {
console.log(
"Inserted Chapter Id -> " +
insertId +
" Novel Id " +
novelId
);
},
(txObj, error) => console.log("Error ", error)
);
}
},
(txObj, error) => console.log("Error ", error)
)
);
});
return novel;
};
export const updateNovelChapters = async (extensionId, novelUrl, novelId) => {
let chapters = await fetchChapters(extensionId, novelUrl);
db.transaction((tx) => {
chapters.map((chapter) =>
tx.executeSql(
"INSERT OR IGNORE INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)",
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
novelId,
],
(txObj, { insertId }) => {
if (insertId !== -1) {
console.log(
"Inserted Chapter " +
chapter.chapterUrl +
" Chapter Name " +
chapter.chapterName +
" Novel Id " +
novelId
);
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
(txObj, res) => {
// console.log(
// "Inserted Chapter Id -> " +
// insertId +
// " Novel Id " +
// novelId
// );
},
(txObj, error) => console.log("Error ", error)
);
}
},
(txObj, error) => console.log("Error ", error)
)
);
});
};
export const updateAllNovels = async () => {
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
const libraryNovels = await getLibrary();
libraryNovels.map((novel, index) =>
setTimeout(async () => {
updateNovelChapters(novel.sourceId, novel.novelUrl, novel.novelId);
console.log(novel.novelName + " Updated");
}, 3000 * index)
);
};
export const parseChapterNumber = (chapterName) => {
chapterName = chapterName.toLowerCase();
chapterName = chapterName.replace(/volume (\d+)/, "");
const basic = chapterName.match(/ch (\d+)/);
const occurrence = chapterName.match(/\d+/);
if (basic) {
return basic[0];
} else if (occurrence) {
return occurrence[0];
} else {
return 0;
}
};
+16 -3
View File
@@ -1,5 +1,5 @@
export const fetchSources = async () => {
const url = `https://lnreader-extensions.herokuapp.com/api/`;
const url = `https://lnreader-extensions.vercel.app/api/`;
let res = await fetch(url);
let sources = await res.json();
@@ -8,7 +8,7 @@ export const fetchSources = async () => {
};
export const fetchNovel = async (sourceId, novelUrl) => {
const url = `https://lnreader-extensions.herokuapp.com/api/${sourceId}/novel/${novelUrl}`;
const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}`;
let data = await fetch(url);
let res = await data.json();
@@ -33,10 +33,23 @@ export const fetchNovel = async (sourceId, novelUrl) => {
};
export const fetchChapter = async (sourceId, novelUrl, chapterUrl) => {
const url = `https://lnreader-extensions.herokuapp.com/api/${sourceId}/novel/${novelUrl}${chapterUrl}`;
const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}${chapterUrl}`;
// console.log(url);
let res = await fetch(url);
let chapter = await res.json();
return chapter;
};
export const fetchChapters = async (sourceId, novelUrl) => {
const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}`;
let data = await fetch(url);
let res = await data.json();
const chapters = res.novelChapters;
return chapters;
};
+24
View File
@@ -15,6 +15,8 @@ export const theme = {
* Light Theme
*/
export const lightTheme = {
id: 1,
name: "Light",
colorPrimary: "#FFFFFF",
colorPrimaryDark: "#FAFAFA",
colorAccentDark: "#2979FF",
@@ -23,12 +25,18 @@ export const lightTheme = {
textColorHintDark: "rgba(0,0,0,0.38)",
rippleColor: "rgba(0,0,0,0.12)",
searchBarColor: "#FFFFFF",
menuColor: "#FFFFFF",
dividerColor: "rgba(0,0,0,0.1)",
colorDisabled: "rgba(220,220,220,255)",
statusBar: "dark",
};
/**
* Dark Theme
*/
export const darkTheme = {
id: 2,
name: "Dark",
colorPrimary: "#242529",
colorPrimaryDark: "#202125",
colorAccentDark: "#3399FF",
@@ -37,12 +45,18 @@ export const darkTheme = {
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#303135",
menuColor: "#242529",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
statusBar: "light",
};
/**
* Amoled Dark Theme
*/
export const amoledDarkTheme = {
id: 0,
name: "AMOLED Dark",
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccentDark: "#3399FF",
@@ -51,12 +65,18 @@ export const amoledDarkTheme = {
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#1F1F1F",
menuColor: "#242529",
dividerColor: "rgba(255,255,255,0.15)",
colorDisabled: "rgba(33,33,33,255)",
statusBar: "light",
};
/**
* Midnight Dusk Theme
*/
export const midnightDuskTheme = {
id: 3,
name: "Midnight Dusk",
colorPrimary: "#201F27",
colorPrimaryDark: "#16151D",
colorAccentDark: "#F02475",
@@ -65,4 +85,8 @@ export const midnightDuskTheme = {
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#201F27",
menuColor: "#201F27",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
statusBar: "light",
};
+171
View File
@@ -0,0 +1,171 @@
import * as Linking from "expo-linking";
import qs from "qs";
const clientId = "d2f499825d64b0213bb77e78193ccbb1";
const baseOAuthUrl = "https://myanimelist.net/v1/oauth2/authorize";
const baseApiUrl = "https://api.myanimelist.net/v2";
const pkceChallenger = () => {
const MAX_LENGTH = 88;
let code = "";
const codes =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const randomPicker = () => Math.floor(Math.random() * codes.length);
for (let index = 0; index < MAX_LENGTH; index++) {
code += codes.charAt(randomPicker());
}
return code;
};
const codeChallenge = pkceChallenger();
const redirectUri = Linking.makeUrl();
export const authUrl = `${baseOAuthUrl}?response_type=code&client_id=${clientId}&code_challenge_method=plain&code_challenge=${codeChallenge}`;
const tokenUrl = `https://myanimelist.net/v1/oauth2/token`;
export const myAnimeListConfig = {
clientId,
redirectUri,
authUrl,
codeChallenge,
tokenUrl,
};
export const getAccessToken = async (code, codeChallenge) => {
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
};
const body = {
client_id: myAnimeListConfig.clientId,
grant_type: "authorization_code",
code,
code_verifier: codeChallenge,
};
const response = await fetch(myAnimeListConfig.tokenUrl, {
headers,
method: "POST",
body: qs.stringify(body),
});
let tokenResponse = await response.json();
tokenResponse = {
...tokenResponse,
expires_in: new Date(Date.now() + tokenResponse.expires_in),
};
console.log(tokenResponse);
return tokenResponse;
};
export const searchNovels = async (novelName, bearerToken) => {
const searchrl = `${baseApiUrl}/manga?q=${novelName}`;
const headers = {
Authorization: "Bearer " + bearerToken,
"Content-Type": "application/x-www-form-urlencoded",
};
const res = await fetch(searchrl, { method: "GET", headers });
const searchResults = await res.json();
return searchResults;
};
export const findListItem = async (malId, bearerToken) => {
const url = `${baseApiUrl}/manga/${malId}?fields=id,num_chapters,my_list_status{start_date,finish_date}`;
const headers = {
Authorization: "Bearer " + bearerToken,
"Content-Type": "application/x-www-form-urlencoded",
};
const res = await fetch(url, { method: "GET", headers });
const listItem = await res.json();
if (!listItem.my_list_status) {
listItem.my_list_status = {
status: "reading",
score: 0,
num_chapters_read: 0,
};
}
return listItem;
};
export const updateItem = async (malId, bearerToken, body) => {
const url = `${baseApiUrl}/manga/${malId}/my_list_status`;
const headers = {
Authorization: "Bearer " + bearerToken,
"Content-Type": "application/x-www-form-urlencoded",
};
const res = await fetch(url, {
method: "PUT",
headers,
body: qs.stringify(body),
});
const listItem = await res.json();
console.log(listItem);
return listItem;
};
export const updateMalChaptersRead = async (malId, bearerToken, body) => {
const url = `${baseApiUrl}/manga/${malId}/my_list_status`;
const headers = {
Authorization: "Bearer " + bearerToken,
"Content-Type": "application/x-www-form-urlencoded",
};
const res = await fetch(url, {
method: "PUT",
headers,
body: qs.stringify(body),
});
const listItem = await res.json();
console.log(listItem.num_chapters_read);
return listItem.num_chapters_read;
};
const revalidateMalToken = async (refreshToken) => {
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
};
const body = {
grant_type: "refresh_token",
refresh_token: refreshToken,
};
const res = await fetch(url, {
headers,
method: "POST",
body: qs.stringify(body),
});
let tokenResponse = res.json();
return tokenResponse;
};
export const malTokenWatcher = async (tracker) => {
if (tracker) {
const date = new Date(Date.now());
if (tracker.expires_in < date) {
return await revalidateMalToken(tracker.refresh_token);
}
}
};
+11
View File
@@ -0,0 +1,11 @@
const fonts = [
{ fontFamily: "", name: "Original" },
{ fontFamily: "arbutus-slab", name: "Arbutus Slab" },
{ fontFamily: "domine", name: "Domine" },
{ fontFamily: "lato", name: "Lato" },
{ fontFamily: "noto-sans", name: "Noto Sans" },
{ fontFamily: "open-sans", name: "Open Sans" },
{ fontFamily: "pt-serif", name: "PT Serif" },
];
export { fonts };