feat: start work on new home screen and other QoL changes
This commit is contained in:
parent
73423d9789
commit
0e9f53a9b9
9 changed files with 195 additions and 14 deletions
11
App.tsx
11
App.tsx
|
@ -1,4 +1,4 @@
|
|||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { NavigationContainer, NavigatorScreenParams } from '@react-navigation/native';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
@ -9,12 +9,14 @@ import Home from './src/pages/home';
|
|||
import { ComicProviderKey } from './src/provider';
|
||||
import { NonLocalProviderKey } from './src/provider/local';
|
||||
import Save from './src/pages/save';
|
||||
import ComicList, { TabParamList } from './src/pages/comiclist';
|
||||
|
||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export type RootStackParamList = {
|
||||
Home: undefined;
|
||||
OldHome: undefined;
|
||||
Home: NavigatorScreenParams<TabParamList>;
|
||||
ComicProfile: { provider: ComicProviderKey; comicId: string; };
|
||||
Chapter: { provider: ComicProviderKey; comicId: string; chapterId: string; };
|
||||
Page: { provider: ComicProviderKey; comicId: string; pageId: string; };
|
||||
|
@ -26,7 +28,10 @@ export default function App() {
|
|||
<QueryClientProvider client={queryClient}>
|
||||
<NavigationContainer>
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen name="Home" component={Home} />
|
||||
<Stack.Screen name="Home" component={ComicList} options={{
|
||||
headerShown: false,
|
||||
}} />
|
||||
<Stack.Screen name="OldHome" component={Home} />
|
||||
<Stack.Screen name="ComicProfile" component={ComicProfile} />
|
||||
<Stack.Screen name="Chapter" component={Chapter} />
|
||||
<Stack.Screen name="Page" component={Comic} />
|
||||
|
|
|
@ -22,10 +22,14 @@
|
|||
"react": "18.0.0",
|
||||
"react-dom": "18.0.0",
|
||||
"react-native": "0.69.5",
|
||||
"react-native-fast-image": "^8.6.1",
|
||||
"react-native-gesture-handler": "~2.5.0",
|
||||
"react-native-image-progress": "^1.2.0",
|
||||
"react-native-modalize": "^2.1.1",
|
||||
"react-native-progress": "^5.0.0",
|
||||
"react-native-safe-area-context": "4.3.1",
|
||||
"react-native-screens": "~3.15.0",
|
||||
"react-native-svg": "12.3.0",
|
||||
"react-native-web": "~0.18.7",
|
||||
"react-query": "^3.39.2"
|
||||
},
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
import { ActivityIndicator, Image, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
import { ActivityIndicator, Image as NativeImage, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
import { createImageProgress } from "react-native-image-progress";
|
||||
import { Pie } from 'react-native-progress';
|
||||
import { RootStackParamList } from "../../App";
|
||||
import Touchable from "../components/Touchable";
|
||||
import { useComicPage } from "../provider";
|
||||
|
||||
const Image = createImageProgress(NativeImage);
|
||||
|
||||
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);
|
||||
|
||||
|
@ -25,7 +29,7 @@ export default function Comic(props: Props) {
|
|||
{data.imageSegments.map((item) => <Image style={{
|
||||
width: '100%',
|
||||
aspectRatio: item.width / item.height,
|
||||
}} source={{ uri: item.url }} resizeMode='center' key={item.url} />)}
|
||||
}} source={{ uri: item.url }} resizeMode='center' key={item.url} indicator={Pie} />)}
|
||||
|
||||
<View style={styles.navigationRow}>
|
||||
<Touchable onPress={() => newPage(data.previousPageId)} disabled={data.previousPageId === undefined}>
|
||||
|
|
56
src/pages/comiclist.tsx
Normal file
56
src/pages/comiclist.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||
import LocalComicList from "./comiclist/Local";
|
||||
|
||||
const Tab = createBottomTabNavigator<TabParamList>();
|
||||
|
||||
export type TabParamList = {
|
||||
Local: undefined;
|
||||
Remote: undefined;
|
||||
};
|
||||
|
||||
function Remote() {
|
||||
return (
|
||||
<View>
|
||||
<Text>Hello, World!</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ComicList() {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={({ navigation, route }) => {
|
||||
return {
|
||||
headerRight: () => (
|
||||
<View style={styles.padding}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.push("OldHome");
|
||||
}}
|
||||
>
|
||||
<MaterialCommunityIcons name="cog" size={24} style={{ color: "black" }} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
),
|
||||
tabBarIcon: (({ focused, size, color }) => {
|
||||
const iconName = route.name === 'Local'
|
||||
? (focused ? 'download' : 'download-outline')
|
||||
: (focused ? 'cloud' : 'cloud-outline');
|
||||
return <MaterialCommunityIcons name={iconName} size={size} color={color} />
|
||||
})
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Tab.Screen name="Local" component={LocalComicList} />
|
||||
<Tab.Screen name="Remote" component={Remote} />
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
padding: {
|
||||
paddingEnd: 16,
|
||||
},
|
||||
});
|
48
src/pages/comiclist/Local.tsx
Normal file
48
src/pages/comiclist/Local.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { FC } from "react"
|
||||
import { ActivityIndicator, FlatList, StyleSheet, Text, View } from "react-native"
|
||||
import Touchable from "../../components/Touchable";
|
||||
import ComicCard from '../../components/ComicCard';
|
||||
import { ComicMetadata, useLocalComics } from "../../provider"
|
||||
import { CompositeNavigationProp, useNavigation } from "@react-navigation/native";
|
||||
import { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
|
||||
import { TabParamList } from "../comiclist";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { RootStackParamList } from "../../../App";
|
||||
import { LocalComicId } from "../../provider/local";
|
||||
|
||||
export type ComicListNavigationProp = CompositeNavigationProp<
|
||||
BottomTabNavigationProp<TabParamList, 'Local'>,
|
||||
NativeStackNavigationProp<RootStackParamList, 'Home'>
|
||||
>;
|
||||
|
||||
const Comic: FC<ComicMetadata & { onClick: () => void }> = (props) => {
|
||||
return <Touchable onPress={() => props.onClick()}>
|
||||
<View style={styles.comic}>
|
||||
<ComicCard comic={props} />
|
||||
</View>
|
||||
</Touchable>
|
||||
}
|
||||
|
||||
export default function LocalComicList() {
|
||||
const { data, error, isLoading } = useLocalComics();
|
||||
const navigation = useNavigation<ComicListNavigationProp>();
|
||||
|
||||
if (isLoading) return <ActivityIndicator />;
|
||||
if (error) return <Text>Failed to load comic: {error.toString()}</Text>;
|
||||
if (!data) return <Text>Whoops! We couldn't find that comic :/</Text>;
|
||||
|
||||
function openComic(comicId: LocalComicId) {
|
||||
navigation.navigate('ComicProfile', {
|
||||
provider: 'local',
|
||||
comicId,
|
||||
});
|
||||
}
|
||||
|
||||
return <FlatList data={data} renderItem={({ item }) => <Comic onClick={() => openComic(item.id)} {...item} />} keyExtractor={item => item.id} />
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
comic: {
|
||||
padding: 8,
|
||||
},
|
||||
});
|
|
@ -31,10 +31,17 @@ export default function Home(props: Props) {
|
|||
alert('library yeeted!');
|
||||
}} />
|
||||
</View>
|
||||
<View style={styles.button}>
|
||||
<Button title="New home" onPress={async () => {
|
||||
props.navigation.navigate('Home', {
|
||||
screen: 'Local',
|
||||
});
|
||||
}} />
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;
|
||||
type Props = NativeStackScreenProps<RootStackParamList, 'OldHome'>;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
|||
import { useQuery } from "react-query";
|
||||
import { RootStackParamList } from "../../App";
|
||||
import comicfuryProvider from "./comicfury";
|
||||
import localProvider from "./local";
|
||||
import localProvider, { LocalComicMetadata } from "./local";
|
||||
|
||||
export interface ComicProvider {
|
||||
getComicInfo(comicId: string): Promise<ComicMetadata | null>;
|
||||
|
@ -112,3 +112,10 @@ export function useComicPage(provider: ComicProviderKey, comicId: string, pageId
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useLocalComics() {
|
||||
return useQuery<LocalComicMetadata[], Error>(
|
||||
'localComics',
|
||||
() => COMIC_PROVIDERS['local'].listComics(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { ComicProvider, ComicProviderKey, COMIC_PROVIDERS, FullPage } from ".";
|
||||
import { ComicMetadata, ComicProvider, ComicProviderKey, COMIC_PROVIDERS, FullPage } from ".";
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
export type NonLocalProviderKey = Exclude<ComicProviderKey, 'local'>;
|
||||
type LocalComicId = `${NonLocalProviderKey}-${string}`;
|
||||
export type LocalComicId = `${NonLocalProviderKey}-${string}`;
|
||||
|
||||
export type LocalComicMetadata = ComicMetadata & { id: LocalComicId };
|
||||
|
||||
type LocalProvider = ComicProvider & {
|
||||
listComics(): Promise<LocalComicMetadata[]>;
|
||||
downloadComicData(providerKey: NonLocalProviderKey, comicId: string, statusCallback?: (message: string) => void): Promise<LocalComicId | null>;
|
||||
};
|
||||
|
||||
|
@ -19,12 +22,12 @@ async function ensureDataDirectory(path: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function parseComicId(comicId: string): [providerKey: string, comicId: string] | null {
|
||||
export function parseComicId(comicId: string): [providerKey: ComicProviderKey, comicId: string] | null {
|
||||
const pts = comicId.split('-', 2);
|
||||
if (pts.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
return pts as [string, string]; // typescript inference bad.
|
||||
return pts as [ComicProviderKey, string]; // typescript inference bad.
|
||||
}
|
||||
|
||||
async function pathExists(path: string): Promise<boolean> {
|
||||
|
@ -43,6 +46,26 @@ async function localJson<T>(comicId: string, path: string): Promise<T | null> {
|
|||
}
|
||||
|
||||
const localProvider: LocalProvider = {
|
||||
async listComics() {
|
||||
const directory = dataPath('comics/');
|
||||
if (!pathExists(directory)) return [];
|
||||
const providers = await FileSystem.readDirectoryAsync(directory) as NonLocalProviderKey[];
|
||||
const comics: LocalComicMetadata[] = [];
|
||||
for (const provider of providers) {
|
||||
const comicIds = await FileSystem.readDirectoryAsync(directory + '/' + provider);
|
||||
for (const comicId of comicIds) {
|
||||
const combinedId: LocalComicId = `${provider}-${comicId}`;
|
||||
const comic = await this.getComicInfo(combinedId);
|
||||
if (comic) {
|
||||
comics.push({
|
||||
id: combinedId,
|
||||
...comic,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return comics;
|
||||
},
|
||||
getComicInfo(comicId) {
|
||||
return localJson(comicId, 'comic.json');
|
||||
},
|
||||
|
|
33
yarn.lock
33
yarn.lock
|
@ -3721,7 +3721,7 @@ css-select@^2.0.0:
|
|||
domutils "^1.7.0"
|
||||
nth-check "^1.0.2"
|
||||
|
||||
css-select@^4.1.3:
|
||||
css-select@^4.1.3, css-select@^4.2.1:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
|
||||
integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==
|
||||
|
@ -3751,7 +3751,7 @@ css-tree@1.0.0-alpha.37:
|
|||
mdn-data "2.0.4"
|
||||
source-map "^0.6.1"
|
||||
|
||||
css-tree@^1.1.2:
|
||||
css-tree@^1.0.0-alpha.39, css-tree@^1.1.2:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
|
||||
integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
|
||||
|
@ -8328,7 +8328,7 @@ prompts@^2.3.2, prompts@^2.4.0:
|
|||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.7.2:
|
||||
prop-types@^15.7.2, prop-types@^15.8.0:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
|
@ -8592,6 +8592,11 @@ react-native-codegen@^0.69.2:
|
|||
jscodeshift "^0.13.1"
|
||||
nullthrows "^1.1.1"
|
||||
|
||||
react-native-fast-image@^8.6.1:
|
||||
version "8.6.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.1.tgz#6a3a11b8ebc7629919265d33a420a04d13c897e0"
|
||||
integrity sha512-ILuP7EuakqHNzTr8ZbumhuxG4tE/wGQ66z4nEEuzc0FlqY6rYaPsnq/UD2ahoyFj6QP1WvA2RIK3odib+VcqWg==
|
||||
|
||||
react-native-gesture-handler@~2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.5.0.tgz#61385583570ed0a45a9ed142425e35f8fe8274fb"
|
||||
|
@ -8608,11 +8613,25 @@ react-native-gradle-plugin@^0.0.7:
|
|||
resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.7.tgz#96602f909745239deab7b589443f14fce5da2056"
|
||||
integrity sha512-+4JpbIx42zGTONhBTIXSyfyHICHC29VTvhkkoUOJAh/XHPEixpuBduYgf6Y4y9wsN1ARlQhBBoptTvXvAFQf5g==
|
||||
|
||||
react-native-image-progress@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-image-progress/-/react-native-image-progress-1.2.0.tgz#81ff1b33e33c541764b9e4aebfa17b1c1a108d04"
|
||||
integrity sha512-RkQydqw3p0i30vHRFFhuGNT/BXWUDJtBR5/hxJbQsfI/Ziipjm5M64bpw3VQqxUcZC8HLLg+ZQtBq1mtUlWjww==
|
||||
dependencies:
|
||||
prop-types "^15.8.0"
|
||||
|
||||
react-native-modalize@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-modalize/-/react-native-modalize-2.1.1.tgz#dcb67d208f4c5bdc76f45075e727be43fd008561"
|
||||
integrity sha512-4/7EZWsrUqAAkkAVEnOsSdpAPQaEBewX7TvwFuzgvGDzxKpq3O58I9SnSeU8QtG/r91XYHJNaU5dAuDrcLjUaQ==
|
||||
|
||||
react-native-progress@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-progress/-/react-native-progress-5.0.0.tgz#f5ac6ceaeee27f184c660b00f29419e82a9d0ab0"
|
||||
integrity sha512-KjnGIt3r9i5Kn2biOD9fXLJocf0bwxPRxOyAgXEnZTJQU2O+HyzgGFRCbM5h3izm9kKIkSc1txh8aGmMafCD9A==
|
||||
dependencies:
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-native-safe-area-context@4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.3.1.tgz#5cf97b25b395e0d09bc1f828920cd7da0d792ade"
|
||||
|
@ -8626,6 +8645,14 @@ react-native-screens@~3.15.0:
|
|||
react-freeze "^1.0.0"
|
||||
warn-once "^0.1.0"
|
||||
|
||||
react-native-svg@12.3.0:
|
||||
version "12.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.3.0.tgz#40f657c5d1ee366df23f3ec8dae76fd276b86248"
|
||||
integrity sha512-ESG1g1j7/WLD7X3XRFTQHVv0r6DpbHNNcdusngAODIxG88wpTWUZkhcM3A2HJTb+BbXTFDamHv7FwtRKWQ/ALg==
|
||||
dependencies:
|
||||
css-select "^4.2.1"
|
||||
css-tree "^1.0.0-alpha.39"
|
||||
|
||||
react-native-web@~0.18.7:
|
||||
version "0.18.8"
|
||||
resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.18.8.tgz#6a761cc5531dfbb8fffa9c1e342f153a792dce07"
|
||||
|
|
Loading…
Reference in a new issue