Files
bookshelf/books_flutter/lib/widgets/book_card.dart
Yuriy Panov 5c7b65a0d3 Implement light minimalistic tech redesign with Material 3
Complete transformation from dark to light theme with a professional,
tech-forward design system featuring:

- Material 3 theming with cyan-based color palette (#0891B2 primary)
- Inter font family integration via Google Fonts
- Comprehensive theme system (colors, spacing, typography, shadows)
- Responsive component redesign across all screens
- Enhanced UX with hover animations, Hero transitions, and shimmer loading
- Accessibility features (reduced motion support, high contrast)
- Clean architecture with zero hardcoded values

Theme System:
- Created app_colors.dart with semantic color constants
- Created app_spacing.dart with 8px base spacing scale
- Created app_theme.dart with complete Material 3 configuration
- Added shimmer_loading.dart for image loading states

UI Components Updated:
- Book cards with hover effects and Hero animations
- Bottom navigation with refined styling
- All screens migrated to theme-based colors and typography
- Forms and inputs using consistent design system

Documentation:
- Added REDESIGN_SUMMARY.md with complete implementation overview
- Added IMPLEMENTATION_CHECKLIST.md with detailed task completion status

All components now use centralized theme with no hardcoded values,
ensuring consistency and easy future customization.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-07 13:26:06 +06:00

188 lines
7.4 KiB
Dart

import 'package:flutter/material.dart';
import '../models/models.dart';
import '../theme/app_theme.dart';
import '../theme/app_spacing.dart';
import 'shimmer_loading.dart';
class BookCard extends StatefulWidget {
final Book book;
final VoidCallback onTap;
const BookCard({super.key, required this.book, required this.onTap});
@override
State<BookCard> createState() => _BookCardState();
}
class _BookCardState extends State<BookCard> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final disableAnimations = MediaQuery.of(context).disableAnimations;
return GestureDetector(
onTap: widget.onTap,
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedScale(
duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 200),
scale: _isHovered ? 1.02 : 1.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 2 / 3,
child: Hero(
tag: 'book-cover-${widget.book.id}',
child: Container(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(
AppSpacing.radiusMedium,
),
boxShadow: AppTheme.shadowMd,
),
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(
AppSpacing.radiusMedium,
),
child: widget.book.coverUrl != null
? Image.network(
widget.book.coverUrl!,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
loadingBuilder:
(context, child, loadingProgress) {
if (loadingProgress == null)
return child;
return ShimmerLoading(
borderRadius: AppSpacing.radiusMedium,
height: double.infinity,
);
},
errorBuilder: (_, e, st) =>
_placeholder(colorScheme),
)
: _placeholder(colorScheme),
),
if (widget.book.isFavorite)
Positioned(
top: AppSpacing.sm,
right: AppSpacing.sm,
child: Container(
padding: const EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.error,
borderRadius: BorderRadius.circular(
AppSpacing.radiusPill,
),
boxShadow: AppTheme.shadowSm,
),
child: const Icon(
Icons.favorite,
color: Colors.white,
size: 18,
),
),
),
if (widget.book.status == 'done')
Positioned(
top: AppSpacing.sm,
left: AppSpacing.sm,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: colorScheme.secondary,
borderRadius: BorderRadius.circular(
AppSpacing.radiusSmall,
),
),
child: Text(
'DONE',
style: textTheme.labelSmall?.copyWith(
color: colorScheme.onSecondary,
fontWeight: FontWeight.bold,
),
),
),
),
if (widget.book.status == 'reading' &&
widget.book.progress != null)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(
AppSpacing.radiusMedium,
),
bottomRight: Radius.circular(
AppSpacing.radiusMedium,
),
),
child: LinearProgressIndicator(
value: widget.book.progress! / 100,
minHeight: 4,
backgroundColor:
colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation(
colorScheme.primary,
),
),
),
),
],
),
),
),
),
const SizedBox(height: AppSpacing.sm),
Text(
widget.book.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.titleSmall,
),
const SizedBox(height: AppSpacing.xs),
Text(
widget.book.author,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
),
),
),
);
}
Widget _placeholder(ColorScheme colorScheme) {
return Container(
color: colorScheme.surfaceContainerHighest,
child: Center(
child: Icon(
Icons.book,
color: colorScheme.primary.withValues(alpha: 0.3),
size: 48,
),
),
);
}
}