From 509d1720bc68cac97b1b1a5678a7a7ce268303bd Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Wed, 12 Aug 2026 22:39:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/book/book_detail_page.dart | 273 ++++++++++++++++++++++- lib/pages/game/game_detail_page.dart | 265 +++++++++++++++++++++- lib/pages/movies/movie_detail_page.dart | 282 +++++++++++++++++++++++- pubspec.yaml | 2 +- 4 files changed, 802 insertions(+), 20 deletions(-) diff --git a/lib/pages/book/book_detail_page.dart b/lib/pages/book/book_detail_page.dart index 7788d54..0a1254a 100644 --- a/lib/pages/book/book_detail_page.dart +++ b/lib/pages/book/book_detail_page.dart @@ -148,10 +148,11 @@ class _BookDetailPageState extends State { if (Breakpoint.isDesktop(context)) { return _buildDesktopStyle(book, colors); } - if (_detailStyle == 1) { - return _buildOverlayStyle(book, colors); - } - return _buildStandardStyle(book, colors); + return switch (_detailStyle) { + 1 => _buildOverlayStyle(book, colors), + 2 => _buildMinimalLayeredStyle(book, colors), + _ => _buildStandardStyle(book, colors), + }; } /// 桌面端左右分栏布局 @@ -972,6 +973,264 @@ class _BookDetailPageState extends State { ); } + /// 浅色极简层叠样式:封面卡片 + 浅色信息卡片层叠(无毛玻璃) + Widget _buildMinimalLayeredStyle(Book book, ColorScheme colors) { + final safeTop = MediaQuery.of(context).padding.top; + return Scaffold( + backgroundColor: colors.surface, + body: Stack( + children: [ + // 整体可滚动(封面卡片 + 内容一起滑动) + Padding( + padding: EdgeInsets.only(top: safeTop + 48), + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 120), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: _buildLayeredCover(book)), + const SizedBox(height: 20), + _buildLayeredHeader(book), + const SizedBox(height: 20), + _buildLayeredInfoRow('作者', book.authors.join(','), colors), + if (book.translators.isNotEmpty) + _buildLayeredInfoRow('译者', book.translators.join(','), colors), + if (book.isbn != null && book.isbn!.isNotEmpty) + _buildLayeredInfoRow('ISBN', book.isbn!, colors), + if (book.publisher != null && book.publisher!.isNotEmpty) + _buildLayeredInfoRow('出版社', book.publisher!, colors), + if (book.publishDate != null) + _buildLayeredInfoRow('出版时间', '${book.publishDate!.year}年${book.publishDate!.month.toString().padLeft(2, '0')}月', colors), + if (book.startDate != null || book.finishDate != null || book.readCount > 0) + _buildLayeredReadingDates(book), + if (book.genres.isNotEmpty) + _buildLayeredGenres(book), + CharacterPreviewSection( + characters: _characters, + onTap: _openCharacterSheet, + ), + WorkPeopleSection(workId: book.id, workType: 'book'), + if (book.summary != null && book.summary!.isNotEmpty) + _buildLayeredSummary(book), + const SizedBox(height: 20), + _buildLayeredExtraSections(book), + ], + ), + ), + ), + _buildStandardTopBar(book.title, colors), + Positioned(right: 16, bottom: 24, child: _buildFloatingActionButtons(book)), + ], + ), + ); + } + + /// 浅色极简:居中封面卡片 + Widget _buildLayeredCover(Book book) { + final colors = Theme.of(context).colorScheme; + final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty; + return Container( + width: 160, height: 230, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(16), + boxShadow: hasCover + ? [BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 20, offset: const Offset(0, 8))] + : null, + ), + clipBehavior: Clip.antiAlias, + child: hasCover + ? FadeInLocalImage(path: book.coverPath, fit: BoxFit.cover) + : Center(child: Icon(Icons.menu_book, size: 48, color: colors.onSurface.withValues(alpha: 0.25))), + ); + } + + /// 浅色极简:居中标题 + 评分/状态 + Widget _buildLayeredHeader(Book book) { + final colors = Theme.of(context).colorScheme; + return Column( + children: [ + Text(book.title, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3)), + if (book.alternateTitles.isNotEmpty) ...[ + const SizedBox(height: 6), + Text(book.alternateTitles.join(' / '), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4), height: 1.4)), + ], + _buildEpubProgressBar(book), + const SizedBox(height: 12), + Wrap(spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ + if (book.rating != null) ...[ + Icon(Icons.star, size: 20, color: colors.onSurface), + const SizedBox(width: 4), + Text(book.rating!.toStringAsFixed(1), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + ], + _buildStatusTag(book), + ]), + const SizedBox(height: 8), + Text('添加于 ${_formatDate(book.createdAt)}', + style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + ], + ); + } + + /// 浅色极简:信息卡片(作者/译者/ISBN/出版社/出版时间) + Widget _buildLayeredInfoRow(String label, String value, ColorScheme colors) { + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 0.5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 64, child: Text(label, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)))), + Expanded(child: Text(value, style: TextStyle(fontSize: 15, color: colors.onSurface, height: 1.5))), + ], + ), + ); + } + + /// 浅色极简:阅读日期/次数卡片 + Widget _buildLayeredReadingDates(Book book) { + final colors = Theme.of(context).colorScheme; + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 0.5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 64, child: Text('阅读日期', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)))), + Expanded( + child: Wrap(spacing: 12, runSpacing: 8, children: [ + if (book.startDate != null) _buildDateChip('开始', book.startDate!, false), + if (book.finishDate != null) _buildDateChip('读完', book.finishDate!, false), + if (book.readCount > 0) + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6)), + child: Text('共 ${book.readCount} 次', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.7))), + ), + ]), + ), + ], + ), + ); + } + + /// 浅色极简:类型卡片 + Widget _buildLayeredGenres(Book book) { + final colors = Theme.of(context).colorScheme; + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 0.5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 64, child: Text('类型', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)))), + Expanded(child: Wrap(spacing: 8, runSpacing: 8, + children: book.genres.map((g) => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16)), + child: Text(g, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))), + )).toList(), + )), + ], + ), + ); + } + + /// 浅色极简:简介卡片 + Widget _buildLayeredSummary(Book book) { + final colors = Theme.of(context).colorScheme; + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 0.5), + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Container(width: 4, height: 14, decoration: BoxDecoration(color: colors.onSurface, borderRadius: BorderRadius.circular(2))), + const SizedBox(width: 8), + Text('简介', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), + ]), + const SizedBox(height: 12), + Text(book.summary!, style: TextStyle(fontSize: 15, color: colors.onSurface, height: 1.8)), + ]), + ); + } + + /// 浅色极简:更多(书评/摘抄/句读/角色) + Widget _buildLayeredExtraSections(Book book) { + final colors = Theme.of(context).colorScheme; + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Container(width: 4, height: 16, decoration: BoxDecoration(color: colors.onSurface, borderRadius: BorderRadius.circular(2))), + const SizedBox(width: 8), + Text('更多', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), + ]), + const SizedBox(height: 12), + _buildExtraSectionItem( + icon: Icons.rate_review_outlined, + title: '书评', + subtitleFuture: context.read().getBookReviewCount(book.id), + emptyText: '暂无书评', + unit: '条书评', + onTap: () => _navigateToReviews(book), + ), + const SizedBox(height: 10), + _buildExtraSectionItem( + icon: Icons.format_quote_outlined, + title: '摘抄', + subtitleFuture: context.read().getBookExcerptCount(book.id), + emptyText: '暂无摘抄', + unit: '条摘抄', + onTap: () => _navigateToExcerpts(book), + ), + const SizedBox(height: 10), + _buildExtraSectionItem( + icon: Icons.highlight_outlined, + title: '句读', + subtitleFuture: _getEpubHighlightCount(book.id), + emptyText: '暂无句读', + unit: '条句读', + onTap: () => _navigateToEpubHighlights(book), + ), + const SizedBox(height: 10), + _buildExtraSectionItem( + icon: Icons.people_outline, + title: '角色', + subtitleFuture: context.read().getBookCharacterCount(book.id), + emptyText: '暂无角色', + unit: '个角色', + onTap: () => _navigateToCharacters(book), + ), + ]); + } + /// 叠层模式顶部:封面小图 + 标题/评分 Widget _buildOverlayHeader(Book book) { final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty; @@ -1310,9 +1569,9 @@ class _BookDetailPageState extends State { void _showStylePicker() { final colors = Theme.of(context).colorScheme; - const names = ['默认样式', '毛玻璃层叠']; - const icons = [Icons.article_outlined, Icons.blur_on_outlined]; - const subtitles = ['标准封面顶部布局', '封面背景 + 毛玻璃卡片']; + const names = ['默认样式', '毛玻璃层叠', '浅色极简']; + const icons = [Icons.article_outlined, Icons.blur_on_outlined, Icons.layers_outlined]; + const subtitles = ['标准封面顶部布局', '封面背景 + 毛玻璃卡片', '封面卡片 + 浅色信息卡片层叠']; appModalBottomSheet( context: context, backgroundColor: colors.surface, diff --git a/lib/pages/game/game_detail_page.dart b/lib/pages/game/game_detail_page.dart index 8e3dbdb..b2b21a7 100644 --- a/lib/pages/game/game_detail_page.dart +++ b/lib/pages/game/game_detail_page.dart @@ -170,9 +170,11 @@ class _GameDetailPageState extends State { if (Breakpoint.isDesktop(context)) { return _buildDesktopStyle(game, colors); } - return _detailStyle == 1 - ? _buildOverlayStyle(game, colors) - : _buildStandardStyle(game, colors); + return switch (_detailStyle) { + 1 => _buildOverlayStyle(game, colors), + 2 => _buildMinimalLayeredStyle(game, colors), + _ => _buildStandardStyle(game, colors), + }; } /// 桌面端左右分栏布局 @@ -1206,6 +1208,257 @@ class _GameDetailPageState extends State { ); } + /// 浅色极简层叠样式:封面卡片 + 浅色信息卡片层叠(无毛玻璃) + Widget _buildMinimalLayeredStyle(Game game, ColorScheme colors) { + final safeTop = MediaQuery.of(context).padding.top; + return Scaffold( + backgroundColor: colors.surface, + body: Stack( + children: [ + // 整体可滚动(封面卡片 + 内容一起滑动) + Padding( + padding: EdgeInsets.only(top: safeTop + 48), + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 120), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: _buildLayeredCover(game)), + const SizedBox(height: 20), + _buildLayeredHeader(game), + const SizedBox(height: 20), + if (game.platforms.isNotEmpty) + _buildLayeredInfoRow('平台', game.platforms.join('、'), colors), + if (game.versions.isNotEmpty) + _buildLayeredInfoRow('版本', game.versions.join('、'), colors), + if (game.genres.isNotEmpty) + _buildLayeredGenres(game), + if (game.developer.isNotEmpty) + _buildLayeredInfoRow('开发者', game.developer.join('、'), colors), + if (game.releaseDate != null) + _buildLayeredInfoRow('发售时间', _formatDate(game.releaseDate!), colors), + if (game.playTimeHours > 0 || game.playTimeMinutes > 0) + _buildLayeredInfoRow('游玩时长', '${game.playTimeHours}小时${game.playTimeMinutes}分钟', colors), + if (game.playCount > 0) + _buildLayeredInfoRow('游玩次数', '${game.playCount} 次', colors), + if (game.purchasePlatforms.isNotEmpty) + _buildLayeredInfoRow('购买平台', game.purchasePlatforms.join('、'), colors), + if (game.purchaseDate != null) + _buildLayeredInfoRow('购买时间', _formatDate(game.purchaseDate!), colors), + if (game.purchasePrice != null && game.purchasePrice!.isNotEmpty) + _buildLayeredInfoRow('购买价格', game.purchasePrice!, colors), + CharacterPreviewSection( + characters: _characters, + onTap: _openCharacterSheet, + ), + WorkPeopleSection(workId: game.id, workType: 'game'), + if (game.summary != null && game.summary!.isNotEmpty) + _buildLayeredSummary(game), + const SizedBox(height: 20), + _buildLayeredExtraSections(game), + ], + ), + ), + ), + Positioned( + top: 0, left: 0, right: 0, + child: Container( + padding: EdgeInsets.only(top: safeTop), + color: colors.surface, + child: SizedBox( + height: 48, + child: Row(children: [ + const SizedBox(width: 4), + IconButton( + icon: widget.embedded + ? Icon(Icons.arrow_back, color: colors.onSurface, size: 18) + : Icon(Icons.arrow_back_ios_new, color: colors.onSurface, size: 18), + onPressed: widget.embedded + ? () => context.read().selectGame(null) + : () => Navigator.pop(context), + ), + const SizedBox(width: 4), + Expanded( + child: Text(game.title, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface), + maxLines: 1, overflow: TextOverflow.ellipsis), + ), + IconButton( + icon: Icon(Icons.tune, color: colors.onSurface, size: 20), + tooltip: '切换样式', + onPressed: _showStylePicker, + ), + ]), + ), + ), + ), + Positioned(right: 16, bottom: 24, child: _buildFloatingActionButtons(game)), + ], + ), + ); + } + + /// 浅色极简:居中封面卡片 + Widget _buildLayeredCover(Game game) { + final colors = Theme.of(context).colorScheme; + final hasCover = game.coverPath != null && game.coverPath!.isNotEmpty; + return Container( + width: 160, height: 230, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(16), + boxShadow: hasCover + ? [BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 20, offset: const Offset(0, 8))] + : null, + ), + clipBehavior: Clip.antiAlias, + child: hasCover + ? FadeInLocalImage(path: game.coverPath, fit: BoxFit.cover) + : Center(child: Icon(Icons.sports_esports_outlined, size: 48, color: colors.onSurface.withValues(alpha: 0.25))), + ); + } + + /// 浅色极简:居中标题 + 评分/状态/分类 + Widget _buildLayeredHeader(Game game) { + final colors = Theme.of(context).colorScheme; + return Column( + children: [ + Text(game.title, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3)), + if (game.genres.isNotEmpty) ...[ + const SizedBox(height: 6), + Text(game.genres.join(' / '), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4), height: 1.4)), + ], + const SizedBox(height: 12), + Wrap(spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ + if (game.rating != null) ...[ + const Icon(Icons.star, size: 20, color: Colors.amber), + const SizedBox(width: 4), + Text(game.rating!.toStringAsFixed(1), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + ], + _buildStatusTag(game), + _buildCategoryTag(game), + ]), + ], + ); + } + + /// 浅色极简:信息卡片(平台/版本/开发者/发售时间等) + Widget _buildLayeredInfoRow(String label, String value, ColorScheme colors) { + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 0.5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 72, child: Text(label, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)))), + Expanded(child: Text(value, style: TextStyle(fontSize: 15, color: colors.onSurface, height: 1.5))), + ], + ), + ); + } + + /// 浅色极简:类型卡片 + Widget _buildLayeredGenres(Game game) { + final colors = Theme.of(context).colorScheme; + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 0.5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 72, child: Text('类型', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)))), + Expanded(child: Wrap(spacing: 8, runSpacing: 8, + children: game.genres.map((g) => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16)), + child: Text(g, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))), + )).toList(), + )), + ], + ), + ); + } + + /// 浅色极简:简介卡片 + Widget _buildLayeredSummary(Game game) { + final colors = Theme.of(context).colorScheme; + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 0.5), + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Container(width: 4, height: 14, decoration: BoxDecoration(color: colors.onSurface, borderRadius: BorderRadius.circular(2))), + const SizedBox(width: 8), + Text('游戏简介', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), + ]), + const SizedBox(height: 12), + Text(game.summary!, style: TextStyle(fontSize: 15, color: colors.onSurface, height: 1.8)), + ]), + ); + } + + /// 浅色极简:更多(评价/截图/角色) + Widget _buildLayeredExtraSections(Game game) { + final colors = Theme.of(context).colorScheme; + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Container(width: 4, height: 16, decoration: BoxDecoration(color: colors.onSurface, borderRadius: BorderRadius.circular(2))), + const SizedBox(width: 8), + Text('更多', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), + ]), + const SizedBox(height: 12), + _buildExtraSectionItem( + icon: Icons.rate_review_outlined, + title: '游戏评价', + subtitleFuture: context.read().getGameReviewCount(game.id), + emptyText: '暂无评价', + unit: '条评价', + onTap: () => _navigateToReviews(game), + ), + const SizedBox(height: 10), + _buildExtraSectionItem( + icon: Icons.photo_library_outlined, + title: '游戏截图', + subtitleFuture: context.read().getGameScreenshotCount(game.id), + emptyText: '暂无截图', + unit: '张截图', + onTap: () => _navigateToScreenshots(game), + ), + const SizedBox(height: 10), + _buildExtraSectionItem( + icon: Icons.people_outline, + title: '角色', + subtitleFuture: context.read().getGameCharacterCount(game.id), + emptyText: '暂无角色', + unit: '个角色', + onTap: () => _navigateToCharacters(game), + ), + ]); + } + Widget _buildOverlayHeader(Game game) { final hasCover = game.coverPath != null && game.coverPath!.isNotEmpty; return Row( @@ -1870,9 +2123,9 @@ class _GameDetailPageState extends State { void _showStylePicker() { final colors = Theme.of(context).colorScheme; final currentStyle = UserPrefs().detailPageStyle; - const names = ['默认样式', '毛玻璃层叠']; - const icons = [Icons.article_outlined, Icons.blur_on_outlined]; - const subtitles = ['标准封面顶部布局', '封面背景 + 毛玻璃卡片']; + const names = ['默认样式', '毛玻璃层叠', '浅色极简']; + const icons = [Icons.article_outlined, Icons.blur_on_outlined, Icons.layers_outlined]; + const subtitles = ['标准封面顶部布局', '封面背景 + 毛玻璃卡片', '封面卡片 + 浅色信息卡片层叠']; appModalBottomSheet( context: context, backgroundColor: colors.surface, diff --git a/lib/pages/movies/movie_detail_page.dart b/lib/pages/movies/movie_detail_page.dart index 0d1ed26..a0077e9 100644 --- a/lib/pages/movies/movie_detail_page.dart +++ b/lib/pages/movies/movie_detail_page.dart @@ -148,9 +148,11 @@ class _MovieDetailPageState extends State { if (Breakpoint.isDesktop(context)) { return _buildDesktopStyle(movie, colors); } - return _detailStyle == 1 - ? _buildOverlayStyle(movie, colors) - : _buildStandardStyle(movie, colors); + return switch (_detailStyle) { + 1 => _buildOverlayStyle(movie, colors), + 2 => _buildMinimalLayeredStyle(movie, colors), + _ => _buildStandardStyle(movie, colors), + }; } /// 桌面端左右分栏布局 @@ -1128,6 +1130,274 @@ class _MovieDetailPageState extends State { ); } + /// 浅色极简层叠样式:海报卡片 + 浅色信息卡片层叠(无毛玻璃) + Widget _buildMinimalLayeredStyle(Movie movie, ColorScheme colors) { + final safeTop = MediaQuery.of(context).padding.top; + return Scaffold( + backgroundColor: colors.surface, + body: Stack( + children: [ + // 整体可滚动(海报卡片 + 内容一起滑动) + Padding( + padding: EdgeInsets.only(top: safeTop + 48), + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 120), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 居中海报卡片 + Center(child: _buildLayeredPoster(movie)), + const SizedBox(height: 20), + _buildLayeredHeader(movie), + const SizedBox(height: 20), + if (movie.directors.isNotEmpty) + _buildLayeredInfoRow('导演', movie.directors.join(','), colors), + if (movie.writers.isNotEmpty) + _buildLayeredInfoRow('编剧', movie.writers.join(','), colors), + if (movie.actors.isNotEmpty) + _buildLayeredInfoRow('主演', movie.actors.join(','), colors), + if (movie.genres.isNotEmpty) + _buildLayeredGenres(movie), + CharacterPreviewSection( + characters: _characters, + onTap: _openCharacterSheet, + ), + WorkPeopleSection(workId: movie.id, workType: 'movie'), + if (movie.summary != null && movie.summary!.isNotEmpty) + _buildLayeredSummary(movie), + const SizedBox(height: 20), + _buildLayeredExtraSections(movie), + ], + ), + ), + ), + // 顶部导航栏 + Positioned( + top: 0, left: 0, right: 0, + child: Container( + padding: EdgeInsets.only(top: safeTop), + color: colors.surface, + child: SizedBox( + height: 48, + child: Row(children: [ + const SizedBox(width: 4), + IconButton( + icon: widget.embedded + ? Icon(Icons.arrow_back, color: colors.onSurface, size: 18) + : Icon(Icons.arrow_back_ios_new, color: colors.onSurface, size: 18), + onPressed: widget.embedded + ? () => context.read().selectMovie(null) + : () => Navigator.pop(context), + ), + const SizedBox(width: 4), + Expanded( + child: Text(movie.title, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface), + maxLines: 1, overflow: TextOverflow.ellipsis), + ), + _buildStyleButton(color: colors.onSurface), + ]), + ), + ), + ), + Positioned(right: 16, bottom: 24, child: _buildFloatingActionButtons(movie)), + ], + ), + ); + } + + /// 浅色极简:居中海报卡片 + Widget _buildLayeredPoster(Movie movie) { + final colors = Theme.of(context).colorScheme; + final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty; + return Container( + width: 160, height: 230, + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(16), + boxShadow: hasPoster + ? [BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 20, offset: const Offset(0, 8))] + : null, + ), + clipBehavior: Clip.antiAlias, + child: hasPoster + ? FadeInLocalImage(path: movie.posterPath, fit: BoxFit.cover) + : Center(child: Icon(Icons.movie_outlined, size: 48, color: colors.onSurface.withValues(alpha: 0.25))), + ); + } + + /// 浅色极简:居中标题 + 评分/状态/分类 + Widget _buildLayeredHeader(Movie movie) { + final colors = Theme.of(context).colorScheme; + return Column( + children: [ + Text(movie.title, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3)), + if (movie.alternateTitles.isNotEmpty) ...[ + const SizedBox(height: 6), + Text(movie.alternateTitles.join(' / '), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4), height: 1.4)), + ], + const SizedBox(height: 12), + Wrap(spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ + if (movie.rating != null) ...[ + Icon(Icons.star, size: 20, color: colors.onSurface), + const SizedBox(width: 4), + Text(movie.rating!.toStringAsFixed(1), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + ], + _buildStatusTag(movie), + _buildCategoryTag(movie), + ]), + if (movie.releaseDate != null) ...[ + const SizedBox(height: 8), + GestureDetector( + onTap: _toggleDateDisplay, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Text( + _showExactDate + ? '${movie.releaseDate!.year}年${movie.releaseDate!.month.toString().padLeft(2, '0')}月${movie.releaseDate!.day.toString().padLeft(2, '0')}日上映' + : '${movie.releaseDate!.year}年${movie.releaseDate!.month.toString().padLeft(2, '0')}月上映', + style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4)), + ), + const SizedBox(width: 4), + Icon(Icons.tune, size: 14, color: colors.onSurface.withValues(alpha: 0.2)), + ]), + ), + ], + if (movie.watchDate != null) ...[ + const SizedBox(height: 4), + Text('观看于 ${_formatDate(movie.watchDate!)}', + style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))), + ], + if (movie.watchCount > 0) ...[ + const SizedBox(height: 4), + Text('已观看 ${movie.watchCount} 次', + style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))), + ], + if (movie.duration > 0) ...[ + const SizedBox(height: 4), + Text(_formatDuration(movie.duration), + style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4))), + ], + ], + ); + } + + /// 浅色极简:信息卡片(导演/编剧/主演) + Widget _buildLayeredInfoRow(String label, String value, ColorScheme colors) { + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 0.5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 48, child: Text(label, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)))), + Expanded(child: Text(value, style: TextStyle(fontSize: 15, color: colors.onSurface, height: 1.5))), + ], + ), + ); + } + + /// 浅色极简:类型卡片 + Widget _buildLayeredGenres(Movie movie) { + final colors = Theme.of(context).colorScheme; + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 0.5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 48, child: Text('类型', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)))), + Expanded(child: Wrap(spacing: 8, runSpacing: 8, + children: movie.genres.map((g) => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16)), + child: Text(g, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))), + )).toList(), + )), + ], + ), + ); + } + + /// 浅色极简:简介卡片 + Widget _buildLayeredSummary(Movie movie) { + final colors = Theme.of(context).colorScheme; + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 0.5), + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Container(width: 4, height: 14, decoration: BoxDecoration(color: colors.onSurface, borderRadius: BorderRadius.circular(2))), + const SizedBox(width: 8), + Text('简介', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), + ]), + const SizedBox(height: 12), + Text(movie.summary!, style: TextStyle(fontSize: 15, color: colors.onSurface, height: 1.8)), + ]), + ); + } + + /// 浅色极简:更多(影评/海报墙/角色)卡片标题 + Widget _buildLayeredExtraSections(Movie movie) { + final colors = Theme.of(context).colorScheme; + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Container(width: 4, height: 16, decoration: BoxDecoration(color: colors.onSurface, borderRadius: BorderRadius.circular(2))), + const SizedBox(width: 8), + Text('更多', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), + ]), + const SizedBox(height: 12), + _buildExtraSectionItem( + icon: Icons.rate_review_outlined, + title: '影评', + subtitleFuture: context.read().getMovieReviewCount(movie.id), + emptyText: '暂无影评', + unit: '条影评', + onTap: () => _navigateToReviews(movie), + ), + const SizedBox(height: 10), + _buildExtraSectionItem( + icon: Icons.photo_library_outlined, + title: '海报墙', + subtitleFuture: context.read().getMoviePosterCount(movie.id), + emptyText: '暂无海报', + unit: '张海报', + onTap: () => _navigateToPosters(movie), + ), + const SizedBox(height: 10), + _buildExtraSectionItem( + icon: Icons.people_outline, + title: '角色', + subtitleFuture: context.read().getMovieCharacterCount(movie.id), + emptyText: '暂无角色', + unit: '个角色', + onTap: () => _navigateToCharacters(movie), + ), + ]); + } + /// 叠层模式:封面小图 + 标题/评分 Widget _buildOverlayHeader(Movie movie) { final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty; @@ -1195,9 +1465,9 @@ class _MovieDetailPageState extends State { void _showStylePicker() { final colors = Theme.of(context).colorScheme; - const names = ['默认样式', '毛玻璃层叠']; - const icons = [Icons.article_outlined, Icons.blur_on_outlined]; - const subtitles = ['标准封面顶部布局', '封面背景 + 毛玻璃卡片']; + const names = ['默认样式', '毛玻璃层叠', '浅色极简']; + const icons = [Icons.article_outlined, Icons.blur_on_outlined, Icons.layers_outlined]; + const subtitles = ['标准封面顶部布局', '封面背景 + 毛玻璃卡片', '海报卡片 + 浅色信息卡片层叠']; appModalBottomSheet( context: context, backgroundColor: colors.surface, diff --git a/pubspec.yaml b/pubspec.yaml index 2b88c84..58aa95d 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.3.1 +version: 0.3.2 environment: sdk: ^3.5.0