diff --git a/App.tsx b/App.tsx index 0329d0c..07ddfd1 100644 --- a/App.tsx +++ b/App.tsx @@ -1,20 +1,39 @@ +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import Chapter from './src/pages/chapter'; +import Comic from './src/pages/comic'; +import ComicProfile from './src/pages/comicprofile'; +import Home from './src/pages/home'; +import { ComicProviderKey } from './src/provider'; +import { NonLocalProviderKey } from './src/provider/local'; +import Save from './src/pages/save'; + +const Stack = createNativeStackNavigator(); +const queryClient = new QueryClient(); + +export type RootStackParamList = { + Home: undefined; + ComicProfile: { provider: ComicProviderKey; comicId: string; }; + Chapter: { provider: ComicProviderKey; comicId: string; chapterId: string; }; + Page: { provider: ComicProviderKey; comicId: string; pageId: string; }; + Save: { provider: NonLocalProviderKey; comicId: string; }; +} export default function App() { return ( - - Open up App.tsx to start working on your app! - - + + + + + + + + + + + + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/app.json b/app.json index ad8093d..e85bf50 100644 --- a/app.json +++ b/app.json @@ -5,7 +5,7 @@ "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", - "userInterfaceStyle": "light", + "userInterfaceStyle": "dark", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", diff --git a/package.json b/package.json index b270aea..6eb2213 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,25 @@ "web": "expo start --web" }, "dependencies": { + "@expo/webpack-config": "^0.17.0", + "@react-native-picker/picker": "2.4.2", + "@react-navigation/native": "^6.0.12", + "@react-navigation/native-stack": "^6.8.0", + "@types/react-native-vector-icons": "^6.4.12", + "cheerio": "^1.0.0-rc.12", "expo": "~46.0.9", + "expo-file-system": "~14.1.0", "expo-status-bar": "~1.4.0", "react": "18.0.0", "react-dom": "18.0.0", "react-native": "0.69.5", - "react-native-web": "~0.18.7" + "react-native-gesture-handler": "~2.5.0", + "react-native-modalize": "^2.1.1", + "react-native-safe-area-context": "4.3.1", + "react-native-screens": "~3.15.0", + "react-native-vector-icons": "^9.2.0", + "react-native-web": "~0.18.7", + "react-query": "^3.39.2" }, "devDependencies": { "@babel/core": "^7.12.9", diff --git a/src/components/ChapterList.tsx b/src/components/ChapterList.tsx new file mode 100644 index 0000000..d9da8b2 --- /dev/null +++ b/src/components/ChapterList.tsx @@ -0,0 +1,46 @@ +import { useNavigation } from "@react-navigation/native"; +import { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import { FC } from "react"; +import { ActivityIndicator, FlatList, StyleSheet, Text, View } from "react-native"; +import { RootStackParamList } from "../../App"; +import { SimpleChapter as ChapterData, ComicProviderKey, useComicChapters } from "../provider"; +import Touchable from "./Touchable"; + +interface Props { + provider: ComicProviderKey; + comicId: string; +} + +const Chapter: FC void }> = (props) => { + return props.onClick(props.id)}> + + {props.title} + + +} + +const ChapterList: FC = (props) => { + const { data, error, isLoading } = useComicChapters(props.provider, props.comicId); + const navigation = useNavigation>(); + + if (isLoading) return ; + if (error) return Failed to load comic: {error.toString()}; + if (!data) return Whoops! We couldn't find that comic :/; + + function openChapter(chapterId: string) { + navigation.navigate('Chapter', { + ...props, + chapterId, + }); + } + + return } keyExtractor={item => item.id} /> +} + +export default ChapterList; + +const styles = StyleSheet.create({ + chapter: { + padding: 8, + }, +}); diff --git a/src/components/ComicCard.tsx b/src/components/ComicCard.tsx new file mode 100644 index 0000000..6641243 --- /dev/null +++ b/src/components/ComicCard.tsx @@ -0,0 +1,47 @@ +import { FC } from "react"; +import { Image, StyleSheet, Text, View } from "react-native"; +import { ComicMetadata } from "../provider"; + +interface Props { + comic: ComicMetadata +} + +const ComicCard: FC = (props) => { + return + + + {props.comic.title} + {props.comic.shortDescription} + By {props.comic.author} + + +} + +export default ComicCard; + +const styles = StyleSheet.create({ + aboutRow: { + flexDirection: 'row', + alignContent: 'center', + }, + titleContainer: { + flex: 1, + flexDirection: 'column', + paddingLeft: 8, + }, + title: { + fontSize: 24, + }, + subtitle: { + fontSize: 16, + }, + author: { + fontSize: 16, + color: '#888', + }, + avatar: { + borderRadius: 8, + width: 128, + height: 128, + }, +}); diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx new file mode 100644 index 0000000..a2f0438 --- /dev/null +++ b/src/components/Divider.tsx @@ -0,0 +1,10 @@ +import { View } from "react-native"; + +export default function Divider() { + return +} diff --git a/src/components/Touchable.tsx b/src/components/Touchable.tsx new file mode 100644 index 0000000..25c5f87 --- /dev/null +++ b/src/components/Touchable.tsx @@ -0,0 +1,12 @@ +import { FC } from "react"; +import { Platform, TouchableNativeFeedback, TouchableOpacity, TouchableOpacityProps } from "react-native"; + +const Touchable: FC = (props) => { + if (Platform.OS === 'android') { + return + } else { + return + } +} + +export default Touchable; diff --git a/src/pages/chapter.tsx b/src/pages/chapter.tsx new file mode 100644 index 0000000..5f21612 --- /dev/null +++ b/src/pages/chapter.tsx @@ -0,0 +1,54 @@ +import { NativeStackScreenProps } from "@react-navigation/native-stack"; +import { FC } from "react"; +import { ActivityIndicator, FlatList, Platform, StyleSheet, Text, TouchableNativeFeedback, TouchableOpacity, View } from "react-native"; +import { RootStackParamList } from "../../App"; +import Divider from "../components/Divider"; +import Touchable from "../components/Touchable"; +import { SimplePage, useComicChapter } from "../provider"; + +const Page: FC void }> = (props) => { + return props.onClick(props.id)}> + + {props.title} + + +} + +export default function Chapter(props: Props) { + const { data, error, isLoading } = useComicChapter(props.route.params.provider, props.route.params.comicId, props.route.params.chapterId, props.navigation); + + if (isLoading) return ; + if (error) return Failed to load comic: {error.toString()}; + if (!data) return Whoops! We couldn't find that chapter :/; + + function openPage(pageId: string) { + props.navigation.navigate('Page', { + ...props.route.params, + pageId, + }); + } + + return + + {data.description} + + + } keyExtractor={item => item.id} /> + +} + +type Props = NativeStackScreenProps; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + }, + description: { + fontSize: 16, + paddingLeft: 8, + }, + page: { + padding: 8, + }, +}); diff --git a/src/pages/comic.tsx b/src/pages/comic.tsx new file mode 100644 index 0000000..5db300a --- /dev/null +++ b/src/pages/comic.tsx @@ -0,0 +1,72 @@ +import { NativeStackScreenProps } from "@react-navigation/native-stack"; +import { ActivityIndicator, Image, ScrollView, StyleSheet, Text, View } from "react-native"; +import { RootStackParamList } from "../../App"; +import Touchable from "../components/Touchable"; +import { useComicPage } from "../provider"; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; + +export default function Comic(props: Props) { + const { data, error, isLoading } = useComicPage(props.route.params.provider, props.route.params.comicId, props.route.params.pageId, props.navigation); + + if (isLoading) return ; + if (error) return Failed to load comic: {error.toString()}; + if (!data) return Whoops! We couldn't find that page :/; + + function newPage(pageId?: string) { + if (!pageId) return; + props.navigation.pop(); + props.navigation.push('Page', { + ...props.route.params, + pageId, + }); + } + + return + {data.imageSegments.map((item) => )} + + + newPage(data.previousPageId)} disabled={data.previousPageId === undefined}> + + + + Previous + + + + newPage(data.nextPageId)} disabled={data.nextPageId === undefined}> + + + + Next + + + + + +} + +type Props = NativeStackScreenProps; + +const styles = StyleSheet.create({ + image: { + width: '100%', + }, + navigationRow: { + flexDirection: 'row', + width: '100%', + // justifyContent: 'space-evenly', + }, + button: { + // padding: 4, + flexDirection: 'column', + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + buttonLabel: { + paddingTop: 4, + } +}); diff --git a/src/pages/comicprofile.tsx b/src/pages/comicprofile.tsx new file mode 100644 index 0000000..f3cd084 --- /dev/null +++ b/src/pages/comicprofile.tsx @@ -0,0 +1,48 @@ +import { NativeStackScreenProps } from "@react-navigation/native-stack"; +import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from "react-native"; +import { RootStackParamList } from "../../App"; +import ComicCard from "../components/ComicCard"; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import { useComicMetadata } from "../provider"; +import Divider from "../components/Divider"; +import ChapterList from "../components/ChapterList"; + +export default function ComicProfile(props: Props) { + const comicQuery = useComicMetadata(props.route.params.provider, props.route.params.comicId, (data) => { + props.navigation.setOptions({ + title: data.title, + headerRight: props.route.params.provider === 'local' ? undefined : () => ( + { + const { provider, comicId } = props.route.params; + if (provider !== 'local') { + props.navigation.push('Save', { + provider, + comicId, + }); + } + }}> + + + ), + }); + }); + + if (comicQuery.isLoading) return ; + if (comicQuery.error) return Failed to load comic: {comicQuery.error.toString()}; + if (!comicQuery.data) return Whoops! We couldn't find that comic :/; + + return + + + + +} + +type Props = NativeStackScreenProps; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + }, +}) diff --git a/src/pages/home.tsx b/src/pages/home.tsx new file mode 100644 index 0000000..45884c4 --- /dev/null +++ b/src/pages/home.tsx @@ -0,0 +1,53 @@ +import { Picker } from "@react-native-picker/picker"; +import { NativeStackScreenProps } from "@react-navigation/native-stack"; +import { useState } from "react"; +import { Button, StyleSheet, Text, TextInput, View } from "react-native"; +import { useQueryClient } from "react-query"; +import { RootStackParamList } from "../../App"; +import { ComicProviderKey } from "../provider"; +import * as FileSystem from 'expo-file-system'; + +export default function Home(props: Props) { + const [selectedProvider, setSelectedProvider] = useState('comicfury'); + const [comicId, setComicId] = useState(''); + const queryClient = useQueryClient(); + + return + Hello, World! + style={styles.input} selectedValue={selectedProvider} onValueChange={(v) => setSelectedProvider(v)}> + + + + + +