Archived
184 lines
4.1 KiB
TypeScript
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",
|
|
},
|
|
});
|