BloodyInside's picture
firsty
947c08e
raw
history blame
26.9 kB
import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react';
import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router';
import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper';
import CircularProgress from 'react-native-circular-progress-indicator';
import { ActivityIndicator } from 'react-native-paper';
import { FlashList } from "@shopify/flash-list";
import uuid from 'react-native-uuid';
import Toast from 'react-native-toast-message';
import { View, AnimatePresence } from 'moti';
import * as Clipboard from 'expo-clipboard';
import * as FileSystem from 'expo-file-system';
import NetInfo from "@react-native-community/netinfo";
import { Marquee } from '@animatereactnative/marquee';
import { Slider } from '@rneui/themed-edge';
import Storage from '@/constants/module/storage';
import ChapterStorage from '@/constants/module/chapter_storage';
import Image from '@/components/Image';
import {CONTEXT} from '@/constants/module/context';
import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager";
import Theme from '@/constants/theme';
import ChapterImage from '../../components/chapter_image';
import Menu from '../../components/menu/menu';
import Disqus from '../../components/disqus';
import { get_chapter } from '../../modules/get_chapter';
import ComicStorage from '@/constants/module/comic_storage';
const Index = ({}:any) => {
const SOURCE = useLocalSearchParams().source;
const COMIC_ID = useLocalSearchParams().comic_id;
const Dimensions = useWindowDimensions();
const StaticDimensions = useMemo(() => Dimensions, [])
const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
const [firstLoading, setFirstLoading]:any = useState(true)
const [chapterInfo, setChapterInfo]:any = useState({})
const [showOptions, setShowOptions]:any = useState({type:"general",state:false})
const [imageKeys, setImageKeys]:any = useState([])
const [isLoading, setIsLoading]:any = useState("")
const [addChapter, setAddChapter]:any = useState(false)
const [zoom, setZoom]:any = useState(0)
const is_adding:any = useRef({state:false,interval:null})
const temp_loading:any = useRef(false)
const temp_image_keys:any = useRef({})
const images:any = useRef({})
const CHAPTER_IDX = useRef(Number(useLocalSearchParams().idx as string));
useEffect(()=>{
setShowMenuContext(false)
},[])
useEffect(()=>{(async () => {
temp_loading.current = true
const stored_chapter = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,CHAPTER_IDX.current, {exclude_fields:["data"]})
setChapterInfo({
chapter_id: stored_chapter?.id,
chapter_idx: stored_chapter?.id,
title: stored_chapter?.title,
})
const stored_chapter_zoom = await Storage.get("CHAPTER_ZOOM") || 0
setZoom(stored_chapter_zoom)
const chapter_current = await get_chapter(SOURCE,COMIC_ID,CHAPTER_IDX.current)
if (chapter_current){
images.current = {...images.current,...chapter_current?.files}
setImageKeys(chapter_current?.file_keys)
setFirstLoading(false)
}
const chapter_next = await get_chapter(SOURCE,COMIC_ID,CHAPTER_IDX.current+1)
if (chapter_next){
temp_image_keys.current[`${CHAPTER_IDX.current+1}`] = chapter_next?.file_keys
images.current = {...images.current,...chapter_next?.files}
}
temp_loading.current = false
})()},[])
useFocusEffect(useCallback(() => {
return () => {
setImageKeys([])
images.current = {}
clearInterval(is_adding.current.interval)
}
},[]))
useEffect(()=>{
if (!addChapter) return
if (is_adding.current.state) return
is_adding.current.state = true
setIsLoading(true)
clearInterval(is_adding.current.interval)
is_adding.current.interval = setInterval(()=>{
if (!temp_loading.current) {
clearInterval(is_adding.current.interval);
temp_loading.current = true;
(async ()=>{
const new_chapter_idx = CHAPTER_IDX.current+1
if (imageKeys.slice(-1)[0]?.type !== "no-chapter-banner"){
const next_stored_chapter = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,new_chapter_idx, {exclude_fields:["data"]})
if (next_stored_chapter.data_state === "completed"){
if (temp_image_keys.current.hasOwnProperty(new_chapter_idx)){
delete temp_image_keys.current[new_chapter_idx-2]
const pre_image_keys = imageKeys.filter((data:any) => {
if (data.type === "image"){
const idx = Number(data.value.split("-")[0])
if (idx < new_chapter_idx-2) return false
else return true
}else if (data.type === "chapter-info-banner"){
if (data.idx < new_chapter_idx-2) return false
}else return true
})
setImageKeys([...pre_image_keys,{type:"chapter-info-banner", idx:CHAPTER_IDX.current, value:{last:chapterInfo.title, next:next_stored_chapter?.title}},...temp_image_keys.current[new_chapter_idx]])
setIsLoading(false)
setAddChapter(false)
is_adding.current.state = false
for (const item of Object.keys(images.current)){
if (Number(item.split("-")[0]) < new_chapter_idx-2) delete images.current[item]
}
const chapter_next = await get_chapter(SOURCE,COMIC_ID,new_chapter_idx + 1)
if (chapter_next){
temp_image_keys.current[`${new_chapter_idx + 1}`] = chapter_next?.file_keys
images.current = {...images.current,...chapter_next?.files}
}else{
temp_image_keys.current[`${new_chapter_idx + 1}`] = [{type:"no-chapter-banner"}]
}
}else setImageKeys([...imageKeys,{type:"no-chapter-banner"}])
}else setImageKeys([...imageKeys,{type:"no-chapter-banner"}])
}
setAddChapter(false)
setIsLoading(false)
temp_loading.current = false
is_adding.current.state = false
})()
}
},100)
},[imageKeys, addChapter])
return (<>{!firstLoading
? <>
<View
style={{
width:"100%",
height:"100%",
backgroundColor:Theme[themeTypeContext].background_color,
zIndex:0
}}
>
<FlashList
data={imageKeys}
renderItem={({item,index}:any) => {
if (item.type === "image"){
item.image_data = images.current[item.value].data
item.layout = images.current[item.value].layout
}
return <ChapterImage key={index} item={item} zoom={zoom} showOptions={showOptions} setShowOptions={setShowOptions}/>
}}
estimatedItemSize={StaticDimensions.height}
extraData={{zoom:zoom,showOptions:showOptions,setShowOptions:setShowOptions}}
onEndReachedThreshold={0.5}
onEndReached={()=>{
setAddChapter(true)
}}
onViewableItemsChanged={async ({ viewableItems, changed }) => {
const expect_chapter_idx = [CHAPTER_IDX.current + 1, CHAPTER_IDX.current - 1]
const current_count = viewableItems.filter((data:any) => data.item.idx === CHAPTER_IDX.current).length
const existed_count = viewableItems.filter((data:any) => expect_chapter_idx.includes(data.item.idx)).length
if (current_count || existed_count){
const choose_idx = current_count > existed_count ? CHAPTER_IDX.current : viewableItems.find((data:any) => expect_chapter_idx.includes(data.item.idx))?.item.idx
if (choose_idx === CHAPTER_IDX.current) return
const stored_chapter = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,choose_idx, {exclude_fields:["data"]})
setChapterInfo({
chapter_id: stored_chapter?.id,
chapter_idx: stored_chapter?.id,
title: stored_chapter?.title,
})
const stored_comic = await ComicStorage.getByID(SOURCE, COMIC_ID)
if (stored_comic.history.idx && choose_idx > stored_comic.history.idx) {
await ComicStorage.updateHistory(SOURCE, COMIC_ID, {idx:stored_chapter?.idx,id:stored_chapter?.id,title:stored_chapter?.title})
}
router.setParams({idx:choose_idx})
CHAPTER_IDX.current = choose_idx
}
}}
/>
{isLoading && (
<View
style={{
display:"flex",
width:"100%",
height:"auto",
justifyContent:"center",
padding:16,
}}
>
<ActivityIndicator animating={true} size={(0.03 * ((Dimensions.width+Dimensions.height)/2)) * (1 - zoom/100)}/>
</View>
)}
</View>
<AnimatePresence exitBeforeEnter>
{showOptions.state &&
<View
style={{
position:"absolute",
width:"100%",
height:"100%",
display:"flex",
flexDirection:"column",
zIndex:1,
}}
>
<>{showOptions.type === "general" && <>
<Pressable
onPress={()=>{setShowOptions({...showOptions,state:!showOptions.state})}}
style={{flex:1,backgroundColor:"rgba(0,0,0,0.5)",pointerEvents:"auto"}}
>
<View
id='reading-options'
style={{
flex:1,
display:"flex",
flexDirection:"column",
alignItems:"flex-start",
backgroundColor:"transparent",
pointerEvents:"auto",
}}
from={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
}}
transition={{
type: 'timing',
duration: 500,
}}
exitTransition={{
type: 'timing',
duration: 250,
}}
>
<View
style={{
width:"100%",
height:"auto",
display:"flex",
flexDirection:"row",
justifyContent:"space-between",
alignItems:"center",
padding:16,
gap:8,
backgroundColor:Theme[themeTypeContext].background_color,
borderBottomWidth:2,
borderColor:Theme[themeTypeContext].border_color,
pointerEvents:"auto",
}}
>
<TouchableRipple
rippleColor={Theme[themeTypeContext].ripple_color_outlined}
style={{
borderRadius:5,
borderWidth:0,
backgroundColor: "transparent",
padding:5,
}}
onPress={()=>{
router.replace(`/view/${SOURCE}/${COMIC_ID}/`)
}}
>
<Icon source={"arrow-left-thin"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
</TouchableRipple>
<View
style={{
flex:1,
}}
>
<Text selectable={false}
numberOfLines={1}
style={{
color:Theme[themeTypeContext].text_color,
fontSize:((Dimensions.width+Dimensions.height)/2)*0.045,
fontFamily:"roboto-bold",
}}
>
{chapterInfo.title}
</Text>
</View>
<TouchableRipple
rippleColor={Theme[themeTypeContext].ripple_color_outlined}
style={{
borderRadius:5,
borderWidth:0,
backgroundColor: "transparent",
padding:5,
}}
onPress={()=>{
console.log("HO2h2")
}}
>
<Icon source={"cloud-refresh"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
</TouchableRipple>
</View>
<View
style={{
display:"flex",
flexDirection:"column",
width:"100%",
flex:1,
padding:16,
alignItems:"flex-end",
gap:12,
justifyContent:"center",
}}
>
<View style={{width:"auto",height:"auto",display:"flex",flexDirection:"column",alignItems:"center",gap:8, pointerEvents:"auto"}}>
<View style={{
width:"auto",
height:"auto",
backgroundColor: 'rgba(0, 0, 0, 0.5)',
paddingHorizontal:12,
paddingVertical:12,
borderRadius:12,
}}>
<Slider
style={{
width: ((Dimensions.width+Dimensions.height)/2)*0.06,
height: Dimensions.height*0.525,
pointerEvents:"auto",
}}
value={zoom}
onValueChange={async (value)=>{
await Storage.store("CHAPTER_ZOOM", value)
setZoom(value)
}}
maximumValue={80}
minimumValue={-80}
step={1}
orientation="vertical"
minimumTrackTintColor={Theme[themeTypeContext].text_color}
maximumTrackTintColor={Theme[themeTypeContext].border_color}
thumbStyle={{
height: "auto",
width: "auto",
backgroundColor: Theme[themeTypeContext].background_color,
padding:5,
}}
thumbProps={{children: (<>
<>{zoom < 0
&& <Icon source={"magnify-plus-outline"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
}</>
<>{zoom === 0
&& <Icon source={"magnify"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
}</>
<>{zoom > 0
&& <Icon source={"magnify-minus-outline"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
}</>
</>)}}
/>
</View>
<TouchableRipple
rippleColor={Theme[themeTypeContext].ripple_color_outlined}
style={{
borderRadius:5,
borderWidth:0,
backgroundColor: "transparent",
padding:5,
pointerEvents:"auto",
}}
onPress={async ()=>{
await Storage.store("CHAPTER_ZOOM", 0)
setZoom(0)
}}
>
<Icon source={"magnify-remove-cursor"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
</TouchableRipple>
</View>
</View>
</View>
</Pressable>
</>}</>
<>{showOptions.type === "comments"&& <>
<View
style={{
width:"100%",
height:"auto",
padding:16,
backgroundColor:Theme[themeTypeContext].background_color,
borderBottomWidth:2,
borderColor:Theme[themeTypeContext].border_color,
}}
>
<Text selectable={false}
numberOfLines={1}
style={{
color:Theme[themeTypeContext].text_color,
fontSize:((Dimensions.width+Dimensions.height)/2)*0.045,
fontFamily:"roboto-bold",
}}
>
{chapterInfo.title}
</Text>
</View>
<View
style={{
flex:1,
backgroundColor:Theme[themeTypeContext].background_color,
}}
>
<Disqus title={chapterInfo.title} identifier={`${SOURCE}-${COMIC_ID}-${CHAPTER_IDX.current}`} url={`${apiBaseContext}/read/${SOURCE}/${COMIC_ID}/`}
paddingVertical={16} paddingHorizontal={25}
/>
</View>
</>}</>
<Menu showOptions={showOptions} setShowOptions={setShowOptions}/>
</View>
}
</AnimatePresence>
</>
:<View style={{zIndex:5,width:"100%",height:"100%",display:"flex",justifyContent:"center",alignItems:"center",backgroundColor:Theme[themeTypeContext].background_color}}>
<Image setShowCloudflareTurnstile={setShowCloudflareTurnstileContext} source={require("@/assets/gif/cat-loading.gif")} style={{width:((Dimensions.width+Dimensions.height)/2)*0.15,height:((Dimensions.width+Dimensions.height)/2)*0.15}}/>
</View>
}</>)
}
export default Index;