From 631c532269a0edaf1e6bc42bd6bd37032b948340 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Fri, 19 Jun 2026 15:50:19 +0800 Subject: [PATCH] =?UTF-8?q?=E9=95=BF=E6=8C=89=E5=9B=BE=E7=89=87=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E4=BD=8D=E7=BD=AE=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=AF=9B?= =?UTF-8?q?=E7=8E=BB=E7=92=83=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/book/book_detail_page.dart | 639 +++++++++++++++++++----- lib/pages/movies/movie_detail_page.dart | 629 ++++++++++++++++++----- lib/pages/statistics_page.dart | 111 ++-- lib/utils/user_prefs.dart | 12 + pubspec.lock | 246 ++++----- pubspec.yaml | 2 +- 6 files changed, 1250 insertions(+), 389 deletions(-) diff --git a/lib/pages/book/book_detail_page.dart b/lib/pages/book/book_detail_page.dart index ebe5653..c773378 100644 --- a/lib/pages/book/book_detail_page.dart +++ b/lib/pages/book/book_detail_page.dart @@ -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 { + late int _detailStyle; + final ValueNotifier _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 { .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 { ); } - 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( + 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 { ); } + /// 样式选择按钮 + 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 { } 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 { '作者', 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 { 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 { } 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 { '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 { 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 { } 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 { '出版社', 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 { 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 { } 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 { '出版时间', 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 { '${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 { } 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 { ); } + 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 { 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 { 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 { ); } + /// 叠层模式:书评、摘抄各自独立毛玻璃卡片 + 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().getBookReviewCount(book.id), + emptyText: '暂无书评', + unit: '条书评', + onTap: () => _navigateToReviews(book), + ), + const SizedBox(height: 12), + _buildFrostedExtraItem( + icon: Icons.format_quote_outlined, + title: '摘抄', + subtitleFuture: context.read().getBookExcerptCount(book.id), + emptyText: '暂无摘抄', + unit: '条摘抄', + onTap: () => _navigateToExcerpts(book), + ), + ], + ), + ); + } + + /// 毛玻璃书评/摘抄卡片 + Widget _buildFrostedExtraItem({ + required IconData icon, + required String title, + required Future 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( + 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, diff --git a/lib/pages/movies/movie_detail_page.dart b/lib/pages/movies/movie_detail_page.dart index efe7860..1b7da82 100644 --- a/lib/pages/movies/movie_detail_page.dart +++ b/lib/pages/movies/movie_detail_page.dart @@ -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 { late bool _showExactDate; + late int _detailStyle; + final ValueNotifier _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 { .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 { ); } + /// 毛玻璃层叠样式 + 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 { ); } - 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( + 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 { } 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 { '导演', 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 { 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 { } 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 { '编剧', 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 { 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 { } 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 { '主演', 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 { 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 { } 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 { 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 { ); } + 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 { 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 { 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 { ); } + /// 叠层模式:影评、海报墙各自独立毛玻璃卡片 + 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().getMovieReviewCount(movie.id), + emptyText: '暂无影评', + unit: '条影评', + onTap: () => _navigateToReviews(movie), + ), + const SizedBox(height: 12), + _buildFrostedExtraItem( + icon: Icons.photo_library_outlined, + title: '海报墙', + subtitleFuture: context.read().getMoviePosterCount(movie.id), + emptyText: '暂无海报', + unit: '张海报', + onTap: () => _navigateToPosters(movie), + ), + ], + ), + ); + } + + /// 毛玻璃影评/海报卡片 + Widget _buildFrostedExtraItem({ + required IconData icon, + required String title, + required Future 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( + 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, diff --git a/lib/pages/statistics_page.dart b/lib/pages/statistics_page.dart index 05abd72..a53fef5 100644 --- a/lib/pages/statistics_page.dart +++ b/lib/pages/statistics_page.dart @@ -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 { 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 { 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))), ], ), ); diff --git a/lib/utils/user_prefs.dart b/lib/utils/user_prefs.dart index 2a5ccc1..2e80979 100644 --- a/lib/utils/user_prefs.dart +++ b/lib/utils/user_prefs.dart @@ -55,6 +55,18 @@ class UserPrefs { bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true; Future setFirstLaunch(bool value) => prefs.setBool('isFirstLaunch', value); + // ========== 详情页样式 ========== + + /// 详情页展示样式: 0=标准(封面顶部), 1=叠层(封面+毛玻璃卡片) + int get detailPageStyle => prefs.getInt('detailPageStyle') ?? 0; + Future setDetailPageStyle(int value) => prefs.setInt('detailPageStyle', value); + + // ========== 封面位置 ========== + + /// 获取封面偏移量(-1.0 到 1.0,0 = 居中) + double getCoverOffset(String itemId) => prefs.getDouble('coverOffset_$itemId') ?? 0.0; + Future setCoverOffset(String itemId, double value) => prefs.setDouble('coverOffset_$itemId', value); + // ========== 主界面显示设置 ========== /// 是否启用底部导航栏滚动隐藏(默认开启) diff --git a/pubspec.lock b/pubspec.lock index 2bed419..8641285 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 83d41df..b3a86bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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