epub阅读+关联书籍

This commit is contained in:
DelLevin-Home
2026-06-27 18:13:34 +08:00
parent d54e33e4cf
commit bcff4f6c01
15 changed files with 1143 additions and 124 deletions

View File

@@ -0,0 +1,212 @@
import 'dart:io';
import 'package:flutter/material.dart';
/// Display mode for the book grid item.
enum ViewMode { relaxed, compact }
/// Book grid item widget displays a single book in the grid.
/// Appearance branches based on [ViewMode].
class BookGridItem extends StatelessWidget {
final Map<String, dynamic> book;
final ViewMode viewMode;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
const BookGridItem({
super.key,
required this.book,
required this.viewMode,
this.onTap,
this.onLongPress,
});
// ─── public build ────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
onLongPress: onLongPress,
child: switch (viewMode) {
ViewMode.relaxed => _buildRelaxed(context),
ViewMode.compact => _buildCompact(context),
},
);
}
// ─── mode helpers ─────────────────────────────────────────────────────────
/// Relaxed: cover + title + author + progress bar.
Widget _buildRelaxed(BuildContext context) {
final title = book['title'] as String? ?? '';
final author = book['author'] as String? ?? '';
final progress = _readingProgress;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildCoverStack(context, fit: StackFit.expand)),
const SizedBox(height: 12),
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
if (author.isNotEmpty)
Text(
author,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 12,
),
),
if (progress > 0)
Padding(
padding: const EdgeInsets.only(top: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: progress,
minHeight: 3,
),
),
),
],
);
}
/// Compact: cover only, title gradient overlay + progress badge.
Widget _buildCompact(BuildContext context) {
final title = book['title'] as String? ?? '';
return _buildCoverStack(
context,
fit: StackFit.expand,
extras: [
// Bottom gradient + title
Positioned(
bottom: 0,
left: 0,
right: 0,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(6),
),
child: Container(
padding: const EdgeInsets.fromLTRB(6, 32, 6, 6),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black87],
),
),
child: Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w500,
shadows: [
const Shadow(
color: Colors.black54,
blurRadius: 2.0,
offset: Offset(0, 1.0),
),
],
),
),
),
),
),
// Progress badge (top-right)
_buildProgressBadge(context),
],
);
}
// ─── shared cover stack ───────────────────────────────────────────────────
Widget _buildCoverStack(
BuildContext context, {
List<Widget> extras = const [],
StackFit fit = StackFit.loose,
}) {
final coverPath = book['cover_path'] as String?;
final hasCover =
coverPath != null && coverPath.isNotEmpty && File(coverPath).existsSync();
return Stack(
fit: fit,
children: [
Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6)),
clipBehavior: Clip.antiAlias,
child: hasCover
? Image.file(
File(coverPath),
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder: (_, __, ___) => _buildPlaceholder(context),
)
: _buildPlaceholder(context),
),
...extras,
],
);
}
Widget _buildPlaceholder(BuildContext context) {
return Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Center(
child: Icon(
Icons.auto_stories_outlined,
size: 36,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.2),
),
),
);
}
// ─── badge helpers ────────────────────────────────────────────────────────
/// Progress badge (compact mode).
Widget _buildProgressBadge(BuildContext context) {
final progress = _readingProgress;
if (progress <= 0) return const SizedBox.shrink();
return Positioned(
top: 8,
right: 8,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
color: Theme.of(context).colorScheme.shadow.withValues(alpha: 0.8),
child: Text(
'${(progress * 100).toStringAsFixed(0)}%',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
// ─── helpers ──────────────────────────────────────────────────────────────
double get _readingProgress =>
(book['reading_percentage'] as num?)?.toDouble() ?? 0.0;
}