Files
MookNote/lib/pages/movies/movie_detail_page.dart
2026-07-06 02:49:46 +08:00

1231 lines
43 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../widgets/fade_in_local_image.dart';
import '../../providers/app_provider.dart';
import '../../models/data_models.dart';
import '../../utils/user_prefs.dart';
import '../../utils/toast_util.dart';
import 'movie_reviews_page.dart';
import 'movie_posters_page.dart';
import 'movie_share_page.dart';
/// 影视详情页 - 极简主义设计
class MovieDetailPage extends StatefulWidget {
final Movie movie;
final bool embedded;
const MovieDetailPage({super.key, required this.movie, this.embedded = false});
@override
State<MovieDetailPage> createState() => _MovieDetailPageState();
}
class _MovieDetailPageState extends State<MovieDetailPage> {
late bool _showExactDate;
late int _detailStyle;
final ValueNotifier<double> _posterOffset = ValueNotifier(0.0);
double _posterDragStartOffset = 0.0;
final ValueNotifier<bool> _draggingPoster = ValueNotifier(false);
final GlobalKey _posterImageKey = GlobalKey();
double _posterImageHeight = 0.0;
final ValueNotifier<bool> _showTitle = ValueNotifier(false);
ScrollController? _overlayScrollController;
@override
void initState() {
super.initState();
_showExactDate = UserPrefs().showExactReleaseDate;
_detailStyle = UserPrefs().detailPageStyle;
_posterOffset.value = UserPrefs().getCoverOffset(widget.movie.id);
}
@override
void dispose() {
_posterOffset.dispose();
_draggingPoster.dispose();
_showTitle.dispose();
_overlayScrollController?.dispose();
super.dispose();
}
void _toggleDateDisplay() {
setState(() => _showExactDate = !_showExactDate);
UserPrefs().setShowExactReleaseDate(_showExactDate);
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final movie = context.watch<AppProvider>().movies
.where((m) => m.id == widget.movie.id)
.firstOrNull ?? widget.movie;
return _detailStyle == 1
? _buildOverlayStyle(movie, colors)
: _buildStandardStyle(movie, colors);
}
/// 标准样式
Widget _buildStandardStyle(Movie movie, ColorScheme colors) {
final topSafe = MediaQuery.of(context).padding.top;
return Scaffold(
backgroundColor: colors.surface,
body: Stack(
children: [
// 整体可滚动(图片 + 内容一起滑动)
Padding(
padding: EdgeInsets.only(top: topSafe + 48),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面图
SizedBox(
height: 320,
width: double.infinity,
child: _buildPosterSection(movie),
),
// 详细信息
_buildBasicInfo(movie),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
if (movie.directors.isNotEmpty)
_buildDirectorsSection(movie),
if (movie.writers.isNotEmpty)
_buildWritersSection(movie),
if (movie.actors.isNotEmpty)
_buildActorsSection(movie),
if (movie.genres.isNotEmpty)
_buildGenresSection(movie),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
if (movie.summary != null && movie.summary!.isNotEmpty)
_buildSummarySection(movie),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildExtraSections(movie),
const SizedBox(height: 120),
],
),
),
),
// 详情页面的标准顶部导航栏
Positioned(
top: 0, left: 0, right: 0,
child: Container(
padding: EdgeInsets.only(top: topSafe),
color: colors.surface,
child: SizedBox(
height: 48,
child: Row(children: [
const SizedBox(width: 4),
IconButton(
icon: widget.embedded
? Icon(Icons.close, color: colors.onSurface, size: 18)
: Icon(Icons.arrow_back_ios_new, color: colors.onSurface, size: 18),
onPressed: widget.embedded
? () => context.read<AppProvider>().selectMovie(null)
: () => Navigator.pop(context),
),
const SizedBox(width: 4),
Expanded(
child: Text(movie.title,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface),
maxLines: 1, overflow: TextOverflow.ellipsis),
),
_buildStyleButton(color: colors.onSurface),
]),
),
),
),
Positioned(
right: 16,
bottom: 24,
child: _buildFloatingActionButtons(movie),
),
],
),
);
}
/// 毛玻璃层叠样式
Widget _buildOverlayStyle(Movie movie, ColorScheme colors) {
final screenH = MediaQuery.of(context).size.height;
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
// 初始化滚动控制器(只创建一次)
_overlayScrollController ??= ScrollController()..addListener(() {
final show = (_overlayScrollController?.offset ?? 0) > 10;
if (_showTitle.value != show) _showTitle.value = show;
});
return Scaffold(
body: Stack(
children: [
// 海报背景
if (hasPoster)
Positioned.fill(
child: Image(
image: FileImage(File(movie.posterPath!)),
fit: BoxFit.cover, width: double.infinity, height: screenH,
repeat: ImageRepeat.repeatY,
),
)
else
Container(color: colors.surfaceContainerHighest),
// 毛玻璃
ClipRect(
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 25, sigmaY: 25),
child: Container(color: Colors.black.withValues(alpha: 0.4)),
),
),
// 内容
SafeArea(
child: Column(children: [
// 顶部栏:只在滚动后显示标题
SizedBox(
height: 48,
child: Row(children: [
const SizedBox(width: 4),
IconButton(
icon: widget.embedded
? const Icon(Icons.close, color: Colors.white, size: 18)
: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 18),
onPressed: widget.embedded
? () => context.read<AppProvider>().selectMovie(null)
: () => Navigator.pop(context),
),
ValueListenableBuilder<bool>(
valueListenable: _showTitle,
builder: (_, show, __) => AnimatedOpacity(
opacity: show ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.5),
child: Text(movie.title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
maxLines: 1, overflow: TextOverflow.ellipsis),
),
),
),
const Spacer(),
_buildStyleButton(),
]),
),
Expanded(
child: SingleChildScrollView(
controller: _overlayScrollController,
padding: const EdgeInsets.fromLTRB(16, 8, 16, 100),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_buildOverlayHeader(movie),
const SizedBox(height: 20),
// 信息区块:无毛玻璃
if (movie.directors.isNotEmpty) _buildDirectorsSection(movie),
if (movie.writers.isNotEmpty) _buildWritersSection(movie),
if (movie.actors.isNotEmpty) _buildActorsSection(movie),
// 类型标签毛玻璃
if (movie.genres.isNotEmpty) ...[
const SizedBox(height: 12),
_buildGenresSection(movie),
],
// 简介:内部已有毛玻璃卡片
if (movie.summary != null && movie.summary!.isNotEmpty) ...[
const SizedBox(height: 12),
_buildSummarySection(movie),
],
const SizedBox(height: 12),
// 影评、海报墙毛玻璃
_buildExtraSectionsOverlay(movie),
]),
),
),
]),
),
Positioned(right: 16, bottom: 24, child: _buildFloatingActionButtons(movie)),
],
),
);
}
/// 叠层模式:封面小图 + 标题/评分
Widget _buildOverlayHeader(Movie movie) {
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 100, height: 140,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 12, offset: const Offset(0, 4))],
),
clipBehavior: Clip.antiAlias,
child: hasPoster
? FadeInLocalImage(path: movie.posterPath, fit: BoxFit.cover)
: Container(color: Colors.white24, child: const Icon(Icons.movie_outlined, color: Colors.white38, size: 32)),
),
const SizedBox(width: 16),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const SizedBox(height: 4),
Text(movie.title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white)),
if (movie.directors.isNotEmpty) ...[
const SizedBox(height: 6),
Text('导演:${movie.directors.join(' / ')}', style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: 0.6))),
],
const SizedBox(height: 12),
Row(children: [
if (movie.rating != null && movie.rating! > 0) ...[
const Icon(Icons.star, size: 16, color: Color(0xFFFFB800)),
const SizedBox(width: 4),
Text(movie.rating!.toStringAsFixed(1), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFFFFB800))),
const SizedBox(width: 16),
],
_statusChip(movie.status),
]),
]),
),
],
);
}
Widget _statusChip(String status) {
final (label, bg, fg) = switch (status) {
'watched' => ('已看', const Color(0xFF1A1A1A), Colors.white),
'watching' => ('在看', const Color(0xFF666666), Colors.white),
'want_to_watch' => ('想看', const Color(0xFF999999), Colors.white),
_ => ('', const Color(0xFF999999), Colors.white),
};
if (label.isEmpty) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12)),
child: Text(label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: fg)),
);
}
Widget _buildStyleButton({Color color = Colors.white}) {
return IconButton(
icon: Icon(Icons.tune, color: color, size: 20),
tooltip: '切换样式',
onPressed: _showStylePicker,
);
}
void _showStylePicker() {
final colors = Theme.of(context).colorScheme;
const names = ['默认样式', '毛玻璃层叠'];
const icons = [Icons.article_outlined, Icons.blur_on_outlined];
const subtitles = ['标准封面顶部布局', '封面背景 + 毛玻璃卡片'];
showModalBottomSheet(
context: context,
backgroundColor: colors.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Container(width: 36, height: 4, decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
const SizedBox(height: 20),
Align(alignment: Alignment.centerLeft, child: Text('详情页样式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface))),
const SizedBox(height: 12),
for (int i = 0; i < names.length; i++) ...[
if (i > 0) Divider(height: 0.5, color: colors.outlineVariant),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
child: Icon(icons[i], size: 20, color: _detailStyle == i ? colors.primary : colors.onSurface.withValues(alpha: 0.6))),
title: Text(names[i], style: TextStyle(fontSize: 13, fontWeight: _detailStyle == i ? FontWeight.w600 : FontWeight.w500, color: colors.onSurface)),
subtitle: Text(subtitles[i], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: _detailStyle == i
? Icon(Icons.check_circle, size: 20, color: colors.primary)
: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
onTap: () { setState(() => _detailStyle = i); UserPrefs().setDetailPageStyle(i); Navigator.pop(ctx); },
),
],
const SizedBox(height: 12),
]),
),
);
}
Widget _buildFloatingActionButtons(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFloatingButton(
icon: Icons.edit_outlined,
onPressed: () => _navigateToEdit(context),
tooltip: '编辑',
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
),
const SizedBox(height: 12),
_buildFloatingButton(
icon: Icons.delete_outline,
onPressed: () => _showDeleteDialog(context),
tooltip: '删除',
backgroundColor: colors.error,
foregroundColor: colors.onError,
),
const SizedBox(height: 12),
_buildFloatingButton(
icon: Icons.share_outlined,
onPressed: () => _showSharePoster(movie),
tooltip: '分享海报',
backgroundColor: const Color(0xFF4CAF50),
foregroundColor: Colors.white,
),
],
);
}
Widget _buildFloatingButton({
required IconData icon,
required VoidCallback onPressed,
required String tooltip,
required Color backgroundColor,
required Color foregroundColor,
}) {
return Tooltip(
message: tooltip,
child: GestureDetector(
onTap: onPressed,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: backgroundColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: backgroundColor.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Icon(icon, size: 18, color: foregroundColor),
),
),
);
}
Widget _buildPosterSection(Movie movie) {
final colors = Theme.of(context).colorScheme;
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
return LayoutBuilder(
builder: (context, constraints) {
final containerH = constraints.maxHeight;
return GestureDetector(
onLongPressStart: hasPoster ? (_) {
HapticFeedback.mediumImpact();
// 测量图片渲染高度
final ctx = _posterImageKey.currentContext;
if (ctx != null) {
final box = ctx.findRenderObject() as RenderBox?;
if (box != null) _posterImageHeight = box.size.height;
}
_draggingPoster.value = true;
_posterDragStartOffset = _posterOffset.value;
} : null,
onLongPressMoveUpdate: hasPoster ? (d) {
// 实时钳制:图片顶部不超过容器顶部(offset<=0),图片底部不低于容器底部(offset+H>=containerH)
final raw = _posterDragStartOffset + d.offsetFromOrigin.dy;
final imgH = _posterImageHeight > 0 ? _posterImageHeight : containerH;
final minOffset = -(imgH - containerH).clamp(0, double.infinity); // 图片底部不能高于容器底部
_posterOffset.value = (raw.clamp(minOffset, 0.0) as double);
} : null,
onLongPressEnd: hasPoster ? (_) {
_draggingPoster.value = false;
final offset = _posterOffset.value;
UserPrefs().setCoverOffset(widget.movie.id, offset);
context.read<AppProvider>().updateMovieCoverOffset(widget.movie.id, offset);
} : null,
child: ValueListenableBuilder<double>(
valueListenable: _posterOffset,
builder: (context, offset, _) {
return Stack(
fit: StackFit.expand,
children: [
if (hasPoster)
ClipRect(
child: Stack(
children: [
Positioned(
top: offset,
left: 0, right: 0,
child: FadeInLocalImage(
key: _posterImageKey,
path: movie.posterPath,
fit: BoxFit.fitWidth,
width: constraints.maxWidth,
),
),
],
),
)
else
_buildPosterPlaceholder(),
// 底部渐变 + 拖动遮罩
ValueListenableBuilder<bool>(
valueListenable: _draggingPoster,
builder: (context, dragging, _) {
return Stack(
children: [
if (!dragging)
Positioned(
left: 0, right: 0, bottom: 0,
child: IgnorePointer(
child: Container(
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
colors.surface.withValues(alpha: 0),
colors.surface,
],
),
),
),
),
),
if (dragging) ...[
Positioned.fill(
child: Container(color: Colors.black.withValues(alpha: 0.3)),
),
Positioned(
left: 0, right: 0, bottom: 20,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(20),
),
child: const Text('上下滑动调整图片位置',
style: TextStyle(fontSize: 13, color: Colors.white70)),
),
),
),
],
],
);
},
),
],
);
},
),
);
},
);
}
Widget _buildPosterPlaceholder() {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.movie_outlined,
size: 64,
color: colors.onSurface.withValues(alpha: 0.25),
),
const SizedBox(height: 16),
Text(
'暂无海报',
style: TextStyle(
fontSize: 14,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
),
);
}
Widget _buildBasicInfo(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
movie.title,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: colors.onSurface,
height: 1.3,
),
),
if (movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
movie.alternateTitles.join(' / '),
style: TextStyle(
fontSize: 14,
color: colors.onSurface.withValues(alpha: 0.4),
height: 1.4,
),
),
],
const SizedBox(height: 16),
Row(
children: [
if (movie.rating != null) ...[
Icon(
Icons.star,
size: 20,
color: colors.onSurface,
),
const SizedBox(width: 4),
Text(
movie.rating!.toStringAsFixed(1),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(width: 16),
],
_buildStatusTag(movie),
const SizedBox(width: 6),
_buildCategoryTag(movie),
],
),
const SizedBox(height: 8),
if (movie.releaseDate != null)
GestureDetector(
onTap: _toggleDateDisplay,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_showExactDate
? '${movie.releaseDate!.year}${movie.releaseDate!.month.toString().padLeft(2, '0')}${movie.releaseDate!.day.toString().padLeft(2, '0')}日上映'
: '${movie.releaseDate!.year}${movie.releaseDate!.month.toString().padLeft(2, '0')}月上映',
style: TextStyle(
fontSize: 14,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(width: 4),
Icon(Icons.tune, size: 14, color: colors.onSurface.withValues(alpha: 0.2)),
],
),
),
const SizedBox(height: 8),
if (movie.watchDate != null)
Text(
'观看于 ${_formatDate(movie.watchDate!)}',
style: TextStyle(
fontSize: 14,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
),
);
}
Widget _buildStatusTag(Movie movie) {
final colors = Theme.of(context).colorScheme;
String label;
Color bgColor;
Color textColor;
switch (movie.status) {
case 'watched':
label = '已看';
bgColor = colors.primary;
textColor = colors.onPrimary;
break;
case 'watching':
label = '在看';
bgColor = colors.outlineVariant;
textColor = colors.onSurface.withValues(alpha: 0.6);
break;
case 'want_to_watch':
label = '想看';
bgColor = colors.surfaceContainerHighest;
textColor = colors.onSurface.withValues(alpha: 0.4);
break;
default:
label = '未知';
bgColor = colors.outlineVariant;
textColor = colors.onSurface.withValues(alpha: 0.25);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(6),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
color: textColor,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildCategoryTag(Movie movie) {
final colors = Theme.of(context).colorScheme;
const labels = {
'movie': '电影',
'tv': '电视剧',
'anime': '动漫',
'variety': '综艺',
'documentary': '纪录片',
'short': '微短剧',
};
final label = labels[movie.category];
if (label == null) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withValues(alpha: 0.5),
fontWeight: FontWeight.w500,
),
),
);
}
Widget _buildDirectorsSection(Movie movie) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: isOverlay ? 5 : 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 48,
child: Text(
'导演',
style: TextStyle(
fontSize: 13,
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
movie.directors.join(''),
style: TextStyle(
fontSize: 15,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.5,
),
),
),
],
),
);
}
Widget _buildWritersSection(Movie movie) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: isOverlay ? 5 : 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 48,
child: Text(
'编剧',
style: TextStyle(
fontSize: 13,
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
movie.writers.join(''),
style: TextStyle(
fontSize: 15,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.5,
),
),
),
],
),
);
}
Widget _buildActorsSection(Movie movie) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: isOverlay ? 5 : 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 48,
child: Text(
'主演',
style: TextStyle(
fontSize: 13,
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
movie.actors.join(''),
style: TextStyle(
fontSize: 15,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.5,
),
),
),
],
),
);
}
Widget _buildGenresSection(Movie movie) {
final isOverlay = _detailStyle == 1;
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: isOverlay ? 5 : 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 48,
child: Text('类型', style: TextStyle(fontSize: 13,
color: isOverlay ? const Color(0x66FFFFFF) : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4))),
),
Expanded(
child: Wrap(
spacing: 8, runSpacing: 8,
children: movie.genres.map((g) => _buildGenreChip(g, isOverlay)).toList(),
),
),
],
),
);
}
Widget _buildGenreChip(String label, bool isOverlay) {
if (isOverlay) {
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(16),
),
child: Text(label, style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.85))),
),
),
);
}
final colors = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
),
child: Text(label, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))),
);
}
Widget _buildSummarySection(Movie movie) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: isOverlay ? Colors.white : colors.onSurface,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
'简介',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: isOverlay ? Colors.white : colors.onSurface,
),
),
],
),
const SizedBox(height: 16),
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
),
child: Text(
movie.summary!,
style: TextStyle(
fontSize: 15,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.8,
),
),
),
),
),
],
),
);
}
Widget _buildExtraSections(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: colors.onSurface,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
'更多',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
],
),
const SizedBox(height: 16),
_buildExtraSectionItem(
icon: Icons.rate_review_outlined,
title: '影评',
subtitleFuture: context.read<AppProvider>().getMovieReviewCount(movie.id),
emptyText: '暂无影评',
unit: '条影评',
onTap: () => _navigateToReviews(movie),
),
const SizedBox(height: 12),
_buildExtraSectionItem(
icon: Icons.photo_library_outlined,
title: '海报墙',
subtitleFuture: context.read<AppProvider>().getMoviePosterCount(movie.id),
emptyText: '暂无海报',
unit: '张海报',
onTap: () => _navigateToPosters(movie),
),
],
),
);
}
/// 叠层模式:影评、海报墙各自独立毛玻璃卡片
Widget _buildExtraSectionsOverlay(Movie movie) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
_buildFrostedExtraItem(
icon: Icons.rate_review_outlined,
title: '影评',
subtitleFuture: context.read<AppProvider>().getMovieReviewCount(movie.id),
emptyText: '暂无影评',
unit: '条影评',
onTap: () => _navigateToReviews(movie),
),
const SizedBox(height: 12),
_buildFrostedExtraItem(
icon: Icons.photo_library_outlined,
title: '海报墙',
subtitleFuture: context.read<AppProvider>().getMoviePosterCount(movie.id),
emptyText: '暂无海报',
unit: '张海报',
onTap: () => _navigateToPosters(movie),
),
],
),
);
}
/// 毛玻璃影评/海报卡片
Widget _buildFrostedExtraItem({
required IconData icon,
required String title,
required Future<int> subtitleFuture,
required String emptyText,
required String unit,
required VoidCallback onTap,
}) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(children: [
Icon(icon, size: 20, color: Colors.white.withValues(alpha: 0.7)),
const SizedBox(width: 12),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)),
FutureBuilder<int>(
future: subtitleFuture,
builder: (ctx, snap) {
final count = snap.data ?? 0;
return Text(count > 0 ? '$count $unit' : emptyText,
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.5)));
},
),
]),
),
Icon(Icons.chevron_right, size: 16, color: Colors.white.withValues(alpha: 0.3)),
]),
),
),
),
),
),
);
}
Widget _buildExtraSectionItem({
required IconData icon,
required String title,
required Future<int> subtitleFuture,
required String emptyText,
required String unit,
required VoidCallback onTap,
}) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Icon(
icon,
size: 20,
color: colors.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(height: 4),
FutureBuilder<int>(
future: subtitleFuture,
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
return Text(
count > 0 ? '$count $unit' : emptyText,
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
),
);
},
),
],
),
),
Icon(
Icons.chevron_right,
color: colors.onSurface.withValues(alpha: 0.25),
),
],
),
),
);
}
void _navigateToReviews(Movie movie) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MovieReviewsPage(movie: movie),
),
);
}
void _navigateToPosters(Movie movie) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MoviePostersPage(movie: movie),
),
);
}
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
void _navigateToEdit(BuildContext context) {
final provider = context.read<AppProvider>();
Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) {
provider.setEditRefresh(widget.movie.id);
provider.loadMovies();
});
}
void _showDeleteDialog(BuildContext context) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Text(
'确认删除',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
content: Text(
'确定要删除"${widget.movie.title}"吗?删除后可在回收站恢复。',
style: TextStyle(
fontSize: 14,
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.5,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () async {
final provider = context.read<AppProvider>();
await provider.removeMovie(widget.movie.id);
if (!mounted || !context.mounted) return;
if (widget.embedded) {
Navigator.of(context).pop(); // close dialog
provider.selectMovie(null);
} else {
final navigator = Navigator.of(context);
navigator.pop();
navigator.pop();
}
if (mounted && context.mounted) {
ToastUtil.show(context, '已删除');
}
},
style: ElevatedButton.styleFrom(
backgroundColor: colors.error,
foregroundColor: colors.onError,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('删除'),
),
],
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
);
}
void _showSharePoster(Movie movie) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MovieSharePage(movie: movie),
),
);
}
}