Compare commits

...

6 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
45 changed files with 2078 additions and 460 deletions
+2 -1
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": {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+37 -3
View File
@@ -6037,6 +6037,11 @@
"is-wsl": "^1.1.0"
}
},
"opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q=="
},
"options": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
@@ -6354,9 +6359,24 @@
}
},
"qs": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz",
"integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==",
"requires": {
"side-channel": "^1.0.4"
},
"dependencies": {
"side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"requires": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
}
}
}
},
"query-string": {
"version": "6.13.7",
@@ -6801,6 +6821,15 @@
"prop-types": "^15.7.2"
}
},
"react-native-inappbrowser-reborn": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/react-native-inappbrowser-reborn/-/react-native-inappbrowser-reborn-3.5.1.tgz",
"integrity": "sha512-pEM9DSvW3LYoI7x1sfr4GtTA1/BSzSoQvHeNRtbJwOpujgRBqnEQrapBwC/24l2pIRhKDWmXGfqc12478IXY3Q==",
"requires": {
"invariant": "^2.2.4",
"opencollective-postinstall": "^2.0.2"
}
},
"react-native-iphone-x-helper": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
@@ -6827,6 +6856,11 @@
"react-native-iphone-x-helper": "^1.3.1"
}
},
"react-native-picker-scrollview": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/react-native-picker-scrollview/-/react-native-picker-scrollview-1.0.1.tgz",
"integrity": "sha512-6N4IuAwLVDLoO3IUCW0QzWJrafXVi7WylofaPg4PNPfPRFOYAsGKMN5yEPw463tWtmbBxP6vZEIOfGl93d9Aeg=="
},
"react-native-reanimated": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-1.13.2.tgz",
+3
View File
@@ -24,13 +24,16 @@
"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",
+10 -1
View File
@@ -14,7 +14,7 @@ const ListSubHeader = ({ children, theme }) => (
</List.Subheader>
);
const ListItem = ({ title, description, icon, onPress, theme }) => (
const ListItem = ({ title, description, icon, onPress, theme, right }) => (
<List.Item
title={title}
titleStyle={{ color: theme.textColorPrimary }}
@@ -30,6 +30,15 @@ const ListItem = ({ title, description, icon, onPress, theme }) => (
/>
)
}
right={() =>
right && (
<List.Icon
color="#47a84a"
icon={right}
style={{ marginVertical: 0 }}
/>
)
}
onPress={onPress}
rippleColor={theme.rippleColor}
/>
+77 -15
View File
@@ -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,
},
});
+4 -2
View File
@@ -35,7 +35,9 @@ export const insertChapters = async (novelId, chapters) => {
};
const getChaptersQuery = (sort, filter) =>
`SELECT * FROM chapters WHERE novelId = ? ${filter ?? ""} ${sort ?? ""}`;
`SELECT * FROM chapters WHERE novelId = ? ${filter ?? ""} ${
sort ?? "ORDER BY chapterId ASC"
}`;
export const getChapters = (novelId, sort, filter) => {
return new Promise((resolve, reject) =>
@@ -111,7 +113,7 @@ export const downloadChapter = async (
chapterUrl,
chapterId
) => {
const downloadUrl = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
// console.log(downloadUrl);
+13 -2
View File
@@ -1,7 +1,7 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const getHistoryQuery = `
const getUpdatesQuery = `
SELECT chapters.chapterId, chapters.chapterUrl, chapters.chapterName, novels.novelUrl, novels.novelId, novels.novelCover, novels.novelName, novels.sourceId, updates.updateTime, updates.updateId
FROM updates
JOIN chapters
@@ -14,7 +14,7 @@ export const getUpdates = async () => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
getHistoryQuery,
getUpdatesQuery,
null,
(txObj, { rows: { _array } }) => {
resolve(_array);
@@ -24,3 +24,14 @@ export const getUpdates = async () => {
});
});
};
export const deleteNovelUpdates = (novelId) => {
db.transaction((tx) => {
tx.executeSql(
"DELETE FROM updates WHERE novelId = ?",
[novelId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
+2
View File
@@ -17,6 +17,7 @@ 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";
@@ -37,6 +38,7 @@ const SettingsStack = () => {
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="About" component={About} />
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen name="Trackers" component={Trackers} />
</Stack.Navigator>
);
};
+2 -5
View File
@@ -27,12 +27,9 @@ export const getChapterAction = (
) => async (dispatch) => {
dispatch({ type: CHAPTER_LOADING });
const isDownloaded = await isChapterDownloaded(novelId);
let chapter;
let chapter = await getChapter(novelId);
if (isDownloaded) {
chapter = await getChapter(novelId);
} else {
if (!chapter) {
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
}
console.log(isDownloaded);
+20 -11
View File
@@ -1,4 +1,8 @@
import { FOLLOW_NOVEL, UNFOLLOW_NOVEL } from "../novel/novel.types";
import {
FOLLOW_NOVEL,
UNFOLLOW_NOVEL,
UPDATE_IN_LIBRARY,
} from "../novel/novel.types";
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
@@ -17,18 +21,23 @@ const libraryReducer = (state = initialState, action) => {
case GET_LIBRARY_NOVELS:
return { ...state, novels: payload, loading: false };
case GET_LIBRARY_SEARCH_RESULTS:
return { ...state, searchResults: payload, loading: false };
case FOLLOW_NOVEL:
return { ...state, novels: payload, loading: false };
// case FOLLOW_NOVEL:
// return {
// ...state,
// novels: [...state.novels, { ...payload, followed: 1 }],
// };
// case UNFOLLOW_NOVEL:
// return {
// ...state,
// novels: state.novels.filter(
// (novel) => novel.novelId !== payload
// ),
// };
case UPDATE_IN_LIBRARY:
return {
...state,
novels: [...state.novels, { ...payload, followed: 1 }],
};
case UNFOLLOW_NOVEL:
return {
...state,
novels: state.novels.filter(
(novel) => novel.novelId !== payload
),
novels: [...state.novels, { novelUrl: payload.novelUrl }],
};
default:
return state;
+6 -1
View File
@@ -32,6 +32,7 @@ import {
downloadChapter,
deleteChapter,
} from "../../database/queries/ChapterQueries";
import { deleteNovelUpdates } from "../../database/queries/UpdateQueries";
export const setNovel = (novel) => async (dispatch) => {
dispatch({ type: SET_NOVEL, payload: novel });
@@ -104,9 +105,13 @@ export const sortAndFilterChapters = (novelUrl, filter, sort) => async (
export const followNovelAction = (novel) => async (dispatch) => {
await followNovel(novel.followed, novel.novelId);
if (!novel.followNovel) {
deleteNovelUpdates(novel.novelId);
}
dispatch({
type: UPDATE_IN_LIBRARY,
payload: !novel.followed,
payload: { novelUrl: novel.novelUrl, followed: !novel.followed },
});
ToastAndroid.show(
+1 -1
View File
@@ -53,7 +53,7 @@ const novelReducer = (state = initialState, action) => {
case UPDATE_IN_LIBRARY:
return {
...state,
novel: { ...state.novel, followed: payload },
novel: { ...state.novel, followed: payload.followed },
};
case UPDATE_NOVEL:
return {
+2
View File
@@ -10,6 +10,7 @@ import extensionReducer from "./source/source.reducers";
import novelReducer from "./novel/novel.reducer";
import historyReducer from "./history/history.reducer";
import updatesReducer from "./updates/updates.reducer";
import trackerReducer from "./tracker/tracker.reducer";
const persistConfig = {
key: "root",
@@ -27,6 +28,7 @@ const persistedReducer = persistReducer(
novelReducer,
historyReducer,
updatesReducer,
trackerReducer,
})
);
+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";
+34 -1
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,6 +71,8 @@ const ChapterItem = ({
return textColor;
};
const chapterNumber = !loading && parseChapterNumber(chapter.chapterName);
const readerLineHeight = (val) => {
const lineHeight = {
12: 20,
@@ -79,6 +96,8 @@ const ChapterItem = ({
);
};
// !loading && parseChapterNumber(chapter.chapterName);
return (
<Provider>
<CollapsibleHeaderScrollView
@@ -154,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
)
);
}
}
}}
>
+4 -4
View File
@@ -25,17 +25,17 @@ const Extension = ({ navigation, route }) => {
const getSourceUrl = () => {
if (sourceId === 1) {
return `https://lnreader-extensions.herokuapp.com/api/1/novels/1/?o=rating`;
return `https://lnreader-extensions.vercel.app/api/1/novels/1/?o=rating`;
} else {
return `https://lnreader-extensions.herokuapp.com/api/${sourceId}/novels/`;
return `https://lnreader-extensions.vercel.app/api/${sourceId}/novels/`;
}
};
const getSearchUrl = () => {
if (sourceId === 1) {
return `https://lnreader-extensions.herokuapp.com/api/1/search/?s=${searchText}&?o=rating`;
return `https://lnreader-extensions.vercel.app/api/1/search/?s=${searchText}&?o=rating`;
} else {
return `https://lnreader-extensions.herokuapp.com/api/${sourceId}/search/?s=${searchText}`;
return `https://lnreader-extensions.vercel.app/api/${sourceId}/search/?s=${searchText}`;
}
};
+7 -6
View File
@@ -30,16 +30,18 @@ const LibraryScreen = ({
getLibraryAction,
searchLibraryAction,
setNovel,
searchResults,
itemsPerRow,
}) => {
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
useEffect(() => {
getLibraryAction();
}, [getLibraryAction, novels]);
useFocusEffect(
useCallback(() => {
setSearchText("");
getLibraryAction();
}, [getLibraryAction])
);
/**
* TODO: fix refreshing
@@ -85,7 +87,7 @@ const LibraryScreen = ({
}}
numColumns={itemsPerRow}
key={itemsPerRow}
data={!searchText ? novels : searchResults}
data={novels}
keyExtractor={(item) => item.novelUrl}
renderItem={({ item }) => (
<NovelCover
@@ -129,7 +131,6 @@ const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
itemsPerRow: state.settingsReducer.itemsPerRow,
novels: state.libraryReducer.novels,
searchResults: state.libraryReducer.searchResults,
loading: state.libraryReducer.loading,
});
+2 -2
View File
@@ -17,12 +17,12 @@ const AboutScreen = ({ navigation }) => {
<ListSection>
<ListItem
title="Version"
description="Stable 1.0.13"
description="Stable 1.0.15"
theme={theme}
/>
<ListItem
title="Build Time"
description="24-04-21 21:00 PM"
description="30-04-21 11:00 AM"
theme={theme}
/>
<ListItem
+10
View File
@@ -43,6 +43,16 @@ const MoreScreen = ({ navigation }) => {
}
theme={theme}
/>
<ListItem
title="Tracking"
icon="sync"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "Trackers",
})
}
theme={theme}
/>
<ListItem
title="About"
icon="information-outline"
+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;
+23 -1
View File
@@ -2,6 +2,7 @@ import React from "react";
import { Text, StyleSheet } from "react-native";
import { Portal, Modal } from "react-native-paper";
import Slider from "@react-native-community/slider";
import { RadioButton, RadioButtonGroup } from "../../../components/RadioButton";
const GridSizeModal = ({
itemsPerRow,
@@ -10,6 +11,14 @@ const GridSizeModal = ({
setItemsPerRow,
theme,
}) => {
const gridSizes = {
1: "XS",
2: "S",
3: "M",
4: "L",
5: "XL",
};
return (
<Portal>
<Modal
@@ -47,6 +56,19 @@ const GridSizeModal = ({
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>
);
@@ -65,7 +87,7 @@ const styles = StyleSheet.create({
marginBottom: 10,
},
modalDescription: {
marginBottom: 4,
marginBottom: 16,
},
slider: {
width: "100%",
+37 -49
View File
@@ -10,18 +10,16 @@ import { Provider, Portal } from "react-native-paper";
import ChapterCard from "./components/ChapterCard";
import NovelInfoHeader from "./components/NovelHeader";
import { BottomSheet } from "./components/BottomSheet";
import ChaptersSettingsSheet from "./components/ChaptersSettingsSheet";
import { connect } from "react-redux";
import {
getNovelAction,
followNovelAction,
sortAndFilterChapters,
updateNovelAction,
downloadAllChaptersAction,
deleteAllChaptersAction,
} from "../../redux/novel/novel.actions";
import TrackSheet from "./components/Tracker/TrackSheet";
const Novel = ({
route,
@@ -31,11 +29,8 @@ const Novel = ({
loading,
getNovelAction,
fetching,
followNovelAction,
sortAndFilterChapters,
updateNovelAction,
downloadAllChaptersAction,
deleteAllChaptersAction,
}) => {
const {
sourceId,
@@ -46,16 +41,20 @@ const Novel = ({
novelId,
} = route.params;
let _panel = useRef(null); // Bottomsheet ref
let chaptersSettingsSheetRef = useRef(null);
let trackerSheetRef = useRef(null);
const [sort, setSort] = useState("ORDER BY chapterId ASC");
const [filter, setFilter] = useState("");
useEffect(() => {
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter);
}, [getNovelAction, followNovelAction, sort, filter]);
}, [getNovelAction, sort, filter]);
let lastRead = !loading && chapters.find((obj) => obj.read === 0);
const onRefresh = async () => {
await updateNovelAction(sourceId, novelUrl, novelId);
ToastAndroid.show(`Updated ${novelName}`, ToastAndroid.SHORT);
};
const renderChapterCard = ({ item }) => (
<ChapterCard
@@ -65,11 +64,6 @@ const Novel = ({
/>
);
const onRefresh = () => {
updateNovelAction(sourceId, novelUrl, novelId);
ToastAndroid.show(`Updated ${novelName}`, ToastAndroid.SHORT);
};
return (
<Provider>
<View
@@ -88,24 +82,14 @@ const Novel = ({
renderItem={renderChapterCard}
ListHeaderComponent={
<NovelInfoHeader
theme={theme}
item={{ novelName, novelCover }}
novel={novel}
noOfChapters={chapters?.length}
followNovelAction={followNovelAction}
loading={loading}
lastRead={lastRead}
bottomSheetRef={_panel}
downloadAllChapters={() =>
downloadAllChaptersAction(
sourceId,
novelUrl,
chapters
)
}
deleteAllChapters={() =>
deleteAllChaptersAction(chapters)
}
novelName={novelName}
novelCover={novelCover}
novel={novel}
chapters={chapters}
trackerSheetRef={trackerSheetRef}
chaptersSettingsSheetRef={chaptersSettingsSheetRef}
theme={theme}
/>
}
refreshControl={
@@ -117,17 +101,25 @@ const Novel = ({
/>
}
/>
<Portal>
<BottomSheet
novelUrl={novelUrl}
bottomSheetRef={_panel}
sortAndFilterChapters={sortAndFilterChapters}
sort={sort}
filter={filter}
setSort={setSort}
setFilter={setFilter}
/>
</Portal>
{!loading && (
<Portal>
<ChaptersSettingsSheet
novelUrl={novelUrl}
bottomSheetRef={chaptersSettingsSheetRef}
sortAndFilterChapters={sortAndFilterChapters}
sort={sort}
filter={filter}
setSort={setSort}
setFilter={setFilter}
/>
<TrackSheet
bottomSheetRef={trackerSheetRef}
novelId={novel.novelId}
novelName={novel.novelName}
theme={theme}
/>
</Portal>
)}
</View>
</Provider>
);
@@ -142,15 +134,11 @@ const mapStateToProps = (state) => ({
export default connect(mapStateToProps, {
getNovelAction,
followNovelAction,
sortAndFilterChapters,
updateNovelAction,
downloadAllChaptersAction,
deleteAllChaptersAction,
})(Novel);
const styles = StyleSheet.create({
container: {
flex: 1,
},
container: { flex: 1 },
});
@@ -1,11 +1,11 @@
import React, { useState } from "react";
import { StyleSheet, View, Text } from "react-native";
import { StyleSheet, View, Text, Animated } from "react-native";
import { List, RadioButton, Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import { useSelector } from "react-redux";
export const BottomSheet = ({
const ChaptersSettingsSheet = ({
bottomSheetRef,
sort,
filter,
@@ -17,8 +17,11 @@ export const BottomSheet = ({
const theme = useSelector((state) => state.themeReducer.theme);
const [animatedValue] = useState(new Animated.Value(0));
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 370, bottom: 0 }}
snappingPoints={[0, 370]}
@@ -159,6 +162,8 @@ export const BottomSheet = ({
);
};
export default ChaptersSettingsSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
@@ -0,0 +1,53 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Chip } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useDispatch } from "react-redux";
import { followNovelAction } from "../../../../redux/novel/novel.actions";
const FollowChip = ({ followed, novel, theme }) => {
const dispatch = useDispatch();
const getIconName = () => (followed ? "heart" : "heart-outline");
const getChipLabel = () => (followed ? "In Library" : "Add to library");
return (
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={getIconName()}
size={21}
color={theme.colorAccentDark}
/>
)}
onPress={() => dispatch(followNovelAction(novel))}
style={[
{ backgroundColor: theme.colorPrimaryDark },
styles.toggleFavourite,
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimary,
}}
>
{getChipLabel()}
</Chip>
);
};
export default FollowChip;
const styles = StyleSheet.create({
toggleFavourite: {
height: 32,
justifyContent: "center",
alignItems: "center",
marginLeft: 16,
paddingLeft: 4,
borderWidth: 0,
elevation: 0,
},
});
@@ -0,0 +1,30 @@
import React from "react";
import { Text, StyleSheet } from "react-native";
export const GenreChip = ({ children, theme }) => (
<Text
style={[
styles.genreChip,
{
color: theme.colorAccentDark,
borderColor: theme.colorAccentDark,
backgroundColor: theme.colorPrimary,
},
]}
>
{children}
</Text>
);
const styles = StyleSheet.create({
genreChip: {
flex: 1,
justifyContent: "center",
paddingVertical: 2,
paddingHorizontal: 10,
marginHorizontal: 2,
fontSize: 13,
borderWidth: 1,
borderRadius: 50,
},
});
@@ -0,0 +1,24 @@
import React from "react";
import { View, ImageBackground, StyleSheet } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
const NovelCoverImage = ({ children, source, theme }) => {
return (
<ImageBackground source={source} style={styles.background}>
<LinearGradient
colors={["rgba(0,0,0,0.2)", theme.colorPrimaryDark]}
style={styles.linearGradient}
>
{children}
</LinearGradient>
</ImageBackground>
);
};
export default NovelCoverImage;
const styles = StyleSheet.create({
background: { height: 285 },
linearGradient: { height: 286 },
});
@@ -0,0 +1,66 @@
import React, { useState } from "react";
import { View, Text, StyleSheet } from "react-native";
const NovelSummary = ({ summary, followed, theme }) => {
const [more, showMore] = useState(!followed);
return (
summary !== "" && (
<View style={styles.summaryContainer}>
<Text
style={[
{ color: theme.textColorPrimary },
styles.summaryHeader,
]}
>
About
</Text>
<Text
style={{
color: theme.textColorSecondary,
lineHeight: 20,
}}
numberOfLines={more ? 200 : 2}
onPress={() => showMore(!more)}
ellipsizeMode="clip"
>
{summary}
</Text>
<Text
style={[
{
color: theme.colorAccentDark,
backgroundColor: theme.colorPrimaryDark,
},
styles.showMoreButton,
]}
onPress={() => showMore(!more)}
>
{more ? "Less" : "More"}
</Text>
</View>
)
);
};
export default NovelSummary;
const styles = StyleSheet.create({
summaryContainer: {
paddingHorizontal: 16,
marginBottom: 8,
},
summaryHeader: {
marginTop: 5,
paddingVertical: 5,
fontSize: 15,
fontWeight: "bold",
},
showMoreButton: {
fontWeight: "bold",
position: "absolute",
bottom: 0,
right: 18,
paddingLeft: 6,
},
});
@@ -0,0 +1,57 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Button } from "react-native-paper";
const ReadButton = ({ navigation, novel, chapters, theme }) => {
const lastRead = chapters.find((obj) => obj.read === 0);
return (
chapters.length > 0 &&
(lastRead ? (
<Button
color="white"
style={[
{ backgroundColor: theme.colorAccentDark },
styles.startButton,
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
onPress={() => {
navigation.navigate("ChapterItem", {
chapterId: lastRead.chapterId,
chapterUrl: lastRead.chapterUrl,
novelUrl: novel.novelUrl,
novelId: lastRead.novelId,
extensionId: novel.sourceId,
chapterName: lastRead.chapterName,
});
}}
>
{novel.unread ? `Start reading ` : `Continue reading `}
{lastRead.chapterName}
</Button>
) : (
<Button
color={theme.textColorHintDark}
style={[
styles.startButton,
{ backgroundColor: theme.colorDisabled },
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
>
All chapters read
</Button>
))
);
};
export default ReadButton;
const styles = StyleSheet.create({
startButton: {
marginTop: 8,
marginBottom: 16,
marginHorizontal: 16,
},
});
@@ -0,0 +1,48 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Chip } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
const TrackerChip = ({ isTracked, trackerSheetRef, theme }) => {
const getIconName = () => (isTracked ? "check" : "sync");
const getChipLabel = () => (isTracked ? "Tracked" : "Tracking");
return (
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={getIconName(isTracked)}
size={21}
color={theme.colorAccentDark}
/>
)}
onPress={() => trackerSheetRef.current.show()}
style={[
styles.toggleFavourite,
{ backgroundColor: theme.colorPrimaryDark },
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimary,
}}
>
{getChipLabel()}
</Chip>
);
};
export default TrackerChip;
const styles = StyleSheet.create({
toggleFavourite: {
height: 32,
justifyContent: "center",
alignItems: "center",
paddingLeft: 4,
marginLeft: 0,
borderWidth: 0,
elevation: 0,
},
});
+132 -272
View File
@@ -1,158 +1,122 @@
import React, { useState } from "react";
import {
View,
Text,
ImageBackground,
StyleSheet,
Image,
FlatList,
Share,
} from "react-native";
import {
TouchableRipple,
IconButton,
Button,
Chip,
Menu,
} from "react-native-paper";
import { View, Text, StyleSheet, Image, FlatList, Share } from "react-native";
import { TouchableRipple, IconButton, Button, Menu } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import * as WebBrowser from "expo-web-browser";
import { useSelector, useDispatch } from "react-redux";
import { LinearGradient } from "expo-linear-gradient";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import {
downloadAllChaptersAction,
deleteAllChaptersAction,
} from "../../../redux/novel/novel.actions";
import { GenreChip } from "./Info/GenreChip";
import NovelCoverImage from "./Info/NovelCoverImage";
import FollowChip from "./Info/FollowChip";
import TrackerChip from "./Info/TrackerChip";
import NovelSummary from "./Info/NovelSummary";
import ReadButton from "./Info/ReadButton";
const NovelInfoHeader = ({
item,
novel,
noOfChapters,
followNovelAction,
novelName,
novelCover,
loading,
bottomSheetRef,
novel,
chapters,
theme,
lastRead,
downloadAllChapters,
deleteAllChapters,
chaptersSettingsSheetRef,
trackerSheetRef,
}) => {
const navigation = useNavigation();
const dispatch = useDispatch();
const [more, setMore] = useState(
!loading && novel.followed !== 1 ? true : false
const [downloadMenu, showDownloadMenu] = useState(false);
const tracker = useSelector((state) => state.trackerReducer.tracker);
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
const [visible, setVisible] = useState(false);
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
let isTracked = false;
const renderGenreChip = ({ item }) => (
<Text
style={[
styles.genre,
{
color: theme.colorAccentDark,
borderColor: theme.colorAccentDark,
backgroundColor: theme.colorPrimary,
},
]}
>
{item}
</Text>
);
if (!loading) {
isTracked = trackedNovels.find((obj) => obj.novelId === novel.novelId);
}
const getGenres = ({ item }) => <GenreChip theme={theme}>{item}</GenreChip>;
return (
<View style={{ flexGrow: 1 }}>
<ImageBackground
source={{ uri: item.novelCover }}
style={styles.background}
>
<LinearGradient
colors={["rgba(0,0,0,0.2)", theme.colorPrimaryDark]}
style={styles.linearGradient}
>
<View style={styles.detailsContainer}>
<View>
<Image
source={{ uri: item.novelCover }}
style={styles.logo}
/>
</View>
<View style={styles.nameContainer}>
<Text
numberOfLines={2}
style={[
styles.name,
{ color: theme.textColorPrimary },
]}
>
{item.novelName}
</Text>
{!loading && (
<>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
fontWeight: "bold",
}}
numberOfLines={2}
>
{novel.author.replace(",", ", ")}
</Text>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
}}
numberOfLines={1}
>
{novel.status}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 14,
}}
numberOfLines={1}
>
{novel.source}
</Text>
</>
)}
</View>
<NovelCoverImage source={{ uri: novelCover }} theme={theme}>
<View style={styles.detailsContainer}>
<View>
<Image
source={{ uri: novelCover }}
style={styles.logo}
/>
</View>
</LinearGradient>
</ImageBackground>
<View style={styles.nameContainer}>
<Text
numberOfLines={2}
style={[
styles.name,
{ color: theme.textColorPrimary },
]}
>
{novelName}
</Text>
{!loading && (
<>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
fontWeight: "bold",
}}
numberOfLines={2}
>
{novel.author.replace(",", ", ")}
</Text>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
}}
numberOfLines={1}
>
{novel.status}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 14,
}}
numberOfLines={1}
>
{novel.source}
</Text>
</>
)}
</View>
</View>
</NovelCoverImage>
{!loading && (
<>
<View style={styles.buttonsContainer}>
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={
novel.followed
? "heart"
: "heart-outline"
}
size={21}
color={theme.colorAccentDark}
/>
)}
onPress={() => followNovelAction(novel)}
style={[
{ backgroundColor: theme.colorPrimaryDark },
styles.toggleFavourite,
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimary,
}}
>
{novel.followed ? "In Library" : "Add to library"}
</Chip>
<FollowChip
theme={theme}
followed={novel.followed}
novel={novel}
/>
{tracker && (
<TrackerChip
theme={theme}
isTracked={isTracked}
trackerSheetRef={trackerSheetRef}
/>
)}
<IconButton
onPress={() =>
WebBrowser.openBrowserAsync(novel.sourceUrl)
@@ -169,16 +133,15 @@ const NovelInfoHeader = ({
color={theme.colorAccentDark}
size={21}
/>
<Menu
visible={visible}
onDismiss={closeMenu}
visible={downloadMenu}
onDismiss={() => showDownloadMenu(false)}
anchor={
<IconButton
icon="download"
color={theme.colorAccentDark}
size={21}
onPress={openMenu}
onPress={() => showDownloadMenu(true)}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
@@ -187,58 +150,31 @@ const NovelInfoHeader = ({
title="Download all"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{ color: theme.textColorPrimary }}
onPress={downloadAllChapters}
onPress={() =>
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
)
)
}
/>
<Menu.Item
title="Delete downloads"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{ color: theme.textColorPrimary }}
onPress={deleteAllChapters}
onPress={() =>
dispatch(deleteAllChaptersAction(chapters))
}
/>
</Menu>
</View>
{novel.novelSummary !== "" && (
<View
style={{
paddingHorizontal: 16,
marginBottom: 8,
}}
>
<Text
style={[
{ color: theme.textColorPrimary },
styles.summaryHeader,
]}
>
About
</Text>
<Text
style={{
color: theme.textColorSecondary,
lineHeight: 20,
}}
numberOfLines={more ? 100 : 2}
onPress={() => setMore(!more)}
ellipsizeMode="clip"
>
{novel.novelSummary}
</Text>
<Text
style={[
{
color: theme.colorAccentDark,
backgroundColor: theme.colorPrimaryDark,
},
styles.moreBtn,
]}
onPress={() => setMore(!more)}
>
{more ? "Less " : "More "}
</Text>
</View>
)}
<NovelSummary
summary={novel.novelSummary}
followed={novel.followed}
theme={theme}
/>
<FlatList
style={
novel.novelSummary === ""
@@ -247,57 +183,23 @@ const NovelInfoHeader = ({
}
contentContainerStyle={styles.genreContainer}
horizontal
data={novel.genre && novel.genre.split(",")}
data={novel.genre.split(",")}
keyExtractor={(item) => item}
renderItem={renderGenreChip}
renderItem={getGenres}
showsHorizontalScrollIndicator={false}
/>
{noOfChapters > 0 &&
(lastRead ? (
<Button
color="white"
style={[
{ backgroundColor: theme.colorAccentDark },
styles.startButton,
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
onPress={() => {
navigation.navigate("ChapterItem", {
chapterId: lastRead.chapterId,
chapterUrl: lastRead.chapterUrl,
novelUrl: novel.novelUrl,
novelId: lastRead.novelId,
extensionId: novel.sourceId,
chapterName: lastRead.chapterName,
});
}}
>
{novel.unread
? `Start reading `
: `Continue reading `}
{lastRead.chapterName}
</Button>
) : (
<Button
color={theme.textColorPrimary}
style={[
{
backgroundColor: theme.textColorPrimary,
opacity: 0.2,
},
styles.startButton,
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
>
All chapters read
</Button>
))}
<ReadButton
novel={novel}
chapters={chapters}
navigation={navigation}
theme={theme}
/>
<TouchableRipple
style={styles.bottomsheet}
onPress={() =>
bottomSheetRef.current.show({ velocity: -1.5 })
chaptersSettingsSheetRef.current.show({
velocity: -1.5,
})
}
rippleColor={theme.rippleColor}
>
@@ -308,7 +210,7 @@ const NovelInfoHeader = ({
styles.chapters,
]}
>
{noOfChapters + " Chapters"}
{`${chapters.length} Chapters`}
</Text>
<IconButton
icon="filter-variant"
@@ -331,12 +233,6 @@ const styles = StyleSheet.create({
marginHorizontal: 16,
paddingTop: 8,
},
background: {
height: 285,
},
linearGradient: {
height: 286,
},
detailsContainer: {
flex: 1,
flexDirection: "row",
@@ -349,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,
@@ -367,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,
@@ -389,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 },
});
-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;
};
+63 -2
View File
@@ -1,6 +1,6 @@
import * as SQLite from "expo-sqlite";
import { getLibrary } from "../database/queries/LibraryQueries";
import { fetchNovel } from "../source/Source";
import { fetchChapters, fetchNovel } from "../source/Source";
const db = SQLite.openDatabase("lnreader.db");
import { ToastAndroid } from "react-native";
@@ -62,6 +62,50 @@ export const updateNovel = async (extensionId, novelUrl, novelId) => {
});
};
export const updateNovelChapters = async (extensionId, novelUrl, novelId) => {
let chapters = await fetchChapters(extensionId, novelUrl);
db.transaction((tx) => {
chapters.map((chapter) =>
tx.executeSql(
"INSERT OR IGNORE INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)",
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
novelId,
],
(txObj, { insertId }) => {
if (insertId !== -1) {
console.log(
"Inserted Chapter " +
chapter.chapterUrl +
" Chapter Name " +
chapter.chapterName +
" Novel Id " +
novelId
);
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
(txObj, res) => {
// console.log(
// "Inserted Chapter Id -> " +
// insertId +
// " Novel Id " +
// novelId
// );
},
(txObj, error) => console.log("Error ", error)
);
}
},
(txObj, error) => console.log("Error ", error)
)
);
});
};
export const updateAllNovels = async () => {
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
@@ -69,8 +113,25 @@ export const updateAllNovels = async () => {
libraryNovels.map((novel, index) =>
setTimeout(async () => {
updateNovel(novel.sourceId, novel.novelUrl, novel.novelId);
updateNovelChapters(novel.sourceId, novel.novelUrl, novel.novelId);
console.log(novel.novelName + " Updated");
}, 3000 * index)
);
};
export const parseChapterNumber = (chapterName) => {
chapterName = chapterName.toLowerCase();
chapterName = chapterName.replace(/volume (\d+)/, "");
const basic = chapterName.match(/ch (\d+)/);
const occurrence = chapterName.match(/\d+/);
if (basic) {
return basic[0];
} else if (occurrence) {
return occurrence[0];
} else {
return 0;
}
};
+16 -3
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;
};
+4
View File
@@ -27,6 +27,7 @@ export const lightTheme = {
searchBarColor: "#FFFFFF",
menuColor: "#FFFFFF",
dividerColor: "rgba(0,0,0,0.1)",
colorDisabled: "rgba(220,220,220,255)",
statusBar: "dark",
};
@@ -46,6 +47,7 @@ export const darkTheme = {
searchBarColor: "#303135",
menuColor: "#242529",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
statusBar: "light",
};
@@ -65,6 +67,7 @@ export const amoledDarkTheme = {
searchBarColor: "#1F1F1F",
menuColor: "#242529",
dividerColor: "rgba(255,255,255,0.15)",
colorDisabled: "rgba(33,33,33,255)",
statusBar: "light",
};
@@ -84,5 +87,6 @@ export const midnightDuskTheme = {
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);
}
}
};