Add MAL tracking logic
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
"expo": {
|
||||
"name": "LNReader",
|
||||
"slug": "LNReader",
|
||||
"version": "1.0.0",
|
||||
"scheme": "lnreader",
|
||||
"version": "1.0.14",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"splash": {
|
||||
|
||||
Generated
+32
-3
@@ -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",
|
||||
|
||||
@@ -24,11 +24,13 @@
|
||||
"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-reanimated": "^1.13.2",
|
||||
|
||||
+10
-1
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -106,7 +106,7 @@ export const followNovelAction = (novel) => async (dispatch) => {
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_IN_LIBRARY,
|
||||
payload: !novel.followed,
|
||||
payload: { novelUrl: novel.novelUrl, followed: !novel.followed },
|
||||
});
|
||||
|
||||
ToastAndroid.show(
|
||||
|
||||
@@ -53,7 +53,7 @@ const novelReducer = (state = initialState, action) => {
|
||||
case UPDATE_IN_LIBRARY:
|
||||
return {
|
||||
...state,
|
||||
novel: { ...state.novel, followed: payload },
|
||||
novel: { ...state.novel, followed: payload.followed },
|
||||
};
|
||||
case UPDATE_NOVEL:
|
||||
return {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
REMOVE_TRACKER,
|
||||
SET_TRACKER,
|
||||
TRACK_NOVEL,
|
||||
UNTRACK_NOVEL,
|
||||
} from "./tracker.types";
|
||||
|
||||
export const setTracker = (res) => async (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_TRACKER,
|
||||
payload: res,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeTracker = () => async (dispatch) => {
|
||||
dispatch({ type: REMOVE_TRACKER });
|
||||
};
|
||||
|
||||
export const trackNovel = (id) => async (dispatch) => {
|
||||
dispatch({ type: TRACK_NOVEL, payload: id });
|
||||
};
|
||||
|
||||
export const untrackNovel = (id) => async (dispatch) => {
|
||||
dispatch({ type: UNTRACK_NOVEL, payload: id });
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
REMOVE_TRACKER,
|
||||
SET_TRACKER,
|
||||
TRACK_NOVEL,
|
||||
UNTRACK_NOVEL,
|
||||
} 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:
|
||||
// console.log(payload);
|
||||
return {
|
||||
...state,
|
||||
trackedNovels: [...state.trackedNovels, payload],
|
||||
};
|
||||
|
||||
case UNTRACK_NOVEL:
|
||||
return {
|
||||
...state,
|
||||
trackedNovels: state.trackedNovels.filter(
|
||||
(id) => id === payload
|
||||
),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default trackerReducer;
|
||||
@@ -0,0 +1,5 @@
|
||||
export const SET_TRACKER = "SET_TRACKER";
|
||||
export const REMOVE_TRACKER = "REMOVE_TRACKER";
|
||||
|
||||
export const TRACK_NOVEL = "TRACK_NOVEL";
|
||||
export const UNTRACK_NOVEL = "UNTRACK_NOVEL";
|
||||
@@ -30,16 +30,19 @@ const LibraryScreen = ({
|
||||
getLibraryAction,
|
||||
searchLibraryAction,
|
||||
setNovel,
|
||||
searchResults,
|
||||
// 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 +88,7 @@ const LibraryScreen = ({
|
||||
}}
|
||||
numColumns={itemsPerRow}
|
||||
key={itemsPerRow}
|
||||
data={!searchText ? novels : searchResults}
|
||||
data={novels}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
@@ -129,7 +132,7 @@ const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
itemsPerRow: state.settingsReducer.itemsPerRow,
|
||||
novels: state.libraryReducer.novels,
|
||||
searchResults: state.libraryReducer.searchResults,
|
||||
// searchResults: state.libraryReducer.searchResults,
|
||||
loading: state.libraryReducer.loading,
|
||||
});
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ const AboutScreen = ({ navigation }) => {
|
||||
<ListSection>
|
||||
<ListItem
|
||||
title="Version"
|
||||
description="Stable 1.0.13"
|
||||
description="Stable 1.0.14"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Build Time"
|
||||
description="24-04-21 21:00 PM"
|
||||
description="26-04-21 20:45 PM"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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 } from "../../trackers/MyAnimeList";
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { 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}
|
||||
/>
|
||||
</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;
|
||||
@@ -3,16 +3,23 @@ import {
|
||||
StyleSheet,
|
||||
View,
|
||||
FlatList,
|
||||
Text,
|
||||
RefreshControl,
|
||||
ToastAndroid,
|
||||
} from "react-native";
|
||||
import { Provider, Portal } from "react-native-paper";
|
||||
import {
|
||||
Provider,
|
||||
Portal,
|
||||
Modal,
|
||||
TextInput,
|
||||
IconButton,
|
||||
} from "react-native-paper";
|
||||
|
||||
import ChapterCard from "./components/ChapterCard";
|
||||
import NovelInfoHeader from "./components/NovelHeader";
|
||||
import { BottomSheet } from "./components/BottomSheet";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { connect, useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
getNovelAction,
|
||||
@@ -22,6 +29,8 @@ import {
|
||||
downloadAllChaptersAction,
|
||||
deleteAllChaptersAction,
|
||||
} from "../../redux/novel/novel.actions";
|
||||
import { TrackerBottomSheet } from "./components/TrackerBottomSheet";
|
||||
import TrackerSearchModal from "./components/TrackerSearchModal";
|
||||
|
||||
const Novel = ({
|
||||
route,
|
||||
@@ -48,9 +57,15 @@ const Novel = ({
|
||||
|
||||
let _panel = useRef(null); // Bottomsheet ref
|
||||
|
||||
let trackerBottomsheet = useRef(null); // Bottomsheet ref
|
||||
|
||||
const [sort, setSort] = useState("ORDER BY chapterId ASC");
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const trackedNovels = useSelector(
|
||||
(state) => state.trackerReducer.trackedNovels
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter);
|
||||
}, [getNovelAction, followNovelAction, sort, filter]);
|
||||
@@ -70,6 +85,13 @@ const Novel = ({
|
||||
ToastAndroid.show(`Updated ${novelName}`, ToastAndroid.SHORT);
|
||||
};
|
||||
|
||||
// Tracker Modal
|
||||
const [visible, setVisible] = useState(false);
|
||||
const showModal = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
const hideModal = () => setVisible(false);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<View
|
||||
@@ -96,6 +118,7 @@ const Novel = ({
|
||||
loading={loading}
|
||||
lastRead={lastRead}
|
||||
bottomSheetRef={_panel}
|
||||
trackerBottomsheetRef={trackerBottomsheet}
|
||||
downloadAllChapters={() =>
|
||||
downloadAllChaptersAction(
|
||||
sourceId,
|
||||
@@ -127,6 +150,18 @@ const Novel = ({
|
||||
setSort={setSort}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
<TrackerBottomSheet
|
||||
bottomSheetRef={trackerBottomsheet}
|
||||
showModal={showModal}
|
||||
novelId={novelId}
|
||||
/>
|
||||
<TrackerSearchModal
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
novelId={novelId}
|
||||
novelName={novelName}
|
||||
theme={theme}
|
||||
/>
|
||||
</Portal>
|
||||
</View>
|
||||
</Provider>
|
||||
|
||||
@@ -31,6 +31,7 @@ const NovelInfoHeader = ({
|
||||
theme,
|
||||
lastRead,
|
||||
downloadAllChapters,
|
||||
trackerBottomsheetRef,
|
||||
deleteAllChapters,
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
@@ -161,6 +162,7 @@ const NovelInfoHeader = ({
|
||||
color={theme.colorAccentDark}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={() =>
|
||||
Share.share({ message: novel.sourceUrl })
|
||||
@@ -169,7 +171,12 @@ const NovelInfoHeader = ({
|
||||
color={theme.colorAccentDark}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={() => trackerBottomsheetRef.current.show()}
|
||||
icon="sync"
|
||||
color={theme.colorAccentDark}
|
||||
size={21}
|
||||
/>
|
||||
<Menu
|
||||
visible={visible}
|
||||
onDismiss={closeMenu}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { findListItem } from "../../../trackers/MyAnimeList";
|
||||
|
||||
export const TrackerBottomSheet = ({ bottomSheetRef, novelId, showModal }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
const tracker = useSelector((state) => state.trackerReducer.tracker);
|
||||
const trackedNovels = useSelector(
|
||||
(state) => state.trackerReducer.trackedNovels
|
||||
);
|
||||
|
||||
const [myListStatus, setMyListStatus] = useState();
|
||||
|
||||
const isNovelTracked = async (id) => {
|
||||
let trackingStatus =
|
||||
trackedNovels && trackedNovels.find((obj) => obj.novelId === id);
|
||||
if (trackingStatus) {
|
||||
let res = await findListItem(
|
||||
trackingStatus.malId,
|
||||
tracker.access_token
|
||||
);
|
||||
setMyListStatus(res);
|
||||
console.log(res);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
isNovelTracked(novelId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 70, bottom: 0 }}
|
||||
snappingPoints={[0, 70]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
{!myListStatus ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
margin: 8,
|
||||
borderRadius: 4,
|
||||
elevation: 2,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{
|
||||
uri:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/7/7a/MyAnimeList_Logo.png",
|
||||
}}
|
||||
style={{
|
||||
height: 50,
|
||||
width: 50,
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlign: "center",
|
||||
color: theme.colorAccentDark,
|
||||
}}
|
||||
onPress={showModal}
|
||||
>
|
||||
Add Tracking
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text>{myListStatus.title}</Text>
|
||||
)}
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
// borderTopRightRadius: 8,
|
||||
// borderTopLeftRadius: 8,
|
||||
},
|
||||
bottomSheetHandle: {
|
||||
backgroundColor: "rgba(255,255,255,0.25)",
|
||||
height: 4,
|
||||
width: 50,
|
||||
borderRadius: 50,
|
||||
top: 10,
|
||||
alignSelf: "center",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
import React, { useEffect, useState, useRef } 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 TrackerSearchModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
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 textInputRef = useRef(null);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getSearchresults = async () => {
|
||||
const res = await searchNovels(searchText, tracker.access_token);
|
||||
setSearchResults(res);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
visible && getSearchresults();
|
||||
}, [visible]);
|
||||
|
||||
const renderSearcResultCard = (item) => (
|
||||
<TouchableRipple
|
||||
style={[
|
||||
{ flexDirection: "row", borderRadius: 4, margin: 8 },
|
||||
selectedNovel &&
|
||||
selectedNovel === item.node.id && {
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
key={item.node.id}
|
||||
onPress={() => setSelectedNovel(item.node.id)}
|
||||
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={visible}
|
||||
onDismiss={hideModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
value={searchText}
|
||||
onChangeText={(text) => setSearchText(text)}
|
||||
onSubmitEditing={getSearchresults}
|
||||
theme={{
|
||||
colors: {
|
||||
primary: theme.colorAccentDark,
|
||||
text: theme.textColorPrimary,
|
||||
background: "transparent",
|
||||
},
|
||||
}}
|
||||
underlineColor={theme.textColorHintDark}
|
||||
dense
|
||||
right={
|
||||
<TextInput.Icon
|
||||
color={theme.textColorSecondary}
|
||||
icon="close"
|
||||
onPress={() => setSearchText("")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ScrollView
|
||||
style={{ flexGrow: 1, maxHeight: 600, paddingVertical: 12 }}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
color={theme.colorAccentDark}
|
||||
size={45}
|
||||
style={{ margin: 16 }}
|
||||
/>
|
||||
) : (
|
||||
searchResults.data.map((result) =>
|
||||
renderSearcResultCard(result)
|
||||
)
|
||||
)}
|
||||
</ScrollView>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{ marginTop: 30 }}
|
||||
labelStyle={{
|
||||
color: theme.colorAccentDark,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccentDark } }}
|
||||
onPress={() => setSelectedNovel(null)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<Button
|
||||
style={{ marginTop: 30 }}
|
||||
labelStyle={{
|
||||
color: theme.colorAccentDark,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccentDark } }}
|
||||
onPress={hideModal}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginTop: 30 }}
|
||||
labelStyle={{
|
||||
color: theme.colorAccentDark,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccentDark } }}
|
||||
onPress={() => {
|
||||
if (selectedNovel) {
|
||||
dispatch(
|
||||
trackNovel({
|
||||
novelId,
|
||||
malId: selectedNovel,
|
||||
})
|
||||
);
|
||||
}
|
||||
hideModal();
|
||||
}}
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrackerSearchModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
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),
|
||||
});
|
||||
|
||||
const tokenResponse = await response.json();
|
||||
|
||||
return tokenResponse;
|
||||
};
|
||||
|
||||
export const searchNovels = async (novelName, bearerToken) => {
|
||||
const searchrl = `https://api.myanimelist.net/v2/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 = `https://api.myanimelist.net/v2/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) {
|
||||
myListStatus = {
|
||||
status: "reading",
|
||||
num_chapters_read: 0,
|
||||
};
|
||||
|
||||
listItem.my_list_status = myListStatus;
|
||||
}
|
||||
|
||||
return listItem;
|
||||
};
|
||||
Reference in New Issue
Block a user