diff --git a/lib/pages/main_content_page.dart b/lib/pages/main_content_page.dart index 0159752..48b1119 100644 --- a/lib/pages/main_content_page.dart +++ b/lib/pages/main_content_page.dart @@ -67,9 +67,7 @@ class _MainContentPageState extends State { children: [ _buildAppBar(context), _buildTabBar(context), - Expanded( - child: _buildTabContent(), - ), + Expanded(child: _buildTabContent()), ], ); } @@ -83,14 +81,7 @@ class _MainContentPageState extends State { _buildCloudSyncButton(context), IconButton( icon: const Icon(Icons.search), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SearchPage(), - ), - ); - }, + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SearchPage())), ), ], ); @@ -99,268 +90,156 @@ class _MainContentPageState extends State { } Widget _buildCloudSyncButton(BuildContext context) { - final colors = Theme.of(context).colorScheme; - return PopupMenuButton( + return IconButton( icon: const Icon(Icons.cloud_sync_outlined), tooltip: '云备份', - offset: const Offset(0, 40), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - itemBuilder: (context) => [ - PopupMenuItem( - value: 'upload', - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.cloud_upload_outlined, - size: 18, - color: colors.onSurface.withValues(alpha: 0.6), - ), + onPressed: () => _showCloudSheet(context), + ); + } + + void _showCloudSheet(BuildContext context) async { + final colors = Theme.of(context).colorScheme; + final hasConfig = (await WebDAVService.instance.getConfig()) != null; + + if (!mounted) return; + showModalBottomSheet( + context: context, + backgroundColor: colors.surface, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))), + builder: (ctx) { + final bc = Theme.of(ctx).colorScheme; + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Center(child: Container( + width: 40, height: 4, margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration(color: bc.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2)), + )), + Text('云备份', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: bc.onSurface)), + const SizedBox(height: 20), + _cloudCard( + icon: Icons.cloud_upload_outlined, + title: '上传数据', + desc: hasConfig ? '将本地数据同步到云端' : '请先配置 WebDAV 服务器', + onTap: hasConfig ? () { Navigator.pop(ctx); _performSync(context, SyncDirection.upload); } : null, + enabled: hasConfig, + colors: bc, ), - const SizedBox(width: 12), - Text( - '上传数据', - style: TextStyle( - fontSize: 14, - color: colors.onSurface, - ), + const SizedBox(height: 12), + _cloudCard( + icon: Icons.cloud_download_outlined, + title: '下载数据', + desc: hasConfig ? '从云端恢复数据到本地' : '请先配置 WebDAV 服务器', + onTap: hasConfig ? () { Navigator.pop(ctx); _performSync(context, SyncDirection.download); } : null, + enabled: hasConfig, + colors: bc, ), - ], + const SizedBox(height: 12), + _cloudCard( + icon: Icons.settings_outlined, + title: 'WebDAV 设置', + desc: '配置服务器地址与认证信息', + onTap: () { Navigator.pop(ctx); Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())); }, + enabled: true, + colors: bc, + ), + ]), ), - ), - PopupMenuItem( - value: 'download', - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.cloud_download_outlined, - size: 18, - color: colors.onSurface.withValues(alpha: 0.6), - ), - ), - const SizedBox(width: 12), - Text( - '下载数据', - style: TextStyle( - fontSize: 14, - color: colors.onSurface, - ), - ), - ], - ), - ), - const PopupMenuDivider(), - PopupMenuItem( - value: 'settings', - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.settings_outlined, - size: 18, - color: colors.onSurface.withValues(alpha: 0.6), - ), - ), - const SizedBox(width: 12), - Text( - 'WebDAV设置', - style: TextStyle( - fontSize: 14, - color: colors.onSurface, - ), - ), - ], - ), - ), - ], - onSelected: (value) async { - switch (value) { - case 'upload': - await _performSync(context, SyncDirection.upload); - break; - case 'download': - await _performSync(context, SyncDirection.download); - break; - case 'settings': - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const WebDAVSyncPage(), - ), - ); - break; - } + ); }, ); } + Widget _cloudCard({required IconData icon, required String title, required String desc, required bool enabled, required VoidCallback? onTap, required ColorScheme colors}) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: enabled ? colors.primary.withValues(alpha: 0.04) : colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: enabled ? colors.primary.withValues(alpha: 0.1) : colors.outlineVariant, width: 0.5), + ), + child: Row(children: [ + Container( + width: 48, height: 48, + decoration: BoxDecoration( + color: enabled ? colors.primary.withValues(alpha: 0.08) : colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, size: 24, color: enabled ? colors.primary : colors.onSurface.withValues(alpha: 0.18)), + ), + const SizedBox(width: 16), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: enabled ? colors.onSurface : colors.onSurface.withValues(alpha: 0.25))), + const SizedBox(height: 2), + Text(desc, style: TextStyle(fontSize: 12, color: enabled ? colors.onSurface.withValues(alpha: 0.4) : colors.onSurface.withValues(alpha: 0.2))), + ])), + Icon(Icons.chevron_right, size: 20, color: enabled ? colors.onSurface.withValues(alpha: 0.15) : colors.onSurface.withValues(alpha: 0.08)), + ]), + ), + ); + } + Future _performSync(BuildContext context, SyncDirection direction) async { final colors = Theme.of(context).colorScheme; final config = await WebDAVService.instance.getConfig(); if (config == null) { - if (context.mounted) { - _showResultDialog( - context, - title: '同步失败', - message: '请先配置 WebDAV 服务器', - isSuccess: false, - ); - } + if (context.mounted) _showResultDialog(context, title: '同步失败', message: '请先配置 WebDAV 服务器', isSuccess: false); return; } - if (context.mounted) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => Center( - child: CircularProgressIndicator( - color: colors.primary, - ), - ), - ); + showDialog(context: context, barrierDismissible: false, builder: (_) => Center(child: CircularProgressIndicator(color: colors.primary))); } - final result = await WebDAVService.instance.syncData(direction: direction); - - if (context.mounted) { - Navigator.pop(context); - } - + if (context.mounted) Navigator.pop(context); if (result.success && result.needReload && context.mounted) { final provider = context.read(); await provider.loadMovies(); await provider.loadBooks(); await provider.loadNotes(); } - if (context.mounted) { - final isSuccess = result.success; - final message = result.message.isNotEmpty ? result.message : (isSuccess ? '同步成功' : '同步失败'); - - _showResultDialog( - context, - title: isSuccess ? '同步成功' : '同步失败', - message: message, - isSuccess: isSuccess, - details: { - 'uploaded': result.uploadedFiles + result.uploadedImages, - 'downloaded': result.downloadedFiles + result.downloadedImages, - }, + _showResultDialog(context, + title: result.success ? '同步成功' : '同步失败', + message: result.message.isNotEmpty ? result.message : (result.success ? '同步成功' : '同步失败'), + isSuccess: result.success, + details: {'uploaded': result.uploadedFiles + result.uploadedImages, 'downloaded': result.downloadedFiles + result.downloadedImages}, ); } } - void _showResultDialog( - BuildContext context, { - required String title, - required String message, - required bool isSuccess, - Map? details, - }) { + void _showResultDialog(BuildContext context, {required String title, required String message, required bool isSuccess, Map? details}) { final colors = Theme.of(context).colorScheme; showDialog( context: context, - builder: (context) => AlertDialog( - backgroundColor: colors.surface, - elevation: 0, + builder: (_) => AlertDialog( + backgroundColor: colors.surface, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: isSuccess ? const Color(0xFFE8F5E9) : const Color(0xFFFFEBEE), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - isSuccess ? Icons.check_circle : Icons.error, - color: isSuccess ? const Color(0xFF4CAF50) : const Color(0xFFE57373), - size: 24, - ), - ), - const SizedBox(width: 12), - Text( - title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: colors.onSurface, - ), - ), + title: Row(children: [ + Container( + width: 40, height: 40, + decoration: BoxDecoration(color: isSuccess ? const Color(0xFFE8F5E9) : const Color(0xFFFFEBEE), borderRadius: BorderRadius.circular(10)), + child: Icon(isSuccess ? Icons.check_circle : Icons.error, color: isSuccess ? const Color(0xFF4CAF50) : const Color(0xFFE57373), size: 24), + ), + const SizedBox(width: 12), + Text(title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + ]), + content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(message, style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)), + if (details != null) ...[ + const SizedBox(height: 16), + Container(padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(8)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (details['uploaded'] != null) _detailRow('上传文件', '${details['uploaded']} 个', colors), + if (details['downloaded'] != null) _detailRow('下载文件', '${details['downloaded']} 个', colors), + ])), ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - message, - style: TextStyle( - fontSize: 14, - color: colors.onSurface.withValues(alpha: 0.6), - height: 1.5, - ), - ), - if (details != null) ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (details['uploaded'] != null) - _buildDetailRow('上传文件', '${details['uploaded']} 个'), - if (details['downloaded'] != null) - _buildDetailRow('下载文件', '${details['downloaded']} 个'), - if (details['conflicts'] != null) - _buildDetailRow('冲突处理', '${details['conflicts']} 个'), - if (details['errors'] != null && details['errors'] > 0) - _buildDetailRow('错误', '${details['errors']} 个', isError: true), - ], - ), - ), - ], - ], - ), + ]), actions: [ ElevatedButton( onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: colors.primary, - foregroundColor: colors.onPrimary, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - ), + style: ElevatedButton.styleFrom(backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12)), child: const Text('确定'), ), ], @@ -369,56 +248,20 @@ class _MainContentPageState extends State { ); } - Widget _buildDetailRow(String label, String value, {bool isError = false}) { - final colors = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: TextStyle( - fontSize: 13, - color: colors.onSurface.withValues(alpha: 0.4), - ), - ), - Text( - value, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: isError ? const Color(0xFFE57373) : colors.onSurface, - ), - ), - ], - ), - ); - } + Widget _detailRow(String label, String value, ColorScheme colors) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(label, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))), + Text(value, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), + ]), + ); String _getAppBarTitle(AppProvider provider) { switch (provider.mainTabIndex) { - case 0: - return '影视'; - case 1: - return '阅读'; - case 2: - return '笔记'; - default: - return 'MookNote'; - } - } - - IconData _getTabIcon(String label) { - switch (label) { - case '影视': - return Icons.movie_outlined; - case '阅读': - return Icons.menu_book_outlined; - case '笔记': - return Icons.notes; - default: - return Icons.circle; + case 0: return '影视'; + case 1: return '阅读'; + case 2: return '笔记'; + default: return 'MookNote'; } } @@ -427,88 +270,65 @@ class _MainContentPageState extends State { builder: (context, provider, child) { final colors = Theme.of(context).colorScheme; final tabs = _enabledTabs; - final currentEnabledIndex = _mapToEnabledTabIndex(provider.mainTabIndex); - final safeIndex = currentEnabledIndex < tabs.length ? currentEnabledIndex : 0; + final safeIndex = _mapToEnabledTabIndex(provider.mainTabIndex).clamp(0, tabs.length - 1); return Container( - decoration: BoxDecoration( - color: colors.surface, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, top: 14), - child: Row( - children: tabs.asMap().entries.map((entry) { - final index = entry.key; - final tab = entry.value; - final isSelected = index == safeIndex; - return Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => provider.setMainTabIndex(tab.originalIndex), - child: Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _getTabIcon(tab.label), - size: 22, - color: isSelected ? colors.primary : colors.onSurface.withValues(alpha: 0.35), - ), - const SizedBox(height: 6), - Text( - tab.label, - style: TextStyle( - fontSize: 13, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, - color: isSelected ? colors.primary : colors.onSurface.withValues(alpha: 0.35), - ), - ), - ], - ), + color: colors.surface, + padding: const EdgeInsets.only(top: 4), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: tabs.asMap().entries.map((entry) { + final idx = entry.key; + final tab = entry.value; + final selected = idx == safeIndex; + return Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => provider.setMainTabIndex(tab.originalIndex), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _tabIcon(tab.label), + size: 18, + color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.3), + ), + const SizedBox(width: 5), + Text(tab.label, textAlign: TextAlign.center, style: TextStyle( + fontSize: 15, + fontWeight: selected ? FontWeight.w700 : FontWeight.w500, + color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.3), + )), + ], ), ), - ); - }).toList(), - ), + ), + ); + }).toList(), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: LayoutBuilder( - builder: (context, constraints) { - final indicatorWidth = 24.0; - final tabWidth = constraints.maxWidth / tabs.length; - final indicatorLeft = safeIndex * tabWidth + (tabWidth - indicatorWidth) / 2; - return SizedBox( - height: 3, - child: Stack( - children: [ - AnimatedPositioned( - duration: const Duration(milliseconds: 600), - curve: Curves.easeInOut, - left: indicatorLeft, - top: 0, - child: Container( - width: indicatorWidth, - height: 3, - decoration: BoxDecoration( - color: colors.primary, - borderRadius: BorderRadius.circular(1.5), - ), - ), - ), - ], - ), - ); - }, - ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: LayoutBuilder( + builder: (context, constraints) { + final tabWidth = tabs.isNotEmpty ? constraints.maxWidth / tabs.length : 0.0; + return SizedBox(height: 2.5, child: Stack(children: [ + AnimatedPositioned( + duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, + left: safeIndex * tabWidth, top: 0, + width: tabWidth, + child: Container(height: 2.5, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(2))), + ), + ])); + }, ), - Divider(height: 0.5, thickness: 0.5, color: colors.outline), - ], - ), + ), + ]), ); }, ); @@ -519,97 +339,45 @@ class _MainContentPageState extends State { builder: (context, provider, child) { final tabs = _enabledTabs; _TabItem? currentTab; - for (final tab in tabs) { - if (tab.originalIndex == provider.mainTabIndex) { - currentTab = tab; - break; - } - } + for (final tab in tabs) { if (tab.originalIndex == provider.mainTabIndex) { currentTab = tab; break; } } if (currentTab == null && tabs.isNotEmpty) { currentTab = tabs.first; - WidgetsBinding.instance.addPostFrameCallback((_) { - provider.setMainTabIndex(currentTab!.originalIndex); - }); + WidgetsBinding.instance.addPostFrameCallback((_) => provider.setMainTabIndex(currentTab!.originalIndex)); } - - if (currentTab == null) { - return const Center(child: Text('请至少启用一个标签页')); - } - + if (currentTab == null) return const Center(child: Text('请至少启用一个标签页')); switch (currentTab.originalIndex) { - case 0: - return const MovieTabPage(); - case 1: - return const BookTabPage(); - case 2: - return const NoteTabPage(); - default: - return const MovieTabPage(); + case 0: return const MovieTabPage(); + case 1: return const BookTabPage(); + case 2: return const NoteTabPage(); + default: return const MovieTabPage(); } }, ); } + IconData _tabIcon(String label) { + switch (label) { + case '影视': return Icons.movie_outlined; + case '阅读': return Icons.menu_book_outlined; + case '笔记': return Icons.note_outlined; + default: return Icons.circle; + } + } + void _showAddDialog(BuildContext context, AppProvider provider) { final colors = Theme.of(context).colorScheme; showModalBottomSheet( - context: context, - backgroundColor: colors.surface, + context: context, backgroundColor: colors.surface, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - builder: (BuildContext context) { - return SafeArea( - child: Wrap( - children: [ - ListTile( - leading: Icon(Icons.movie, color: colors.onSurface), - title: const Text('添加观影'), - onTap: () { - Navigator.pop(context); - final statusMap = { - 0: 'watched', - 1: 'watching', - 2: 'want_to_watch', - }; - final currentStatus = statusMap[provider.movieStatusIndex] ?? 'want_to_watch'; - Navigator.pushNamed( - context, - '/movie-form', - arguments: {'initialStatus': currentStatus}, - ); - }, - ), - Divider(height: 0.5, indent: 56, color: colors.outlineVariant), - ListTile( - leading: Icon(Icons.menu_book, color: colors.onSurface), - title: const Text('添加阅读'), - onTap: () { - Navigator.pop(context); - final statusMap = { - 0: 'read', - 1: 'reading', - 2: 'want_to_read', - }; - final currentStatus = statusMap[provider.bookStatusIndex] ?? 'want_to_read'; - Navigator.pushNamed( - context, - '/book-form', - arguments: {'initialStatus': currentStatus}, - ); - }, - ), - Divider(height: 0.5, indent: 56, color: colors.outlineVariant), - ListTile( - leading: Icon(Icons.note, color: colors.onSurface), - title: const Text('添加笔记'), - onTap: () { - Navigator.pop(context); - Navigator.pushNamed(context, '/note-form'); - }, - ), - ], - ), - ); - }, + builder: (_) => SafeArea( + child: Wrap(children: [ + ListTile(leading: Icon(Icons.movie, color: colors.onSurface), title: const Text('添加观影'), onTap: () { Navigator.pop(context); Navigator.pushNamed(context, '/movie-form', arguments: {'initialStatus': ['watched', 'watching', 'want_to_watch'][provider.movieStatusIndex]}); }), + Divider(height: 0.5, indent: 56, color: colors.outlineVariant), + ListTile(leading: Icon(Icons.menu_book, color: colors.onSurface), title: const Text('添加阅读'), onTap: () { Navigator.pop(context); Navigator.pushNamed(context, '/book-form', arguments: {'initialStatus': ['read', 'reading', 'want_to_read'][provider.bookStatusIndex]}); }), + Divider(height: 0.5, indent: 56, color: colors.outlineVariant), + ListTile(leading: Icon(Icons.note, color: colors.onSurface), title: const Text('添加笔记'), onTap: () { Navigator.pop(context); Navigator.pushNamed(context, '/note-form'); }), + ]), + ), ); } } @@ -617,6 +385,5 @@ class _MainContentPageState extends State { class _TabItem { final String label; final int originalIndex; - _TabItem(this.label, this.originalIndex); } diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index a47786e..a26c89d 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -8,7 +8,7 @@ import 'movies/movie_detail_page.dart'; import 'book/book_detail_page.dart'; import 'note/note_detail_page.dart'; -/// 搜索页面 - 统一搜索影视/书籍/笔记 +/// 搜索页面 class SearchPage extends StatefulWidget { const SearchPage({super.key}); @@ -32,9 +32,7 @@ class _SearchPageState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _focusNode.requestFocus(); - }); + WidgetsBinding.instance.addPostFrameCallback((_) => _focusNode.requestFocus()); } @override @@ -53,13 +51,9 @@ class _SearchPageState extends State { void _performSearch() { final keyword = _searchController.text.trim(); if (keyword.isEmpty) { - setState(() { - _results = []; - _hasSearched = false; - }); + setState(() { _results = []; _hasSearched = false; }); return; } - final provider = context.read(); final lowerKeyword = keyword.toLowerCase(); final results = <_SearchResult>[]; @@ -68,36 +62,35 @@ class _SearchPageState extends State { for (final movie in provider.movies.where((m) => !m.isDeleted)) { if (movie.title.toLowerCase().contains(lowerKeyword) || movie.alternateTitles.any((t) => t.toLowerCase().contains(lowerKeyword)) || - (movie.summary?.toLowerCase().contains(lowerKeyword) ?? false)) { + (movie.summary?.toLowerCase().contains(lowerKeyword) ?? false) || + movie.genres.any((g) => g.toLowerCase().contains(lowerKeyword))) { results.add(_SearchResult(type: 'movie', data: movie)); } } } - if (_showBooks) { for (final book in provider.books.where((b) => !b.isDeleted)) { if (book.title.toLowerCase().contains(lowerKeyword) || book.alternateTitles.any((t) => t.toLowerCase().contains(lowerKeyword)) || - (book.summary?.toLowerCase().contains(lowerKeyword) ?? false)) { + (book.summary?.toLowerCase().contains(lowerKeyword) ?? false) || + book.authors.any((a) => a.toLowerCase().contains(lowerKeyword))) { results.add(_SearchResult(type: 'book', data: book)); } } } - if (_showNotes) { for (final note in provider.notes.where((n) => !n.isDeleted)) { - if (note.content.toLowerCase().contains(lowerKeyword)) { + if (note.content.toLowerCase().contains(lowerKeyword) || + note.tags.any((t) => t.toLowerCase().contains(lowerKeyword))) { results.add(_SearchResult(type: 'note', data: note)); } } } - - setState(() { - _results = results; - _hasSearched = true; - }); + setState(() { _results = results; _hasSearched = true; }); } + // ═══════════════════════════════════════════════════════════════════ + @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; @@ -106,132 +99,96 @@ class _SearchPageState extends State { appBar: AppBar( title: const Text('搜索'), elevation: 0, + scrolledUnderElevation: 0, ), - body: Column( - children: [ - _buildSearchBar(), - _buildFilterRow(), - Expanded( - child: _hasSearched - ? _results.isEmpty ? _buildEmptyState() : _buildResultList() - : _buildInitialState(), - ), - ], - ), + body: Column(children: [ + _buildSearchBar(), + _buildFilterRow(), + Expanded( + child: _hasSearched + ? _results.isEmpty ? _buildEmptyState() : _buildResultList() + : _buildInitialState(), + ), + ]), ); } Widget _buildSearchBar() { final colors = Theme.of(context).colorScheme; - return Container( - padding: const EdgeInsets.fromLTRB(20, 12, 20, 4), + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: TextField( controller: _searchController, focusNode: _focusNode, style: TextStyle(fontSize: 15, color: colors.onSurface), decoration: InputDecoration( - hintText: '搜索标题、别名、内容...', - hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.35), fontSize: 15), - prefixIcon: Padding( - padding: const EdgeInsets.only(left: 12, right: 8), - child: Icon(Icons.search, color: colors.onSurface, size: 22), - ), - prefixIconConstraints: const BoxConstraints(minWidth: 42, minHeight: 42), + hintText: '搜索标题、作者、标签...', + hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.3), fontSize: 15), + prefixIcon: Icon(Icons.search, color: colors.onSurface.withValues(alpha: 0.4), size: 22), suffixIcon: _searchController.text.isNotEmpty ? GestureDetector( - onTap: () { - _searchController.clear(); - _scheduleSearch(); - _focusNode.requestFocus(); - }, + onTap: () { _searchController.clear(); setState(() {}); _scheduleSearch(); _focusNode.requestFocus(); }, child: Container( - margin: const EdgeInsets.only(right: 4), - width: 28, - height: 28, - decoration: BoxDecoration( - color: colors.outline, - borderRadius: BorderRadius.circular(14), - ), - child: Icon(Icons.close, color: colors.onSurface.withValues(alpha: 0.6), size: 16), + margin: const EdgeInsets.all(8), + decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(14)), + child: Icon(Icons.close, color: colors.onSurface.withValues(alpha: 0.5), size: 16), ), ) : null, - filled: true, - fillColor: colors.surfaceContainerHigh, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide(color: colors.primary, width: 1.5), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + filled: true, fillColor: colors.surfaceContainerHighest, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide(color: colors.primary, width: 1.5)), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), - onSubmitted: (_) { - _debounce?.cancel(); - _performSearch(); - }, - onChanged: (_) { - _debounce?.cancel(); - setState(() {}); - _scheduleSearch(); - }, + onSubmitted: (_) { _debounce?.cancel(); _performSearch(); }, + onChanged: (_) { _debounce?.cancel(); setState(() {}); _scheduleSearch(); }, ), ); } Widget _buildFilterRow() { + final colors = Theme.of(context).colorScheme; + final keyword = _searchController.text.trim(); + final provider = context.read(); + int movieCount = 0, bookCount = 0, noteCount = 0; + if (keyword.isNotEmpty) { + final kw = keyword.toLowerCase(); + movieCount = provider.movies.where((m) => !m.isDeleted && (m.title.toLowerCase().contains(kw) || m.alternateTitles.any((t) => t.toLowerCase().contains(kw)) || (m.summary?.toLowerCase().contains(kw) ?? false) || m.genres.any((g) => g.toLowerCase().contains(kw)))).length; + bookCount = provider.books.where((b) => !b.isDeleted && (b.title.toLowerCase().contains(kw) || b.alternateTitles.any((t) => t.toLowerCase().contains(kw)) || (b.summary?.toLowerCase().contains(kw) ?? false) || b.authors.any((a) => a.toLowerCase().contains(kw)))).length; + noteCount = provider.notes.where((n) => !n.isDeleted && (n.content.toLowerCase().contains(kw) || n.tags.any((t) => t.toLowerCase().contains(kw)))).length; + } + return Padding( - padding: const EdgeInsets.fromLTRB(20, 6, 20, 12), - child: Row( - children: [ - _buildTypeChip('影视', Icons.movie_outlined, _showMovies, (v) { - setState(() { _showMovies = v; _performSearch(); }); - }), - const SizedBox(width: 8), - _buildTypeChip('书籍', Icons.menu_book_outlined, _showBooks, (v) { - setState(() { _showBooks = v; _performSearch(); }); - }), - const SizedBox(width: 8), - _buildTypeChip('笔记', Icons.note_outlined, _showNotes, (v) { - setState(() { _showNotes = v; _performSearch(); }); - }), - ], - ), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Row(children: [ + _filterChip('影视', Icons.movie_outlined, _showMovies, movieCount, () { setState(() { _showMovies = !_showMovies; _performSearch(); }); }), + const SizedBox(width: 8), + _filterChip('书籍', Icons.menu_book_outlined, _showBooks, bookCount, () { setState(() { _showBooks = !_showBooks; _performSearch(); }); }), + const SizedBox(width: 8), + _filterChip('笔记', Icons.note_outlined, _showNotes, noteCount, () { setState(() { _showNotes = !_showNotes; _performSearch(); }); }), + ]), ); } - Widget _buildTypeChip(String label, IconData icon, bool selected, ValueChanged onChanged) { + Widget _filterChip(String label, IconData icon, bool selected, int count, VoidCallback onTap) { final colors = Theme.of(context).colorScheme; + final showCount = _searchController.text.trim().isNotEmpty; return GestureDetector( - onTap: () => onChanged(!selected), + onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), decoration: BoxDecoration( color: selected ? colors.primary : colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 14, color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5)), - const SizedBox(width: 5), - Text( - label, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5), - ), - ), - ], - ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(icon, size: 14, color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.4)), + const SizedBox(width: 5), + Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.4))), + if (showCount) ...[const SizedBox(width: 4), Text('$count', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: selected ? colors.onPrimary.withValues(alpha: 0.7) : colors.onSurface.withValues(alpha: 0.25)))], + ]), ), ); } @@ -239,274 +196,214 @@ class _SearchPageState extends State { Widget _buildInitialState() { final colors = Theme.of(context).colorScheme; return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 88, - height: 88, - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(24), - ), - child: Icon(Icons.search_rounded, size: 44, color: colors.onSurface.withValues(alpha: 0.2)), - ), - const SizedBox(height: 24), - Text('输入关键词搜索', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))), - const SizedBox(height: 6), - Text('可同时筛选影视、书籍、笔记', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25))), - ], - ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Container( + width: 80, height: 80, + decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)), + child: Icon(Icons.search_rounded, size: 40, color: colors.onSurface.withValues(alpha: 0.2)), + ), + const SizedBox(height: 20), + Text('输入关键词搜索', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.35))), + const SizedBox(height: 4), + Text('支持标题、作者、标签、类型', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.2))), + ]), ); } Widget _buildEmptyState() { final colors = Theme.of(context).colorScheme; return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 88, - height: 88, - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(24), - ), - child: Icon(Icons.search_off_rounded, size: 44, color: colors.onSurface.withValues(alpha: 0.2)), - ), - const SizedBox(height: 24), - Text('未找到相关内容', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.4))), - const SizedBox(height: 6), - Text('换个关键词试试', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.25))), - ], - ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Container( + width: 80, height: 80, + decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)), + child: Icon(Icons.search_off_rounded, size: 40, color: colors.onSurface.withValues(alpha: 0.2)), + ), + const SizedBox(height: 20), + Text('未找到相关内容', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.35))), + const SizedBox(height: 4), + Text('换个关键词试试', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.2))), + ]), ); } Widget _buildResultList() { - return ListView.builder( - padding: const EdgeInsets.fromLTRB(20, 4, 20, 24), - itemCount: _results.length, - itemBuilder: (context, index) { - final item = _results[index]; - switch (item.type) { - case 'movie': - return _buildMovieItem(item.data as Movie); - case 'book': - return _buildBookItem(item.data as Book); - case 'note': - return _buildNoteItem(item.data as Note); - default: - return const SizedBox.shrink(); - } - }, - ); + final colors = Theme.of(context).colorScheme; + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 4, 20, 8), + child: Text('找到 ${_results.length} 条结果', + style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))), + ), + Expanded( + child: ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + itemCount: _results.length, + itemBuilder: (context, index) { + final item = _results[index]; + switch (item.type) { + case 'movie': return _buildMovieItem(item.data as Movie); + case 'book': return _buildBookItem(item.data as Book); + case 'note': return _buildNoteItem(item.data as Note); + default: return const SizedBox.shrink(); + } + }, + ), + ), + ]); } - // ─── 影视结果项 ────────────────────────────────────────────────────── - Widget _buildMovieItem(Movie movie) { final colors = Theme.of(context).colorScheme; return GestureDetector( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => MovieDetailPage(movie: movie))), child: Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - ), - child: Row( - children: [ - _buildPosterThumb(movie.posterPath, Icons.movie_outlined), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - _typeBadge('影视', const Color(0xFF4A90D9)), - const Spacer(), - _statusBadge(movie.status), - ], - ), - const SizedBox(height: 8), - Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), - if (movie.alternateTitles.isNotEmpty) ...[ - const SizedBox(height: 3), - Text(movie.alternateTitles.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))), - ], + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(12)), + child: Row(children: [ + _posterThumb(movie.posterPath, Icons.movie_outlined), + const SizedBox(width: 12), + Expanded( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + _typeBadge('影视'), + const Spacer(), + _statusBadge(movie.status, colors), + ]), + const SizedBox(height: 6), + Text(movie.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), + const SizedBox(height: 4), + Row(children: [ + if (movie.rating != null) ...[ + Icon(Icons.star, size: 13, color: const Color(0xFFFFB800)), + const SizedBox(width: 2), + Text('${movie.rating}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: const Color(0xFFFFB800))), + const SizedBox(width: 8), ], - ), - ), - const SizedBox(width: 8), - Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20), - ], - ), + if (movie.genres.isNotEmpty) + Expanded(child: Text(movie.genres.take(2).join(' · '), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35)))), + ]), + ]), + ), + const SizedBox(width: 4), + Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.15), size: 18), + ]), ), ); } - // ─── 书籍结果项 ────────────────────────────────────────────────────── - Widget _buildBookItem(Book book) { final colors = Theme.of(context).colorScheme; return GestureDetector( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => BookDetailPage(book: book))), child: Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - ), - child: Row( - children: [ - _buildPosterThumb(book.coverPath, Icons.menu_book_outlined), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - _typeBadge('书籍', const Color(0xFF7E57C2)), - const Spacer(), - _bookStatusBadge(book.status), - ], - ), - const SizedBox(height: 8), - Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), - if (book.authors.isNotEmpty) ...[ - const SizedBox(height: 3), - Text(book.authors.take(2).join('、'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))), - ], + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(12)), + child: Row(children: [ + _posterThumb(book.coverPath, Icons.menu_book_outlined), + const SizedBox(width: 12), + Expanded( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + _typeBadge('书籍'), + const Spacer(), + _statusBadge(book.status, colors), + ]), + const SizedBox(height: 6), + Text(book.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), + const SizedBox(height: 4), + Row(children: [ + if (book.rating != null) ...[ + Icon(Icons.star, size: 13, color: const Color(0xFFFFB800)), + const SizedBox(width: 2), + Text('${book.rating}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: const Color(0xFFFFB800))), + const SizedBox(width: 8), ], - ), - ), - const SizedBox(width: 8), - Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20), - ], - ), + if (book.authors.isNotEmpty) + Expanded(child: Text(book.authors.take(2).join(' · '), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35)))), + ]), + ]), + ), + const SizedBox(width: 4), + Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.15), size: 18), + ]), ), ); } - // ─── 笔记结果项 ────────────────────────────────────────────────────── - Widget _buildNoteItem(Note note) { final colors = Theme.of(context).colorScheme; + final summary = note.summary.trim().isEmpty ? '(无内容)' : note.summary.trim(); return GestureDetector( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => NoteDetailPage(note: note))), child: Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - _typeBadge('笔记', const Color(0xFF66BB6A)), - const Spacer(), - Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20), + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(12)), + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.only(top: 2), + child: _typeBadge('笔记'), + ), + const SizedBox(width: 10), + Expanded( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(summary, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 14, color: colors.onSurface, height: 1.5)), + if (note.tags.isNotEmpty) ...[ + const SizedBox(height: 8), + Wrap(spacing: 6, runSpacing: 4, children: note.tags.map((t) => Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(4)), + child: Text(t, style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4))), + )).toList()), ], - ), - const SizedBox(height: 10), - Text( - note.summary.trim().isEmpty ? '(无内容)' : note.summary.trim(), - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75), height: 1.6), - ), - if (note.tags.isNotEmpty) ...[ - const SizedBox(height: 10), - Wrap( - spacing: 6, - runSpacing: 6, - children: note.tags.map((tag) => Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: colors.surface, - borderRadius: BorderRadius.circular(6), - ), - child: Text(tag, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.5))), - )).toList(), - ), - ], - ], - ), + ]), + ), + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.only(top: 2), + child: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.15), size: 18), + ), + ]), ), ); } - // ─── 通用组件 ──────────────────────────────────────────────────────── - - Widget _buildPosterThumb(String? path, IconData fallback) { + Widget _posterThumb(String? path, IconData fallback) { final colors = Theme.of(context).colorScheme; return Container( - width: 48, - height: 64, - decoration: BoxDecoration( - color: colors.outlineVariant, - borderRadius: BorderRadius.circular(8), - ), + width: 44, height: 58, + decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(6)), clipBehavior: Clip.antiAlias, child: path != null && path.isNotEmpty - ? Image.file(File(path), fit: BoxFit.cover, - errorBuilder: (_, __, ___) => Icon(fallback, size: 22, color: colors.onSurface.withValues(alpha: 0.25))) - : Icon(fallback, size: 22, color: colors.onSurface.withValues(alpha: 0.25)), + ? Image.file(File(path), fit: BoxFit.cover, errorBuilder: (_, __, ___) => Icon(fallback, size: 20, color: colors.onSurface.withValues(alpha: 0.25))) + : Icon(fallback, size: 20, color: colors.onSurface.withValues(alpha: 0.25)), ); } - Widget _typeBadge(String label, Color color) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(5), - ), - child: Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color)), - ); - } - - Widget _statusBadge(String status) { + Widget _typeBadge(String label) { final colors = Theme.of(context).colorScheme; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration(color: colors.primary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(4)), + child: Text(label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.primary)), + ); + } + + Widget _statusBadge(String status, ColorScheme colors) { final (label, bg, fg) = switch (status) { - 'watched' => ('已看', colors.primary, colors.onPrimary), - 'watching' => ('在看', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)), - 'want_to_watch' => ('想看', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.4)), + 'watched' || 'read' => ('已看' , colors.primary, colors.onPrimary), + 'watching' || 'reading' => ('在看', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)), + 'want_to_watch' || '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: 8, vertical: 3), - decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(5)), - child: Text(label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: fg)), - ); - } - - Widget _bookStatusBadge(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: 8, vertical: 3), - decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(5)), + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(4)), child: Text(label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: fg)), ); }