This repository has been archived on 2026-07-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cloud-reader/mobile/src/screens/LibraryScreen.tsx
T
crisleo94 22ded87250 feat: implement mobile reader
- implement mobile epub reader
2026-06-20 13:33:44 -05:00

184 lines
4.1 KiB
TypeScript

import { useState, useEffect, useCallback, type ReactNode } from "react";
import {
View,
Text,
Image,
FlatList,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
RefreshControl,
} from "react-native";
import { ebooksApi } from "../api/ebooks";
import type { EBookListItem } from "@cloud-reader/shared";
export default function LibraryScreen({
navigation,
}: {
navigation: any;
}): ReactNode {
const [books, setBooks] = useState<EBookListItem[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const loadBooks = useCallback(async () => {
try {
const { data } = await ebooksApi.list();
setBooks(data.results);
} catch {
// Silent error for now; pull-to-refresh lets the user retry.
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
useEffect(() => {
loadBooks();
}, [loadBooks]);
const onRefresh = () => {
setRefreshing(true);
loadBooks();
};
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>
);
};
if (loading && books.length === 0) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#4f8ef7" />
</View>
);
}
return (
<View style={styles.container}>
<FlatList
data={books}
renderItem={renderBook}
keyExtractor={(item) => String(item.id)}
contentContainerStyle={styles.list}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#4f8ef7"
/>
}
ListEmptyComponent={
<View style={styles.centered}>
<Text style={styles.emptyText}>Your library is empty</Text>
<Text style={styles.emptySubtext}>
Upload books from the web app to get started
</Text>
</View>
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0f0f23",
},
centered: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 24,
},
list: {
padding: 16,
flexGrow: 1,
},
bookCard: {
flexDirection: "row",
backgroundColor: "#1a1a2e",
borderRadius: 12,
padding: 12,
marginBottom: 12,
borderWidth: 1,
borderColor: "#333",
},
bookCover: {
width: 60,
height: 80,
backgroundColor: "#2a2a4e",
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
},
coverText: {
fontSize: 24,
fontWeight: "bold",
color: "#4f8ef7",
},
bookInfo: {
flex: 1,
marginLeft: 12,
justifyContent: "center",
},
bookTitle: {
fontSize: 16,
fontWeight: "600",
color: "#fff",
marginBottom: 4,
},
bookAuthor: {
fontSize: 14,
color: "#888",
marginBottom: 4,
},
bookMeta: {
fontSize: 12,
color: "#666",
},
emptyText: {
fontSize: 18,
fontWeight: "600",
color: "#888",
marginBottom: 8,
},
emptySubtext: {
fontSize: 14,
color: "#666",
textAlign: "center",
},
});