Skip to content

Commit

Permalink
馃嵒 POC EpubJS RN reader (#289)
Browse files Browse the repository at this point in the history
Reorganized a bit of the reader screen to account for future readers a bit better, and added a really bare-bones EPUB reader. This frankly will not hold up in time, IMO, and I'll likely either have to move off of epubjs in favor of an alternative that works well with RN or just build something in house.
  • Loading branch information
aaronleopold committed Mar 10, 2024
1 parent 1d30084 commit aec4118
Show file tree
Hide file tree
Showing 23 changed files with 433 additions and 82 deletions.
6 changes: 5 additions & 1 deletion apps/expo/package.json
Expand Up @@ -7,6 +7,8 @@
"web": "expo start --web"
},
"dependencies": {
"@epubjs-react-native/core": "^1.3.0",
"@epubjs-react-native/expo-file-system": "^1.0.0",
"@hookform/resolvers": "^3.3.2",
"@react-native-async-storage/async-storage": "1.21.0",
"@react-navigation/bottom-tabs": "^6.5.12",
Expand All @@ -27,12 +29,14 @@
"react-dom": "18.2.0",
"react-hook-form": "^7.50.1",
"react-native": "0.73.4",
"react-native-gesture-handler": "~2.14.1",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.14.0",
"react-native-reanimated": "~3.6.2",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-svg": "14.1.0",
"react-native-web": "~0.19.10",
"react-native-webview": "13.6.4",
"tailwind-merge": "^1.14.0",
"zod": "^3.22.4",
"zustand": "^4.5.0"
Expand Down
3 changes: 2 additions & 1 deletion apps/expo/src/App.tsx
Expand Up @@ -54,8 +54,9 @@ export default function AppWrapper() {

// TODO: remove, just debugging stuff
useEffect(() => {
setBaseUrl('https://demo.stumpapp.dev')
// setBaseUrl('https://demo.stumpapp.dev')
// setBaseUrl('http://localhost:10801')
setBaseUrl('http://192.168.0.202:10801')
}, [setBaseUrl])

useEffect(() => {
Expand Down
11 changes: 11 additions & 0 deletions apps/expo/src/components/reader/UnsupportedReader.tsx
@@ -0,0 +1,11 @@
import React from 'react'

import { ScreenRootView, Text } from '../primitives'

export default function UnsupportedReader() {
return (
<ScreenRootView>
<Text>The book reader for this format is not supported yet. Check back later!</Text>
</ScreenRootView>
)
}
27 changes: 27 additions & 0 deletions apps/expo/src/components/reader/epub/EpubJSFooter.tsx
@@ -0,0 +1,27 @@
import { useReader } from '@epubjs-react-native/core'
import React from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'

import { Text, View } from '@/components/primitives'

export const FOOTER_HEIGHT = 24

export default function EpubJSFooter() {
const { currentLocation } = useReader()

const { bottom } = useSafeAreaInsets()

const currentPage = currentLocation?.start?.displayed.page || 1
const totalPages = currentLocation?.end?.displayed.page || 1

return (
<View
className="w-full shrink-0 items-start justify-start px-4"
style={{ height: FOOTER_HEIGHT + bottom }}
>
<Text>
{currentPage}/{totalPages}
</Text>
</View>
)
}
125 changes: 125 additions & 0 deletions apps/expo/src/components/reader/epub/EpubJSReader.tsx
@@ -0,0 +1,125 @@
import { Location, Reader } from '@epubjs-react-native/core'
import { useFileSystem } from '@epubjs-react-native/expo-file-system'
import { API, isAxiosError, updateEpubProgress } from '@stump/api'
import { Media } from '@stump/types'
import { useColorScheme } from 'nativewind'
import React, { useCallback, useEffect, useState } from 'react'
import { useWindowDimensions } from 'react-native'

import EpubJSReaderContainer from './EpubJSReaderContainer'

type Props = {
/**
* The media which is being read
*/
book: Media
/**
* The initial CFI to start the reader on
*/
initialCfi?: string
/**
* Whether the reader should be in incognito mode
*/
incognito?: boolean
}

/**
* A reader for books that are EPUBs, using EpubJS as the reader
*
* TODO: create a custom reader component, this is a HUGE effort but will pay off in
* the long run
*/
export default function EpubJSReader({ book, initialCfi, incognito }: Props) {
/**
* The base64 representation of the book file. The reader component does not accept
* credentials in the fetch, so we just have to fetch manually and pass the base64
* representation to the reader as the source.
*/
const [base64, setBase64] = useState<string | null>(null)

const { width, height } = useWindowDimensions()
const { colorScheme } = useColorScheme()

/**
* An effect that fetches the book file and loads it into the reader component
* as a base64 string
*/
useEffect(() => {
async function fetchBook() {
try {
const response = await fetch(`${API.getUri()}/media/${book.id}/file`)
const data = await response.blob()
const reader = new FileReader()
reader.onloadend = () => {
const result = reader.result as string
// Note: uncomment this line to show an infinite loader...
// setBase64(result)
const adjustedResult = result.split(',')[1] || result
setBase64(adjustedResult)
}
reader.readAsDataURL(data)
} catch (e) {
console.error(e)
}
}

fetchBook()
}, [book.id])

/**
* A callback that updates the read progress of the current location
*
* If the reader is in incognito mode, this will do nothing.
*/
const handleLocationChanged = useCallback(
async (_: number, currentLocation: Location, progress: number) => {
if (!incognito) {
const {
start: { cfi },
} = currentLocation

try {
await updateEpubProgress({
epubcfi: cfi,
id: book.id,
is_complete: progress >= 1.0,
percentage: progress,
})
} catch (e) {
console.error(e)
if (isAxiosError(e)) {
console.error(e.response?.data)
}
}
}
},
[incognito, book.id],
)

if (!base64) {
return null
}

return (
<EpubJSReaderContainer>
<Reader
src={base64}
onDisplayError={(error) => console.error(error)}
width={width}
// height={height - height * 0.08}
height={height}
fileSystem={useFileSystem}
initialLocation={initialCfi}
onLocationChange={handleLocationChanged}
// renderLoadingFileComponent={LoadingSpinner}
defaultTheme={
colorScheme === 'dark'
? {
body: { background: '#0F1011 !important', color: '#E8EDF4' },
}
: { body: { color: 'black' } }
}
/>
</EpubJSReaderContainer>
)
}
20 changes: 20 additions & 0 deletions apps/expo/src/components/reader/epub/EpubJSReaderContainer.tsx
@@ -0,0 +1,20 @@
import { ReaderProvider } from '@epubjs-react-native/core'
import React from 'react'

import { ScreenRootView, View } from '@/components/primitives'

type Props = {
children: React.ReactNode
}

// total ass, I hate epubjs lol maybe im just dumb? I cannot get the reader to listen to the height
export default function EpubJSReaderContainer({ children }: Props) {
return (
<ScreenRootView classes="flex-none bg-white dark:bg-gray-950">
<ReaderProvider>
<View className="flex-none shrink dark:bg-gray-950">{children}</View>
{/* <EpubJSFooter /> */}
</ReaderProvider>
</ScreenRootView>
)
}
39 changes: 39 additions & 0 deletions apps/expo/src/components/reader/epub/LoadingSpinner.tsx
@@ -0,0 +1,39 @@
import { LoadingFileProps } from '@epubjs-react-native/core'
import React, { useEffect, useState } from 'react'

import { Text, View } from '@/components/primitives'

// FIXME: This causes an error...
export default function LoadingSpinner({
// downloadProgress,
// downloadError,
downloadSuccess,
}: LoadingFileProps) {
// Setup a timeout that will check if we are stuck loading, abougt 10 seconds
const [didTimeout, setDidTimeout] = useState(false)

// If we are still loading after 10 seconds, we are stuck
useEffect(() => {
const timeout = setTimeout(() => {
setDidTimeout(true)
}, 10000)

return () => clearTimeout(timeout)
}, [])

if (didTimeout && !downloadSuccess) {
return (
<View>
<Text>It looks like we are stuck loading the book. Check your server logs</Text>
</View>
)
} else if (!downloadSuccess) {
return (
<View>
<Text>Loading...</Text>
</View>
)
} else {
return null
}
}
1 change: 1 addition & 0 deletions apps/expo/src/components/reader/epub/index.ts
@@ -0,0 +1 @@
export { default as EpubJSReader } from './EpubJSReader'

0 comments on commit aec4118

Please sign in to comment.