长按图片调整位置,新增毛玻璃详情

This commit is contained in:
DelLevin-Home
2026-06-19 15:50:19 +08:00
parent 67b77f14e0
commit 631c532269
6 changed files with 1250 additions and 389 deletions

View File

@@ -1,5 +1,7 @@
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 'package:share_plus/share_plus.dart';
@@ -7,6 +9,7 @@ import 'package:cross_file/cross_file.dart';
import '../../providers/app_provider.dart';
import '../../models/data_models.dart';
import '../../utils/toast_util.dart';
import '../../utils/user_prefs.dart';
import 'book_reviews_page.dart';
import 'book_excerpts_page.dart';
import 'book_share_page.dart';
@@ -22,6 +25,25 @@ class BookDetailPage extends StatefulWidget {
}
class _BookDetailPageState extends State<BookDetailPage> {
late int _detailStyle;
final ValueNotifier<double> _coverOffset = ValueNotifier(0.0);
double _coverDragStartOffset = 0.0;
bool _draggingCover = false;
final GlobalKey _coverImageKey = GlobalKey();
double _coverImageHeight = 0.0;
@override
void dispose() {
_coverOffset.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
_detailStyle = UserPrefs().detailPageStyle;
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
@@ -29,49 +51,197 @@ class _BookDetailPageState extends State<BookDetailPage> {
.where((b) => b.id == widget.book.id)
.firstOrNull ?? widget.book;
if (_detailStyle == 1) {
return _buildOverlayStyle(book, colors);
}
return _buildStandardStyle(book, colors);
}
/// 标准样式
Widget _buildStandardStyle(Book book, ColorScheme colors) {
final topSafe = MediaQuery.of(context).padding.top;
return Scaffold(
backgroundColor: colors.surface,
body: Stack(
children: [
CustomScrollView(
slivers: [
_buildSliverAppBar(book),
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildBasicInfo(book),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildAuthorsSection(book),
if (book.genres.isNotEmpty)
_buildGenresSection(book),
if (book.isbn != null && book.isbn!.isNotEmpty)
_buildIsbnSection(book),
if (book.publisher != null && book.publisher!.isNotEmpty)
_buildPublisherSection(book),
if (book.publishDate != null)
_buildPublishDateSection(book),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
if (book.summary != null && book.summary!.isNotEmpty)
_buildSummarySection(book),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildExtraSections(book),
const SizedBox(height: 120),
],
),
// 图片从导航栏下方开始,固定在顶部
Column(
children: [
SizedBox(height: topSafe + 48),
SizedBox(
height: 320,
width: double.infinity,
child: _buildCoverSection(book),
),
],
),
// 可滚动内容区域从图片底部开始
Positioned(
right: 16,
bottom: 24,
child: _buildFloatingActionButtons(book),
top: topSafe + 48 + 320,
left: 0, right: 0, bottom: 0,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildBasicInfo(book),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildAuthorsSection(book),
if (book.genres.isNotEmpty) _buildGenresSection(book),
if (book.isbn != null && book.isbn!.isNotEmpty) _buildIsbnSection(book),
if (book.publisher != null && book.publisher!.isNotEmpty) _buildPublisherSection(book),
if (book.publishDate != null) _buildPublishDateSection(book),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
if (book.summary != null && book.summary!.isNotEmpty) _buildSummarySection(book),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildExtraSections(book),
const SizedBox(height: 120),
],
),
),
),
_buildStandardTopBar(book.title, colors),
Positioned(right: 16, bottom: 24, child: _buildFloatingActionButtons(book)),
],
),
);
}
/// 毛玻璃层叠样式:海报背景 + 毛玻璃 + 内容
Widget _buildOverlayStyle(Book book, ColorScheme colors) {
final screenH = MediaQuery.of(context).size.height;
final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty;
return Scaffold(
body: Stack(
children: [
// 海报背景(高度不够时重复拼接)
if (hasCover)
Positioned.fill(
child: Image(
image: FileImage(File(book.coverPath!)),
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.3)),
),
),
// 内容
SafeArea(
child: Column(
children: [
// 顶部栏
SizedBox(
height: 48,
child: Row(children: [
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 18),
onPressed: () => Navigator.pop(context),
),
const Spacer(),
Text(book.title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
maxLines: 1, overflow: TextOverflow.ellipsis),
const Spacer(),
_buildStyleButton(),
]),
),
// 可滚动内容
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面缩略图 + 标题
_buildOverlayHeader(book),
const SizedBox(height: 20),
// 信息区块:无毛玻璃
_buildAuthorsSection(book),
if (book.isbn != null && book.isbn!.isNotEmpty) _buildIsbnSection(book),
if (book.publisher != null && book.publisher!.isNotEmpty) _buildPublisherSection(book),
if (book.publishDate != null) _buildPublishDateSection(book),
// 类型标签毛玻璃
if (book.genres.isNotEmpty) _buildGenresSection(book),
// 简介:内部已有毛玻璃卡片
if (book.summary != null && book.summary!.isNotEmpty) ...[
const SizedBox(height: 12),
_buildSummarySection(book),
],
const SizedBox(height: 12),
// 书评、书摘毛玻璃
_buildExtraSectionsOverlay(book),
],
),
),
),
],
),
),
Positioned(right: 16, bottom: 24, child: _buildFloatingActionButtons(book)),
],
),
);
}
/// 叠层模式顶部:封面小图 + 标题/评分
Widget _buildOverlayHeader(Book book) {
final hasCover = book.coverPath != null && book.coverPath!.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: hasCover
? FadeInLocalImage(path: book.coverPath, fit: BoxFit.cover)
: Container(color: Colors.white24, child: const Icon(Icons.menu_book, color: Colors.white38, size: 32)),
),
const SizedBox(width: 16),
// 标题 + 信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(book.title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white)),
if (book.authors.isNotEmpty) ...[
const SizedBox(height: 6),
Text(book.authors.join(' / '), style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: 0.6))),
],
const SizedBox(height: 12),
Row(children: [
if (book.rating != null && book.rating! > 0) ...[
const Icon(Icons.star, size: 16, color: Color(0xFFFFB800)),
const SizedBox(width: 4),
Text(book.rating!.toStringAsFixed(1), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFFFFB800))),
const SizedBox(width: 16),
],
_statusChip(book.status),
]),
],
),
),
],
);
}
Widget _buildFloatingActionButtons(Book book) {
final colors = Theme.of(context).colorScheme;
return Column(
@@ -137,52 +307,130 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
Widget _buildBackButton() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => Navigator.pop(context),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(8),
child: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 22,
/// 固定在顶部的导航栏(纯色,与主体一致)
Widget _buildStandardTopBar(String title, ColorScheme colors) {
final topPadding = MediaQuery.of(context).padding.top;
return Positioned(
top: 0, left: 0, right: 0,
child: Container(
padding: EdgeInsets.only(top: topPadding),
color: colors.surface,
child: SizedBox(
height: 48,
child: Row(children: [
const SizedBox(width: 4),
IconButton(
icon: Icon(Icons.arrow_back_ios_new, color: colors.onSurface, size: 18),
onPressed: () => Navigator.pop(context),
),
),
const SizedBox(width: 4),
Expanded(
child: Text(title,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface),
maxLines: 1, overflow: TextOverflow.ellipsis),
),
_buildStyleButton(color: colors.onSurface),
]),
),
),
);
}
Widget _buildSliverAppBar(Book book) {
final colors = Theme.of(context).colorScheme;
return SliverAppBar(
expandedHeight: 320,
pinned: true,
backgroundColor: colors.surfaceContainerHighest,
leading: _buildBackButton(),
flexibleSpace: FlexibleSpaceBar(
background: _buildCoverSection(book),
),
);
}
Widget _buildCoverSection(Book book) {
return SizedBox.expand(
child: book.coverPath != null && book.coverPath!.isNotEmpty
? FadeInLocalImage(
path: book.coverPath,
fit: BoxFit.cover,
)
: _buildCoverPlaceholder(),
final colors = Theme.of(context).colorScheme;
final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty;
return LayoutBuilder(
builder: (context, constraints) {
final containerH = constraints.maxHeight;
return GestureDetector(
onLongPressStart: hasCover ? (_) {
HapticFeedback.mediumImpact();
final ctx = _coverImageKey.currentContext;
if (ctx != null) {
final box = ctx.findRenderObject() as RenderBox?;
if (box != null) _coverImageHeight = box.size.height;
}
setState(() => _draggingCover = true);
_coverDragStartOffset = _coverOffset.value;
} : null,
onLongPressMoveUpdate: hasCover ? (d) {
final raw = _coverDragStartOffset + d.offsetFromOrigin.dy;
final imgH = _coverImageHeight > 0 ? _coverImageHeight : containerH;
final minOffset = -(imgH - containerH).clamp(0, double.infinity);
_coverOffset.value = (raw.clamp(minOffset, 0.0) as double);
} : null,
onLongPressEnd: hasCover ? (_) {
setState(() => _draggingCover = false);
} : null,
child: ValueListenableBuilder<double>(
valueListenable: _coverOffset,
builder: (context, offset, _) {
return Stack(
fit: StackFit.expand,
children: [
if (hasCover)
ClipRect(
child: Stack(
children: [
Positioned(
top: offset,
left: 0, right: 0,
child: FadeInLocalImage(
key: _coverImageKey,
path: book.coverPath,
fit: BoxFit.fitWidth,
width: constraints.maxWidth,
),
),
],
),
)
else
_buildCoverPlaceholder(),
if (!_draggingCover)
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 (_draggingCover) ...[
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)),
),
),
),
],
],
);
},
),
);
},
);
}
@@ -210,6 +458,66 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 样式选择按钮
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 = ['默认样式', '毛玻璃层叠'];
showModalBottomSheet(
context: context,
backgroundColor: colors.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) => Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('详情页样式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
const SizedBox(height: 16),
Wrap(spacing: 10, runSpacing: 10, children: List.generate(names.length, (i) {
final selected = _detailStyle == i;
return GestureDetector(
onTap: () { setState(() => _detailStyle = i); UserPrefs().setDetailPageStyle(i); Navigator.pop(ctx); },
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
decoration: BoxDecoration(
color: selected ? colors.primary : colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: selected ? colors.primary : colors.outline, width: 0.5),
),
child: Text(names[i], style: TextStyle(
fontSize: 14, fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
)),
),
);
})),
]),
),
);
}
Widget _statusChip(String status) {
final colors = Theme.of(context).colorScheme;
final (label, bg, fg) = switch (status) {
'read' => ('已读', colors.primary, colors.onPrimary),
'reading' => ('在读', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)),
'want_to_read' => ('想读', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.4)),
_ => ('', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.3)),
};
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 _buildBasicInfo(Book book) {
final colors = Theme.of(context).colorScheme;
return Padding(
@@ -318,6 +626,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
Widget _buildAuthorsSection(Book book) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
@@ -330,7 +639,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
'作者',
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
),
),
),
@@ -339,7 +648,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
book.authors.join(''),
style: TextStyle(
fontSize: 15,
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.5,
),
),
@@ -350,6 +659,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
Widget _buildIsbnSection(Book book) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
@@ -362,7 +672,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
'ISBN',
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
),
),
),
@@ -371,7 +681,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
book.isbn!,
style: TextStyle(
fontSize: 15,
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.5,
),
),
@@ -382,6 +692,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
Widget _buildPublisherSection(Book book) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
@@ -394,7 +705,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
'出版社',
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
),
),
),
@@ -403,7 +714,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
book.publisher!,
style: TextStyle(
fontSize: 15,
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.5,
),
),
@@ -414,6 +725,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
Widget _buildPublishDateSection(Book book) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
@@ -426,7 +738,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
'出版时间',
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
),
),
),
@@ -435,7 +747,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
'${book.publishDate!.year}${book.publishDate!.month.toString().padLeft(2, '0')}',
style: TextStyle(
fontSize: 15,
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.5,
),
),
@@ -446,42 +758,21 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
Widget _buildGenresSection(Book book) {
final colors = Theme.of(context).colorScheme;
final isOverlay = _detailStyle == 1;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 64,
child: Text(
'类型',
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
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: book.genres.map((genre) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
child: Text(
genre,
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.6),
),
),
);
}).toList(),
spacing: 8, runSpacing: 8,
children: book.genres.map((g) => _buildGenreChip(g, isOverlay)).toList(),
),
),
],
@@ -489,7 +780,36 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
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(Book book) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(24),
@@ -502,7 +822,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
width: 4,
height: 16,
decoration: BoxDecoration(
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
borderRadius: BorderRadius.circular(2),
),
),
@@ -512,24 +832,30 @@ class _BookDetailPageState extends State<BookDetailPage> {
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Text(
book.summary!,
style: TextStyle(
fontSize: 15,
color: colors.onSurface,
height: 1.8,
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(
book.summary!,
style: TextStyle(
fontSize: 15,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.8,
),
),
),
),
),
@@ -589,6 +915,85 @@ class _BookDetailPageState extends State<BookDetailPage> {
);
}
/// 叠层模式:书评、摘抄各自独立毛玻璃卡片
Widget _buildExtraSectionsOverlay(Book book) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
_buildFrostedExtraItem(
icon: Icons.rate_review_outlined,
title: '书评',
subtitleFuture: context.read<AppProvider>().getBookReviewCount(book.id),
emptyText: '暂无书评',
unit: '条书评',
onTap: () => _navigateToReviews(book),
),
const SizedBox(height: 12),
_buildFrostedExtraItem(
icon: Icons.format_quote_outlined,
title: '摘抄',
subtitleFuture: context.read<AppProvider>().getBookExcerptCount(book.id),
emptyText: '暂无摘抄',
unit: '条摘抄',
onTap: () => _navigateToExcerpts(book),
),
],
),
);
}
/// 毛玻璃书评/摘抄卡片
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,

View File

@@ -1,5 +1,7 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
@@ -27,11 +29,18 @@ class MovieDetailPage extends StatefulWidget {
class _MovieDetailPageState extends State<MovieDetailPage> {
late bool _showExactDate;
late int _detailStyle;
final ValueNotifier<double> _posterOffset = ValueNotifier(0.0);
double _posterDragStartOffset = 0.0;
bool _draggingPoster = false;
final GlobalKey _posterImageKey = GlobalKey();
double _posterImageHeight = 0.0;
@override
void initState() {
super.initState();
_showExactDate = UserPrefs().showExactReleaseDate;
_detailStyle = UserPrefs().detailPageStyle;
}
void _toggleDateDisplay() {
@@ -46,38 +55,82 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
.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: [
CustomScrollView(
slivers: [
_buildSliverAppBar(movie),
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_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),
],
),
// 图片从导航栏下方开始,固定在顶部
Column(
children: [
SizedBox(height: topSafe + 48),
SizedBox(
height: 320,
width: double.infinity,
child: _buildPosterSection(movie),
),
],
),
// 可滚动内容区域从图片底部开始
Positioned(
top: topSafe + 48 + 320,
left: 0, right: 0, bottom: 0,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_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: Icon(Icons.arrow_back_ios_new, color: colors.onSurface, size: 18),
onPressed: () => 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,
@@ -88,6 +141,201 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
);
}
/// 毛玻璃层叠样式
Widget _buildOverlayStyle(Movie movie, ColorScheme colors) {
final screenH = MediaQuery.of(context).size.height;
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
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: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 18),
onPressed: () => Navigator.pop(context),
),
const Spacer(),
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(
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 ClipRRect(
borderRadius: BorderRadius.circular(16),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(16),
),
child: 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 = ['默认样式', '毛玻璃层叠'];
showModalBottomSheet(
context: context,
backgroundColor: colors.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) => Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('详情页样式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
const SizedBox(height: 16),
Wrap(spacing: 10, runSpacing: 10, children: List.generate(names.length, (i) {
final selected = _detailStyle == i;
return GestureDetector(
onTap: () { setState(() => _detailStyle = i); UserPrefs().setDetailPageStyle(i); Navigator.pop(ctx); },
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
decoration: BoxDecoration(
color: selected ? colors.primary : colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: selected ? colors.primary : colors.outline, width: 0.5),
),
child: Text(names[i], style: TextStyle(
fontSize: 14, fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
)),
),
);
})),
]),
),
);
}
Widget _buildFloatingActionButtons(Movie movie) {
final colors = Theme.of(context).colorScheme;
return Column(
@@ -153,52 +401,105 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
);
}
Widget _buildBackButton() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => Navigator.pop(context),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(8),
child: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 22,
),
),
),
),
);
}
Widget _buildSliverAppBar(Movie movie) {
final colors = Theme.of(context).colorScheme;
return SliverAppBar(
expandedHeight: 320,
pinned: true,
backgroundColor: colors.surfaceContainerHighest,
leading: _buildBackButton(),
flexibleSpace: FlexibleSpaceBar(
background: _buildPosterSection(movie),
),
);
}
Widget _buildPosterSection(Movie movie) {
return SizedBox.expand(
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
? FadeInLocalImage(
path: movie.posterPath,
fit: BoxFit.cover,
)
: _buildPosterPlaceholder(),
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;
}
setState(() => _draggingPoster = 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 ? (_) {
setState(() => _draggingPoster = false);
} : 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(),
// 底部白色渐变过渡(拖动时隐藏)
if (!_draggingPoster)
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 (_draggingPoster) ...[
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)),
),
),
),
],
],
);
},
),
);
},
);
}
@@ -356,6 +657,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
}
Widget _buildDirectorsSection(Movie movie) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
@@ -368,7 +670,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
'导演',
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
),
),
),
@@ -377,7 +679,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
movie.directors.join(''),
style: TextStyle(
fontSize: 15,
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.5,
),
),
@@ -388,6 +690,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
}
Widget _buildWritersSection(Movie movie) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
@@ -400,7 +703,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
'编剧',
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
),
),
),
@@ -409,7 +712,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
movie.writers.join(''),
style: TextStyle(
fontSize: 15,
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.5,
),
),
@@ -420,6 +723,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
}
Widget _buildActorsSection(Movie movie) {
final isOverlay = _detailStyle == 1;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
@@ -432,7 +736,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
'主演',
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
),
),
),
@@ -441,7 +745,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
movie.actors.join(''),
style: TextStyle(
fontSize: 15,
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
height: 1.5,
),
),
@@ -452,7 +756,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
}
Widget _buildGenresSection(Movie movie) {
final colors = Theme.of(context).colorScheme;
final isOverlay = _detailStyle == 1;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
@@ -460,34 +764,13 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
children: [
SizedBox(
width: 48,
child: Text(
'类型',
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
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((genre) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
child: Text(
genre,
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.6),
),
),
);
}).toList(),
spacing: 8, runSpacing: 8,
children: movie.genres.map((g) => _buildGenreChip(g, isOverlay)).toList(),
),
),
],
@@ -495,7 +778,36 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
);
}
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),
@@ -508,7 +820,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
width: 4,
height: 16,
decoration: BoxDecoration(
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
borderRadius: BorderRadius.circular(2),
),
),
@@ -518,24 +830,30 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: colors.onSurface,
color: isOverlay ? Colors.white : colors.onSurface,
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Text(
movie.summary!,
style: TextStyle(
fontSize: 15,
color: colors.onSurface,
height: 1.8,
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,
),
),
),
),
),
@@ -595,6 +913,85 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
);
}
/// 叠层模式:影评、海报墙各自独立毛玻璃卡片
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,

View File

@@ -1,5 +1,6 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
@@ -247,22 +248,29 @@ class _StatisticsPageState extends State<StatisticsPage> {
final avg = allRatings.reduce((a, b) => a + b) / allRatings.length;
// 按区间统计 (0-2, 2-4, 4-6, 6-8, 8-10)
final ranges = ['0-2', '2-4', '4-6', '6-8', '8-10'];
final counts = [0, 0, 0, 0, 0];
// 按 1-10 分统计
final counts = List.filled(10, 0);
for (final r in allRatings) {
if (r < 2) counts[0]++;
else if (r < 4) counts[1]++;
else if (r < 6) counts[2]++;
else if (r < 8) counts[3]++;
else counts[4]++;
final idx = (r.round()).clamp(1, 10) - 1;
counts[idx]++;
}
final maxCount = counts.isEmpty ? 1 : counts.reduce((a, b) => a > b ? a : b);
final maxCount = counts.reduce((a, b) => a > b ? a : b).toDouble();
if (maxCount == 0) return const SizedBox.shrink();
// 渐变色:低分灰色 → 高分金色
const barColors = [
Color(0xFFBDBDBD), Color(0xFFBDBDBD), // 1-2 灰
Color(0xFFFFCC80), Color(0xFFFFCC80), // 3-4 橙黄
Color(0xFFFFB74D), Color(0xFFFFB74D), // 5-6 橙
Color(0xFFFFA726), Color(0xFFFFA726), // 7-8 深橙
Color(0xFFFFB800), Color(0xFFFFB800), // 9-10 金
];
return _buildCard(
title: title,
child: Column(
children: [
// 平均评分
Row(
children: [
Text('平均评分', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))),
@@ -271,32 +279,71 @@ class _StatisticsPageState extends State<StatisticsPage> {
Text(' / 10', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.3))),
],
),
const SizedBox(height: 16),
...List.generate(5, (i) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
SizedBox(
width: 28,
child: Text(ranges[i], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
),
const SizedBox(width: 8),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: maxCount > 0 ? counts[i] / maxCount : 0.0,
backgroundColor: colors.outlineVariant,
color: colors.primary,
minHeight: 6,
),
const SizedBox(height: 20),
// 柱状图
SizedBox(
height: 160,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: maxCount * 1.2,
minY: 0,
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (_) => colors.inverseSurface,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
'${group.x + 1}${rod.toY.toInt()}',
TextStyle(color: colors.onInverseSurface, fontSize: 12, fontWeight: FontWeight.w600),
);
},
),
),
const SizedBox(width: 8),
Text('${counts[i]}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface)),
],
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return Padding(
padding: const EdgeInsets.only(top: 6),
child: Text('${value.toInt() + 1}', style: TextStyle(
fontSize: 10, color: colors.onSurface.withValues(alpha: 0.5))),
);
},
reservedSize: 24,
),
),
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
gridData: const FlGridData(show: false),
barGroups: List.generate(10, (i) {
return BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: counts[i].toDouble(),
color: barColors[i],
width: 20,
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
backDrawRodData: BackgroundBarChartRodData(
show: true,
toY: maxCount * 1.2,
color: colors.outlineVariant.withValues(alpha: 0.3),
),
),
],
);
}),
),
),
)),
),
const SizedBox(height: 8),
// 底部标签
Text('星级评分', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
],
),
);

View File

@@ -55,6 +55,18 @@ class UserPrefs {
bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true;
Future<bool> setFirstLaunch(bool value) => prefs.setBool('isFirstLaunch', value);
// ========== 详情页样式 ==========
/// 详情页展示样式: 0=标准(封面顶部), 1=叠层(封面+毛玻璃卡片)
int get detailPageStyle => prefs.getInt('detailPageStyle') ?? 0;
Future<bool> setDetailPageStyle(int value) => prefs.setInt('detailPageStyle', value);
// ========== 封面位置 ==========
/// 获取封面偏移量(-1.0 到 1.00 = 居中)
double getCoverOffset(String itemId) => prefs.getDouble('coverOffset_$itemId') ?? 0.0;
Future<bool> setCoverOffset(String itemId, double value) => prefs.setDouble('coverOffset_$itemId', value);
// ========== 主界面显示设置 ==========
/// 是否启用底部导航栏滚动隐藏(默认开启)

View File

@@ -6,7 +6,7 @@ packages:
description:
name: archive
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.6.1"
args:
@@ -14,7 +14,7 @@ packages:
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
@@ -22,7 +22,7 @@ packages:
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
@@ -30,7 +30,7 @@ packages:
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
@@ -38,7 +38,7 @@ packages:
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
checked_yaml:
@@ -46,7 +46,7 @@ packages:
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
@@ -54,7 +54,7 @@ packages:
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
@@ -62,7 +62,7 @@ packages:
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
@@ -70,7 +70,7 @@ packages:
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
collection:
@@ -78,7 +78,7 @@ packages:
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
@@ -86,7 +86,7 @@ packages:
description:
name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.3.5+2"
crypto:
@@ -94,7 +94,7 @@ packages:
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
@@ -102,7 +102,7 @@ packages:
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
equatable:
@@ -110,7 +110,7 @@ packages:
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
extended_text_field:
@@ -118,7 +118,7 @@ packages:
description:
name: extended_text_field
sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "16.0.2"
extended_text_library:
@@ -126,7 +126,7 @@ packages:
description:
name: extended_text_library
sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "12.0.1"
fake_async:
@@ -134,7 +134,7 @@ packages:
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
@@ -142,7 +142,7 @@ packages:
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
@@ -150,7 +150,7 @@ packages:
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_picker:
@@ -158,7 +158,7 @@ packages:
description:
name: file_picker
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "8.3.7"
file_selector_linux:
@@ -166,7 +166,7 @@ packages:
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
@@ -174,7 +174,7 @@ packages:
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
@@ -182,7 +182,7 @@ packages:
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
@@ -190,7 +190,7 @@ packages:
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
fixnum:
@@ -198,7 +198,7 @@ packages:
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
fl_chart:
@@ -206,7 +206,7 @@ packages:
description:
name: fl_chart
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.69.2"
flutter:
@@ -219,7 +219,7 @@ packages:
description:
name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
flutter_lints:
@@ -227,7 +227,7 @@ packages:
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
flutter_localizations:
@@ -240,7 +240,7 @@ packages:
description:
name: flutter_markdown_plus
sha256: "039177906850278e8fb1cd364115ee0a46281135932fa8ecea8455522166d2de"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.7"
flutter_plugin_android_lifecycle:
@@ -248,7 +248,7 @@ packages:
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.35"
flutter_staggered_grid_view:
@@ -256,7 +256,7 @@ packages:
description:
name: flutter_staggered_grid_view
sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
flutter_test:
@@ -274,7 +274,7 @@ packages:
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
http:
@@ -282,7 +282,7 @@ packages:
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
@@ -290,7 +290,7 @@ packages:
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
@@ -298,7 +298,7 @@ packages:
description:
name: image
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.3.0"
image_picker:
@@ -306,7 +306,7 @@ packages:
description:
name: image_picker
sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
image_picker_android:
@@ -314,7 +314,7 @@ packages:
description:
name: image_picker_android
sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.8.13+17"
image_picker_for_web:
@@ -322,7 +322,7 @@ packages:
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
@@ -330,7 +330,7 @@ packages:
description:
name: image_picker_ios
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.8.13+6"
image_picker_linux:
@@ -338,7 +338,7 @@ packages:
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
@@ -346,7 +346,7 @@ packages:
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
@@ -354,7 +354,7 @@ packages:
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
@@ -362,7 +362,7 @@ packages:
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
intl:
@@ -370,7 +370,7 @@ packages:
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
jni:
@@ -378,7 +378,7 @@ packages:
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
@@ -386,7 +386,7 @@ packages:
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
json_annotation:
@@ -394,7 +394,7 @@ packages:
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
leak_tracker:
@@ -402,7 +402,7 @@ packages:
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
@@ -410,7 +410,7 @@ packages:
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
@@ -418,7 +418,7 @@ packages:
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
@@ -426,7 +426,7 @@ packages:
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logging:
@@ -434,7 +434,7 @@ packages:
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
markdown:
@@ -442,7 +442,7 @@ packages:
description:
name: markdown
sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "7.3.1"
matcher:
@@ -450,7 +450,7 @@ packages:
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
@@ -458,7 +458,7 @@ packages:
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
@@ -466,7 +466,7 @@ packages:
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
mime:
@@ -474,7 +474,7 @@ packages:
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nested:
@@ -482,7 +482,7 @@ packages:
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
@@ -490,7 +490,7 @@ packages:
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "9.4.1"
package_config:
@@ -498,7 +498,7 @@ packages:
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
package_info_plus:
@@ -506,7 +506,7 @@ packages:
description:
name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "8.3.1"
package_info_plus_platform_interface:
@@ -514,7 +514,7 @@ packages:
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path:
@@ -522,7 +522,7 @@ packages:
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
@@ -530,7 +530,7 @@ packages:
description:
name: path_provider
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.6"
path_provider_android:
@@ -538,7 +538,7 @@ packages:
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
@@ -546,7 +546,7 @@ packages:
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
@@ -554,7 +554,7 @@ packages:
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
@@ -562,7 +562,7 @@ packages:
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_windows:
@@ -570,7 +570,7 @@ packages:
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
@@ -578,7 +578,7 @@ packages:
description:
name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "11.4.0"
permission_handler_android:
@@ -586,7 +586,7 @@ packages:
description:
name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "12.1.0"
permission_handler_apple:
@@ -594,7 +594,7 @@ packages:
description:
name: permission_handler_apple
sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "9.4.10"
permission_handler_html:
@@ -602,7 +602,7 @@ packages:
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
@@ -610,7 +610,7 @@ packages:
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
@@ -618,7 +618,7 @@ packages:
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
@@ -626,7 +626,7 @@ packages:
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform:
@@ -634,7 +634,7 @@ packages:
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
@@ -642,7 +642,7 @@ packages:
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
@@ -650,7 +650,7 @@ packages:
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
@@ -658,7 +658,7 @@ packages:
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
record_use:
@@ -666,7 +666,7 @@ packages:
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
share_plus:
@@ -674,7 +674,7 @@ packages:
description:
name: share_plus
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "10.1.4"
share_plus_platform_interface:
@@ -682,7 +682,7 @@ packages:
description:
name: share_plus_platform_interface
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
shared_preferences:
@@ -690,7 +690,7 @@ packages:
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
@@ -698,7 +698,7 @@ packages:
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
@@ -706,7 +706,7 @@ packages:
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
@@ -714,7 +714,7 @@ packages:
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
@@ -722,7 +722,7 @@ packages:
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
@@ -730,7 +730,7 @@ packages:
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
@@ -738,7 +738,7 @@ packages:
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
@@ -751,7 +751,7 @@ packages:
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
sqflite:
@@ -759,7 +759,7 @@ packages:
description:
name: sqflite
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.2+1"
sqflite_android:
@@ -767,7 +767,7 @@ packages:
description:
name: sqflite_android
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.2+3"
sqflite_common:
@@ -775,7 +775,7 @@ packages:
description:
name: sqflite_common
sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.5.8"
sqflite_darwin:
@@ -783,7 +783,7 @@ packages:
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
@@ -791,7 +791,7 @@ packages:
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
@@ -799,7 +799,7 @@ packages:
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
@@ -807,7 +807,7 @@ packages:
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
@@ -815,7 +815,7 @@ packages:
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
synchronized:
@@ -823,7 +823,7 @@ packages:
description:
name: synchronized
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.4.0+1"
term_glyph:
@@ -831,7 +831,7 @@ packages:
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
@@ -839,7 +839,7 @@ packages:
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
typed_data:
@@ -847,7 +847,7 @@ packages:
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
@@ -855,7 +855,7 @@ packages:
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
@@ -863,7 +863,7 @@ packages:
description:
name: url_launcher_android
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.3.30"
url_launcher_ios:
@@ -871,7 +871,7 @@ packages:
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
url_launcher_linux:
@@ -879,7 +879,7 @@ packages:
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
@@ -887,7 +887,7 @@ packages:
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
@@ -895,7 +895,7 @@ packages:
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
@@ -903,7 +903,7 @@ packages:
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
url_launcher_windows:
@@ -911,7 +911,7 @@ packages:
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
uuid:
@@ -919,7 +919,7 @@ packages:
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vector_math:
@@ -927,7 +927,7 @@ packages:
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
@@ -935,7 +935,7 @@ packages:
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
web:
@@ -943,7 +943,7 @@ packages:
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
webview_flutter:
@@ -951,7 +951,7 @@ packages:
description:
name: webview_flutter
sha256: e4d3474277c5043f5f3100ed27d94ad693abfd5c602b0221c719a4497e4ba1e4
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.14.0"
webview_flutter_android:
@@ -959,7 +959,7 @@ packages:
description:
name: webview_flutter_android
sha256: ad5182eff9a550925330cb9f0cb038eddfdd5712aba8b77aa0f0400e50f6e688
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
webview_flutter_platform_interface:
@@ -967,7 +967,7 @@ packages:
description:
name: webview_flutter_platform_interface
sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.15.1"
webview_flutter_wkwebview:
@@ -975,7 +975,7 @@ packages:
description:
name: webview_flutter_wkwebview
sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.25.1"
win32:
@@ -983,7 +983,7 @@ packages:
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories:
@@ -991,7 +991,7 @@ packages:
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
@@ -999,7 +999,7 @@ packages:
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
yaml:
@@ -1007,7 +1007,7 @@ packages:
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:

View File

@@ -1,7 +1,7 @@
name: mooknote
description: "app for tracking movies, books, and notes"
publish_to: 'none'
version: 0.1.9
version: 0.2.0
environment:
sdk: ^3.5.0