feat: implement mobile reader

- implement mobile epub reader
This commit is contained in:
2026-06-20 13:33:44 -05:00
parent 84c0497f21
commit 22ded87250
41 changed files with 3184 additions and 1111 deletions
+54 -52
View File
@@ -2,34 +2,31 @@ import { useState, useEffect, useCallback, type ReactNode } from "react";
import {
View,
Text,
Image,
FlatList,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
RefreshControl,
} from "react-native";
import { fetchBooks } from "../api/books";
import type { Book, PaginatedResponse } from "@cloud-reader/shared";
import { ebooksApi } from "../api/ebooks";
import type { EBookListItem } from "@cloud-reader/shared";
export default function LibraryScreen({ navigation }: { navigation: any }): ReactNode {
const [books, setBooks] = useState<Book[]>([]);
export default function LibraryScreen({
navigation,
}: {
navigation: any;
}): ReactNode {
const [books, setBooks] = useState<EBookListItem[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const loadBooks = useCallback(async (pageNum: number, isRefresh = false) => {
const loadBooks = useCallback(async () => {
try {
const data: PaginatedResponse<Book> = await fetchBooks(pageNum);
if (isRefresh) {
setBooks(data.results);
} else {
setBooks((prev) => [...prev, ...data.results]);
}
setHasMore(data.next !== null);
setPage(pageNum);
const { data } = await ebooksApi.list();
setBooks(data.results);
} catch {
// Silent error for now
// Silent error for now; pull-to-refresh lets the user retry.
} finally {
setLoading(false);
setRefreshing(false);
@@ -37,44 +34,49 @@ export default function LibraryScreen({ navigation }: { navigation: any }): Reac
}, []);
useEffect(() => {
loadBooks(1, true);
loadBooks();
}, [loadBooks]);
const onRefresh = () => {
setRefreshing(true);
loadBooks(1, true);
loadBooks();
};
const loadMore = () => {
if (hasMore && !loading) {
loadBooks(page + 1);
}
const renderBook = ({ item }: { item: EBookListItem }) => {
const progress =
typeof item.progress === "number"
? Math.round(item.progress)
: null;
return (
<TouchableOpacity
style={styles.bookCard}
onPress={() => navigation.navigate("BookDetail", { ebookId: item.id })}
>
{item.cover_image ? (
<Image source={{ uri: item.cover_image }} style={styles.bookCover} />
) : (
<View style={styles.bookCover}>
<Text style={styles.coverText}>
{item.title.charAt(0).toUpperCase()}
</Text>
</View>
)}
<View style={styles.bookInfo}>
<Text style={styles.bookTitle} numberOfLines={2}>
{item.title}
</Text>
<Text style={styles.bookAuthor} numberOfLines={1}>
{item.author || "Unknown author"}
</Text>
<Text style={styles.bookMeta}>
{item.format?.toUpperCase()}
{progress !== null ? ` · ${progress}% read` : ""}
</Text>
</View>
</TouchableOpacity>
);
};
const renderBook = ({ item }: { item: Book }) => (
<TouchableOpacity
style={styles.bookCard}
onPress={() =>
navigation.navigate("BookDetail", { bookId: item.id })
}
>
<View style={styles.bookCover}>
<Text style={styles.coverText}>
{item.title.charAt(0).toUpperCase()}
</Text>
</View>
<View style={styles.bookInfo}>
<Text style={styles.bookTitle} numberOfLines={1}>
{item.title}
</Text>
<Text style={styles.bookAuthor} numberOfLines={1}>
{item.author}
</Text>
<Text style={styles.bookPages}>{item.total_pages} pages</Text>
</View>
</TouchableOpacity>
);
if (loading && books.length === 0) {
return (
<View style={styles.centered}>
@@ -88,9 +90,7 @@ export default function LibraryScreen({ navigation }: { navigation: any }): Reac
<FlatList
data={books}
renderItem={renderBook}
keyExtractor={(item) => item.id}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
keyExtractor={(item) => String(item.id)}
contentContainerStyle={styles.list}
refreshControl={
<RefreshControl
@@ -103,7 +103,7 @@ export default function LibraryScreen({ navigation }: { navigation: any }): Reac
<View style={styles.centered}>
<Text style={styles.emptyText}>Your library is empty</Text>
<Text style={styles.emptySubtext}>
Add books to get started
Upload books from the web app to get started
</Text>
</View>
}
@@ -125,6 +125,7 @@ const styles = StyleSheet.create({
},
list: {
padding: 16,
flexGrow: 1,
},
bookCard: {
flexDirection: "row",
@@ -164,7 +165,7 @@ const styles = StyleSheet.create({
color: "#888",
marginBottom: 4,
},
bookPages: {
bookMeta: {
fontSize: 12,
color: "#666",
},
@@ -177,5 +178,6 @@ const styles = StyleSheet.create({
emptySubtext: {
fontSize: 14,
color: "#666",
textAlign: "center",
},
});
});