Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions apps/desktop/src/pages/Admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ import { Routes, Route, Link, useLocation } from 'react-router-dom';
import { getApiClient, User, EbookSource, Book } from '@bookdock/api-client';
import { getCoverImageUrl } from '../utils/network';
import { Button, Card, CardHeader, CardTitle, CardContent, Input } from '@bookdock/ui';

function BookCover({ book, className = "" }: { book: Book; className?: string }) {
const [coverError, setCoverError] = useState(false);
const coverSrc = getCoverImageUrl(book.coverUrl);
return (
<div className={`bg-gray-100 dark:bg-gray-700 rounded overflow-hidden flex-shrink-0 ${className}`}>
{coverSrc && !coverError ? (
<img
src={coverSrc}
alt={book.title}
className="w-full h-full object-cover"
onError={() => setCoverError(true)}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-400 to-purple-500">
<span className="text-white font-bold">{book.title.charAt(0)}</span>
</div>
)}
</div>
);
}
import {
BookOpen,
FileText,
Expand Down Expand Up @@ -786,20 +807,7 @@ function BooksManagement() {
<tr key={book.id} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<div className="w-10 h-14 bg-gray-100 dark:bg-gray-700 rounded overflow-hidden flex-shrink-0">
{book.coverUrl ? (
<img
src={getCoverImageUrl(book.coverUrl)}
alt={book.title}
className="w-full h-full object-cover"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-400 to-purple-500">
<span className="text-white font-bold">{book.title.charAt(0)}</span>
</div>
)}
</div>
<BookCover book={book} className="w-10 h-14" />
<span className="font-medium text-gray-900 dark:text-white truncate max-w-[200px]">{book.title}</span>
</div>
</td>
Expand Down
41 changes: 24 additions & 17 deletions apps/desktop/src/pages/AuthorDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ import { getApiClient, type Book, type Author } from "@bookdock/api-client";
import { getCoverImageUrl } from "../utils/network";
import { ArrowLeft, BookOpen } from "lucide-react";

function BookCover({ book, className = "" }: { book: Book; className?: string }) {
const [coverError, setCoverError] = useState(false);
const coverSrc = getCoverImageUrl(book.coverUrl);
return (
<div className={`aspect-[2/3] bg-gray-100 dark:bg-gray-700 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow ${className}`}>
{coverSrc && !coverError ? (
<img
src={coverSrc}
alt={book.title}
className="w-full h-full object-cover"
onError={() => setCoverError(true)}
/>
) : (
<div className={`w-full h-full flex items-center justify-center bg-gradient-to-br ${getBookGradient(book.title)}`}>
<span className="text-5xl text-white font-bold">
{book.title.charAt(0)}
</span>
</div>
)}
</div>
);
}

function getBookGradient(title: string): string {
const gradients = [
"from-blue-400 to-purple-500",
Expand Down Expand Up @@ -146,23 +169,7 @@ export default function AuthorDetail() {
className="w-36 cursor-pointer"
onClick={() => handleBookSelect(book)}
>
<div className="aspect-[2/3] bg-gray-100 dark:bg-gray-700 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow">
{book.coverUrl ? (
<img
src={getCoverImageUrl(book.coverUrl)}
alt={book.title}
className="w-full h-full object-cover"
/>
) : (
<div
className={`w-full h-full flex items-center justify-center bg-gradient-to-br ${getBookGradient(book.title)}`}
>
<span className="text-5xl text-white font-bold">
{book.title.charAt(0)}
</span>
</div>
)}
</div>
<BookCover book={book} />
<p className="mt-2 text-sm font-medium text-gray-900 dark:text-white truncate">
{book.title}
</p>
Expand Down
41 changes: 24 additions & 17 deletions apps/desktop/src/pages/BookDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,29 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { getCoverImageUrl } from "../utils/network";

function BookCover({ book, className = "" }: { book: Book; className?: string }) {
const [coverError, setCoverError] = useState(false);
const coverSrc = getCoverImageUrl(book.coverUrl);
return (
<div className={`aspect-[2/3] rounded-xl overflow-hidden shadow-lg ${className}`}>
{coverSrc && !coverError ? (
<img
src={coverSrc}
alt={book.title}
className="w-full h-full object-cover"
onError={() => setCoverError(true)}
/>
) : (
<div className={`w-full h-full flex items-center justify-center bg-gradient-to-br ${getBookGradient(book.title)}`}>
<span className="text-6xl text-white font-bold">
{book.title.charAt(0)}
</span>
</div>
)}
</div>
);
}

function getBookGradient(title: string): string {
const gradients = [
"from-blue-400 to-purple-500",
Expand Down Expand Up @@ -314,23 +337,7 @@ export default function BookDetail() {
<div className="flex flex-col sm:flex-row items-start gap-4 sm:gap-6 md:gap-8 mb-8">
{/* 封面 */}
<div className="w-32 sm:w-44 md:w-52 flex-shrink-0 self-center sm:self-start">
<div className="aspect-[2/3] rounded-xl overflow-hidden shadow-lg">
{book.coverUrl ? (
<img
src={getCoverImageUrl(book.coverUrl)}
alt={book.title}
className="w-full h-full object-cover"
/>
) : (
<div
className={`w-full h-full flex items-center justify-center bg-gradient-to-br ${getBookGradient(book.title)}`}
>
<span className="text-6xl text-white font-bold">
{book.title.charAt(0)}
</span>
</div>
)}
</div>
<BookCover book={book} />
</div>

{/* 信息 */}
Expand Down
42 changes: 25 additions & 17 deletions apps/desktop/src/pages/CollectionDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ import { getApiClient, Book, Collection } from "@bookdock/api-client";
import { getCoverImageUrl } from "../utils/network";
import { ArrowLeft, X, Trash2 } from "lucide-react";

function BookCover({ book, className = "", onClick }: { book: Book; className?: string; onClick?: () => void }) {
const [coverError, setCoverError] = useState(false);
const coverSrc = getCoverImageUrl(book.coverUrl);
return (
<div className={`rounded overflow-hidden flex-shrink-0 ${className}`} onClick={onClick}>
{coverSrc && !coverError ? (
<img
src={coverSrc}
alt={book.title}
className="w-full h-full object-cover"
onError={() => setCoverError(true)}
/>
) : (
<div className={`w-full h-full flex items-center justify-center bg-gradient-to-br ${getBookGradient(book.title)}`}>
<span className="text-xl text-white font-bold">{book.title.charAt(0)}</span>
</div>
)}
</div>
);
}

function getBookGradient(title: string): string {
const gradients = [
"from-blue-400 to-purple-500",
Expand Down Expand Up @@ -107,24 +128,11 @@ export default function CollectionDetail() {
key={book.id}
className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg group"
>
<div
className="w-12 h-18 rounded overflow-hidden flex-shrink-0 cursor-pointer"
<BookCover
book={book}
className="w-12 h-18 cursor-pointer"
onClick={() => navigate(`/book/${book.id}/detail`)}
>
{book.coverUrl ? (
<img
src={getCoverImageUrl(book.coverUrl)}
alt={book.title}
className="w-full h-full object-cover"
/>
) : (
<div
className={`w-full h-full flex items-center justify-center bg-gradient-to-br ${getBookGradient(book.title)}`}
>
<span className="text-xl text-white font-bold">{book.title.charAt(0)}</span>
</div>
)}
</div>
/>
<div
className="flex-1 min-w-0 cursor-pointer"
onClick={() => navigate(`/book/${book.id}/detail`)}
Expand Down
131 changes: 70 additions & 61 deletions apps/desktop/src/pages/Library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ const BookCard: React.FC<{ book: Book; onSelect: () => void }> = ({
: progress >= 100
? "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400"
: "bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400";
const [coverError, setCoverError] = useState(false);
const coverSrc = getCoverImageUrl(book.coverUrl);

return (
<div
Expand All @@ -89,11 +91,12 @@ const BookCard: React.FC<{ book: Book; onSelect: () => void }> = ({
>
{/* Cover */}
<div className="aspect-[2/3] rounded-xl overflow-hidden relative shadow-sm group-hover:shadow-md transition-shadow">
{book.coverUrl ? (
{coverSrc && !coverError ? (
<img
src={getCoverImageUrl(book.coverUrl)}
src={coverSrc}
alt={book.title}
className="w-full h-full object-cover"
onError={() => setCoverError(true)}
/>
) : (
<div className={`w-full h-full flex items-center justify-center bg-gradient-to-br ${getBookGradient(book.title)}`}>
Expand Down Expand Up @@ -150,6 +153,68 @@ const BookCard: React.FC<{ book: Book; onSelect: () => void }> = ({
);
};

const BookListItem: React.FC<{ book: Book; onSelect: () => void }> = ({ book, onSelect }) => {
const progress = book.readingProgress ?? 0;
const statusText = progress === 0 ? "未读" : progress >= 100 ? "已读完" : "在读";
const statusColor = progress === 0
? "bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
: progress >= 100
? "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400"
: "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400";
const [coverError, setCoverError] = useState(false);
const coverSrc = getCoverImageUrl(book.coverUrl);

return (
<div
className="flex items-center gap-4 py-4 cursor-pointer group hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors -mx-4 px-4 rounded-lg"
onClick={onSelect}
>
{/* Cover */}
<div className="w-12 h-16 rounded-lg overflow-hidden flex-shrink-0 shadow-sm">
{coverSrc && !coverError ? (
<img
src={coverSrc}
alt={book.title}
className="w-full h-full object-cover"
onError={() => setCoverError(true)}
/>
) : (
<div className={`w-full h-full flex items-center justify-center bg-gradient-to-br ${getBookGradient(book.title)}`}>
<span className="text-sm text-white font-bold">
{book.title.charAt(0)}
</span>
</div>
)}
</div>

{/* Title + Author */}
<div className="flex-1 min-w-0">
<h3 className="font-medium text-gray-900 dark:text-white truncate">
{book.title}
</h3>
<p className="text-sm text-gray-400 dark:text-gray-500 truncate">
{book.author || "未知作者"}
</p>
</div>

{/* Format */}
<span className="text-sm text-orange-500 dark:text-orange-400 flex-shrink-0">
{(book.fileType || book.format || '').toUpperCase()}
</span>

{/* Status badge */}
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium flex-shrink-0 ${statusColor}`}>
{statusText}
</span>

{/* File size */}
<span className="text-sm text-gray-400 dark:text-gray-500 flex-shrink-0 w-20 text-right">
{formatFileSize(book.fileSize)}
</span>
</div>
);
};

export default function Library() {
const navigate = useNavigate();
const { books, isLoading, error, searchQuery, fetchBooks, setSearchQuery } =
Expand Down Expand Up @@ -629,65 +694,9 @@ export default function Library() {
</div>
) : (
<div className="divide-y divide-gray-100 dark:divide-gray-800">
{filteredBooks.map((book) => {
const progress = book.readingProgress ?? 0;
const statusText = progress === 0 ? "未读" : progress >= 100 ? "已读完" : "在读";
const statusColor = progress === 0
? "bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
: progress >= 100
? "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400"
: "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400";

return (
<div
key={book.id}
className="flex items-center gap-4 py-4 cursor-pointer group hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors -mx-4 px-4 rounded-lg"
onClick={() => handleBookSelect(book)}
>
{/* Cover */}
<div className="w-12 h-16 rounded-lg overflow-hidden flex-shrink-0 shadow-sm">
{book.coverUrl ? (
<img
src={getCoverImageUrl(book.coverUrl)}
alt={book.title}
className="w-full h-full object-cover"
/>
) : (
<div className={`w-full h-full flex items-center justify-center bg-gradient-to-br ${getBookGradient(book.title)}`}>
<span className="text-sm text-white font-bold">
{book.title.charAt(0)}
</span>
</div>
)}
</div>

{/* Title + Author */}
<div className="flex-1 min-w-0">
<h3 className="font-medium text-gray-900 dark:text-white truncate">
{book.title}
</h3>
<p className="text-sm text-gray-400 dark:text-gray-500 truncate">
{book.author || "未知作者"}
</p>
</div>

{/* Format */}
<span className="text-sm text-orange-500 dark:text-orange-400 flex-shrink-0">
{(book.fileType || book.format || '').toUpperCase()}
</span>

{/* Status badge */}
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium flex-shrink-0 ${statusColor}`}>
{statusText}
</span>

{/* File size */}
<span className="text-sm text-gray-400 dark:text-gray-500 flex-shrink-0 w-20 text-right">
{formatFileSize(book.fileSize)}
</span>
</div>
);
})}
{filteredBooks.map((book) => (
<BookListItem key={book.id} book={book} onSelect={() => handleBookSelect(book)} />
))}
</div>
)}
</section>
Expand Down
Loading
Loading