From 6b5488aff82431c84f5928fe2a71fd8693f1ce74 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Fri, 22 May 2026 12:15:43 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BC=96=E8=BE=91=E7=95=8C=E9=9D=A2=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/models/data_models.dart | 2 +- lib/pages/book/book_detail_page.dart | 87 +-- lib/pages/home_page.dart | 23 +- lib/pages/movies/movie_detail_page.dart | 87 +-- lib/pages/movies/movie_form_page.dart | 22 +- lib/pages/note/note_detail_page.dart | 554 ++++++++-------- lib/pages/note/note_form_page.dart | 817 +++++++++++------------- lib/pages/note/note_share_page.dart | 263 ++++++++ lib/pages/profile_page.dart | 12 +- lib/pages/recycle_bin_page.dart | 633 ++++++++---------- lib/pages/search_page.dart | 4 +- lib/pages/statistics_page.dart | 407 +++++------- lib/widgets/note_list_item.dart | 73 ++- 13 files changed, 1446 insertions(+), 1538 deletions(-) create mode 100644 lib/pages/note/note_share_page.dart diff --git a/lib/models/data_models.dart b/lib/models/data_models.dart index be014ae..35f0e6d 100644 --- a/lib/models/data_models.dart +++ b/lib/models/data_models.dart @@ -296,7 +296,7 @@ class Note { final String id; final String title; final String content; - final String contentType; // markdown / plain_text + final String contentType; // 内容类型(markdown) final List tags; final List images; // 图片路径列表 final DateTime createdAt; diff --git a/lib/pages/book/book_detail_page.dart b/lib/pages/book/book_detail_page.dart index 5d3d6e9..7387286 100644 --- a/lib/pages/book/book_detail_page.dart +++ b/lib/pages/book/book_detail_page.dart @@ -110,12 +110,15 @@ class _BookDetailPageState extends State { /// 构建右下角悬浮按钮组 Widget _buildFloatingActionButtons(Book book) { - final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty; - return Column( mainAxisSize: MainAxisSize.min, children: [ - // 删除按钮 + _buildFloatingButton( + icon: Icons.edit_outlined, + onPressed: () => _navigateToEdit(context), + tooltip: '编辑', + ), + const SizedBox(height: 12), _buildFloatingButton( icon: Icons.delete_outline, onPressed: () => _showDeleteDialog(context), @@ -123,23 +126,6 @@ class _BookDetailPageState extends State { backgroundColor: Colors.red, ), const SizedBox(height: 12), - // 清空封面按钮(仅当有封面时显示) - if (hasCover) ...[ - _buildFloatingButton( - icon: Icons.hide_image_outlined, - onPressed: () => _showClearCoverDialog(book), - tooltip: '清空封面', - ), - const SizedBox(height: 12), - ], - // 编辑按钮 - _buildFloatingButton( - icon: Icons.edit_outlined, - onPressed: () => _navigateToEdit(context), - tooltip: '编辑', - ), - const SizedBox(height: 12), - // 分享按钮 _buildFloatingButton( icon: Icons.share_outlined, onPressed: () => _showSharePoster(book), @@ -796,67 +782,6 @@ class _BookDetailPageState extends State { ); } - /// 显示清空封面对话框 - void _showClearCoverDialog(Book book) { - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: const Text( - '清空封面', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - content: const Text( - '确定要清空封面吗?清空后将使用默认占位图。', - style: TextStyle( - fontSize: 14, - color: Color(0xFF666666), - height: 1.5, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - style: TextButton.styleFrom( - foregroundColor: const Color(0xFF666666), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - child: const Text('取消'), - ), - ElevatedButton( - onPressed: () async { - Navigator.pop(context); - final updatedBook = book.copyWith( - coverPath: null, - updatedAt: DateTime.now(), - ); - await context.read().updateBook(updatedBook); - if (mounted) { - ToastUtil.show(context, '封面已清空'); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - child: const Text('清空'), - ), - ], - actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - ); - } - /// 下载封面到本地 Future _downloadCover(Book book) async { if (book.coverPath == null || book.coverPath!.isEmpty) { diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 2d6d39a..9c251fc 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -16,8 +16,8 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - /// 当前正在滑动的页面索引(用于PageView) final PageController _pageController = PageController(); + bool _isSwitchingPage = false; @override void dispose() { @@ -28,33 +28,36 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - // 左侧弹出菜单(仅在主页显示) drawer: context.watch().bottomNavIndex == 0 ? CustomDrawer() : null, - // 主体内容 body: Consumer( builder: (context, provider, child) { - // 同步底部导航栏和 PageView 的页面 final currentPage = provider.bottomNavIndex == 0 ? 0 : 1; if (_pageController.hasClients && _pageController.page?.round() != currentPage) { + _isSwitchingPage = true; _pageController.jumpToPage(currentPage); + // 切换完成后重置标记,确保导航栏显示 + Future.delayed(const Duration(milliseconds: 300), () { + if (mounted) { + _isSwitchingPage = false; + provider.setBottomNavVisible(true); + } + }); } return NotificationListener( onNotification: (notification) { + if (_isSwitchingPage) return false; if (notification is ScrollUpdateNotification) { final delta = notification.scrollDelta; if (delta != null && delta.abs() > 2) { - // 根据用户设置决定是否启用滚动隐藏 final userPrefs = UserPrefs(); if (!userPrefs.hideBottomNavOnScroll) return false; if (delta < 0) { - // 下拉(内容向下滚动)- 显示导航栏 provider.setBottomNavVisible(true); } else { - // 上滑(内容向上滚动)- 隐藏导航栏 provider.setBottomNavVisible(false); } } @@ -63,10 +66,8 @@ class _HomePageState extends State { }, child: Stack( children: [ - // 底层:主体内容(支持左右滑动切换) _buildPageView(provider), - // 底部导航栏(带动画) Positioned( left: 0, right: 0, @@ -79,7 +80,6 @@ class _HomePageState extends State { ), ), - // 导航栏隐藏时的展开按钮 if (!provider.bottomNavVisible) Positioned( left: 0, @@ -126,11 +126,10 @@ class _HomePageState extends State { ); } - /// 构建主体内容(使用 PageView 支持左右滑动切换) Widget _buildPageView(AppProvider provider) { return PageView( controller: _pageController, - physics: const BouncingScrollPhysics(), + physics: const NeverScrollableScrollPhysics(), onPageChanged: (index) { if (index == 0) { provider.setBottomNavIndex(0); diff --git a/lib/pages/movies/movie_detail_page.dart b/lib/pages/movies/movie_detail_page.dart index 84c49e0..2d5c5ae 100644 --- a/lib/pages/movies/movie_detail_page.dart +++ b/lib/pages/movies/movie_detail_page.dart @@ -110,12 +110,15 @@ class _MovieDetailPageState extends State { /// 构建右下角悬浮按钮组 Widget _buildFloatingActionButtons(Movie movie) { - final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty; - return Column( mainAxisSize: MainAxisSize.min, children: [ - // 删除按钮 + _buildFloatingButton( + icon: Icons.edit_outlined, + onPressed: () => _navigateToEdit(context), + tooltip: '编辑', + ), + const SizedBox(height: 12), _buildFloatingButton( icon: Icons.delete_outline, onPressed: () => _showDeleteDialog(context), @@ -123,23 +126,6 @@ class _MovieDetailPageState extends State { backgroundColor: Colors.red, ), const SizedBox(height: 12), - // 清空海报按钮(仅当有海报时显示) - if (hasPoster) ...[ - _buildFloatingButton( - icon: Icons.hide_image_outlined, - onPressed: () => _showClearPosterDialog(movie), - tooltip: '清空海报', - ), - const SizedBox(height: 12), - ], - // 编辑按钮 - _buildFloatingButton( - icon: Icons.edit_outlined, - onPressed: () => _navigateToEdit(context), - tooltip: '编辑', - ), - const SizedBox(height: 12), - // 分享按钮 _buildFloatingButton( icon: Icons.share_outlined, onPressed: () => _showSharePoster(movie), @@ -244,67 +230,6 @@ class _MovieDetailPageState extends State { ); } - /// 显示清空海报对话框 - void _showClearPosterDialog(Movie movie) { - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: const Text( - '清空海报', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - content: const Text( - '确定要清空海报吗?清空后将使用默认占位图。', - style: TextStyle( - fontSize: 14, - color: Color(0xFF666666), - height: 1.5, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - style: TextButton.styleFrom( - foregroundColor: const Color(0xFF666666), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - child: const Text('取消'), - ), - ElevatedButton( - onPressed: () async { - Navigator.pop(context); - final updatedMovie = movie.copyWith( - posterPath: null, - updatedAt: DateTime.now(), - ); - await context.read().updateMovie(updatedMovie); - if (mounted) { - ToastUtil.show(context, '海报已清空'); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - child: const Text('清空'), - ), - ], - actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - ); - } - /// 构建基本信息 Widget _buildBasicInfo(Movie movie) { return Padding( diff --git a/lib/pages/movies/movie_form_page.dart b/lib/pages/movies/movie_form_page.dart index c9af0f1..4880de9 100644 --- a/lib/pages/movies/movie_form_page.dart +++ b/lib/pages/movies/movie_form_page.dart @@ -446,10 +446,11 @@ class _MovieFormPageState extends State { height: 90, child: _buildInfoCard( label: '别名', - value: _alternateTitles.isEmpty - ? '' + value: _alternateTitles.isEmpty + ? '' : '${_alternateTitles.length}个:${_alternateTitles.join('、')}', icon: Icons.alternate_email_outlined, + scrollHorizontal: true, onTap: () => _showMultiValueDialog( title: '添加别名', initialValues: _alternateTitles, @@ -594,9 +595,10 @@ class _MovieFormPageState extends State { IconData? icon, double? height, bool scrollable = false, + bool scrollHorizontal = false, }) { final hasValue = value.isNotEmpty; - + // 构建值显示部分 Widget buildContent() { if (scrollable && height != null) { @@ -614,6 +616,20 @@ class _MovieFormPageState extends State { ), ), ); + } else if (scrollHorizontal) { + // 水平可滚动模式(别名等长文本使用) + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + child: Text( + hasValue ? value : '点击填写', + style: TextStyle( + fontSize: 15, + color: hasValue ? const Color(0xFF1A1A1A) : const Color(0xFFCCCCCC), + fontWeight: hasValue ? FontWeight.w500 : FontWeight.normal, + ), + ), + ); } else { // 普通模式:只显示一行 return Text( diff --git a/lib/pages/note/note_detail_page.dart b/lib/pages/note/note_detail_page.dart index a95bb28..d70a7a5 100644 --- a/lib/pages/note/note_detail_page.dart +++ b/lib/pages/note/note_detail_page.dart @@ -4,9 +4,9 @@ import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../models/data_models.dart'; -import '../../utils/toast_util.dart'; +import 'note_share_page.dart'; -/// 笔记详情页 - 极简主义设计 +/// 笔记详情页 class NoteDetailPage extends StatefulWidget { final Note note; @@ -17,275 +17,241 @@ class NoteDetailPage extends StatefulWidget { } class _NoteDetailPageState extends State { + static const _weekdays = ['一', '二', '三', '四', '五', '六', '日']; + @override Widget build(BuildContext context) { - // 从 Provider 获取最新的笔记数据 final note = context.watch().notes.firstWhere( (n) => n.id == widget.note.id, orElse: () => widget.note, ); - + return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: Text( - note.title.isNotEmpty ? note.title : '无标题', + note.title.isNotEmpty + ? note.title + : _truncateContent(note.content), overflow: TextOverflow.ellipsis, maxLines: 1, ), - actions: [ - IconButton( - icon: const Icon(Icons.edit_outlined), - onPressed: () => _navigateToEdit(context), - ), - const SizedBox(width: 8), - ], ), - body: Column( + body: Stack( children: [ - // 标签区域 - if (note.tags.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Color(0xFFE8E8E8), width: 0.5), + Column( + children: [ + // 日期 + 字数 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFE8E8E8), width: 0.5), + ), + ), + child: Row( + children: [ + Text( + '${note.createdAt.day}', + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.w200, + color: Color(0xFF333333), + height: 1.0, + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${note.createdAt.year}/${note.createdAt.month.toString().padLeft(2, '0')} 周${_weekdays[note.createdAt.weekday - 1]}', + style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFF777777)), + ), + const SizedBox(height: 1), + Text( + '${note.createdAt.hour.toString().padLeft(2, '0')}:${note.createdAt.minute.toString().padLeft(2, '0')}', + style: const TextStyle(fontSize: 11, color: Color(0xFF999999)), + ), + ], + ), + const Spacer(), + Text( + '${note.content.length} 字', + style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)), + ), + ], ), ), - child: Row( - children: [ - Wrap( - spacing: 10, - runSpacing: 10, - children: note.tags.map((tag) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - child: Text( - tag, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF666666), - ), - ), - ); - }).toList(), - ), - ], - ), - ), - // 内容区域 - Markdown 渲染 - Expanded( - child: _buildMarkdownContent(note), + // 标签行 + if (note.tags.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 6), + child: _buildTagRow(note.tags), + ), + + // 内容 + Expanded( + child: Markdown( + data: note.content, + styleSheet: _buildMarkdownStyleSheet(), + padding: const EdgeInsets.all(16), + // ignore: deprecated_member_use + imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note), + ), + ), + + // 图片行 + if (note.images.isNotEmpty) _buildImageRow(note.images), + ], + ), + + // 右下角悬浮按钮组 + Positioned( + right: 16, + bottom: 24, + child: _buildFloatingActionButtons(), ), ], ), ); } - /// 构建 Markdown 内容 - Widget _buildMarkdownContent(Note note) { - return Markdown( - data: note.content, - styleSheet: _buildMarkdownStyleSheet(), - padding: const EdgeInsets.all(16), - // TODO: migrate to sizedImageBuilder when flutter_markdown is updated - // ignore: deprecated_member_use - imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri), + Widget _buildTagRow(List tags) { + return Container( + height: 28, + margin: const EdgeInsets.only(bottom: 2), + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: tags.length, + separatorBuilder: (_, __) => const SizedBox(width: 6), + itemBuilder: (context, index) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + tags[index], + style: const TextStyle(fontSize: 11, color: Color(0xFF666666)), + ), + ); + }, + ), + ); + } + + Widget _buildImageRow(List images) { + return Container( + height: 80, + decoration: BoxDecoration( + color: Colors.white, + border: const Border( + top: BorderSide(color: Color(0xFFE8E8E8), width: 0.5), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.03), + blurRadius: 6, + offset: const Offset(0, -2), + ), + ], + ), + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + itemCount: images.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, index) { + return InkWell( + onTap: () => _showImagePreview(images, index), + child: Container( + width: 64, + height: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), + ), + clipBehavior: Clip.antiAlias, + child: Image.file( + File(images[index]), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: const Color(0xFFF5F5F5), + child: const Icon(Icons.broken_image_outlined, size: 20, color: Color(0xFFCCCCCC)), + ), + ), + ), + ); + }, + ), ); } - /// 构建 Markdown 样式表 MarkdownStyleSheet _buildMarkdownStyleSheet() { return MarkdownStyleSheet( - h1: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - height: 1.4, - ), - h2: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - height: 1.4, - ), - h3: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - height: 1.4, - ), - h4: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - height: 1.4, - ), - p: const TextStyle( - fontSize: 15, - color: Color(0xFF333333), - height: 1.8, - ), - code: const TextStyle( - fontSize: 14, - color: Color(0xFF1A1A1A), - backgroundColor: Color(0xFFF5F5F5), - fontFamily: 'monospace', - ), + h1: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4), + h2: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4), + h3: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4), + h4: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A), height: 1.4), + p: const TextStyle(fontSize: 15, color: Color(0xFF333333), height: 1.8), + code: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A), backgroundColor: Color(0xFFF5F5F5), fontFamily: 'monospace'), codeblockDecoration: BoxDecoration( color: const Color(0xFFF8F8F8), border: Border.all(color: const Color(0xFFE5E5E5)), borderRadius: BorderRadius.circular(6), ), codeblockPadding: const EdgeInsets.all(12), - blockquote: const TextStyle( - fontSize: 15, - color: Color(0xFF666666), - fontStyle: FontStyle.italic, - height: 1.8, - ), + blockquote: const TextStyle(fontSize: 15, color: Color(0xFF666666), fontStyle: FontStyle.italic, height: 1.8), blockquoteDecoration: const BoxDecoration( border: Border(left: BorderSide(color: Color(0xFF999999), width: 4)), ), blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), - listBullet: const TextStyle( - fontSize: 15, - color: Color(0xFF1A1A1A), - ), + listBullet: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)), listIndent: 24, - a: const TextStyle( - fontSize: 15, - color: Color(0xFF4A90D9), - decoration: TextDecoration.underline, - ), - tableHead: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - tableBody: const TextStyle( - fontSize: 14, - color: Color(0xFF333333), - ), - tableBorder: TableBorder.all( - color: const Color(0xFFE5E5E5), - width: 0.5, - ), + a: const TextStyle(fontSize: 15, color: Color(0xFF4A90D9), decoration: TextDecoration.underline), + tableHead: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)), + tableBody: const TextStyle(fontSize: 14, color: Color(0xFF333333)), + tableBorder: TableBorder.all(color: const Color(0xFFE5E5E5), width: 0.5), tableColumnWidth: const FlexColumnWidth(), - tableCellsDecoration: const BoxDecoration( - color: Colors.white, - ), + tableCellsDecoration: const BoxDecoration(color: Colors.white), tablePadding: const EdgeInsets.all(8), - strong: const TextStyle( - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - em: const TextStyle( - fontStyle: FontStyle.italic, - color: Color(0xFF333333), - ), - del: const TextStyle( - decoration: TextDecoration.lineThrough, - color: Color(0xFF999999), - ), + strong: const TextStyle(fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)), + em: const TextStyle(fontStyle: FontStyle.italic, color: Color(0xFF333333)), + del: const TextStyle(decoration: TextDecoration.lineThrough, color: Color(0xFF999999)), ); } - /// 构建 Markdown 中的图片 - Widget _buildMarkdownImage(Uri uri) { - // 检查是否是本地图片路径 + Widget _buildMarkdownImage(Uri uri, Note note) { final path = uri.toString(); if (path.isEmpty) return const SizedBox.shrink(); - // 尝试从笔记图片列表中查找 - final noteImages = widget.note.images; - String? matchedPath; - for (final imgPath in noteImages) { + for (final imgPath in note.images) { if (imgPath.contains(path) || path.contains(imgPath)) { - matchedPath = imgPath; - break; + if (File(imgPath).existsSync()) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file(File(imgPath), fit: BoxFit.cover), + ); + } } } - if (matchedPath != null && File(matchedPath).existsSync()) { - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.file( - File(matchedPath), - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return _buildImageErrorWidget(); - }, - ), - ); - } - - // 如果是网络图片 if (path.startsWith('http')) { return ClipRRect( borderRadius: BorderRadius.circular(8), - child: Image.network( - path, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return _buildImageErrorWidget(); - }, - ), + child: Image.network(path, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const SizedBox.shrink()), ); } - return _buildImageErrorWidget(); + return const SizedBox.shrink(); } - /// 构建图片错误状态 - Widget _buildImageErrorWidget() { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - const Icon(Icons.broken_image_outlined, size: 20, color: Color(0xFF999999)), - const SizedBox(width: 8), - const Expanded( - child: Text( - '图片加载失败', - style: TextStyle(fontSize: 13, color: Color(0xFF999999)), - ), - ), - ], - ), - ); - } - - /// 格式化日期时间 - String _formatDateTime(DateTime dateTime) { - return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; - } - - /// 跳转到编辑页面 - void _navigateToEdit(BuildContext context) { - // 从 Provider 获取最新的笔记数据,确保图片等字段是最新的 - final currentNote = context.read().notes.firstWhere( - (n) => n.id == widget.note.id, - orElse: () => widget.note, - ); - Navigator.pushNamed(context, '/note-form', arguments: currentNote).then((_) async { - // 返回时刷新笔记列表 - await context.read().loadNotes(); - }); - } - - /// 显示图片预览 - void _showImagePreview(BuildContext context, List images, int initialIndex) { + void _showImagePreview(List images, int initialIndex) { showDialog( context: context, barrierDismissible: true, @@ -299,10 +265,7 @@ class _NoteDetailPageState extends State { boundaryMargin: const EdgeInsets.all(20), minScale: 0.5, maxScale: 4, - child: Image.file( - File(images[initialIndex]), - fit: BoxFit.contain, - ), + child: Image.file(File(images[initialIndex]), fit: BoxFit.contain), ), ), ), @@ -310,91 +273,120 @@ class _NoteDetailPageState extends State { ); } - /// 构建操作按钮 - Widget _buildActionButton({ + /// 截取内容前N个字作为标题 + String _truncateContent(String content) { + final cleaned = content + .replaceAll(RegExp(r'^#+\s+', multiLine: true), '') + .replaceAll(RegExp(r'\*\*(.+?)\*\*'), r'$1') + .replaceAll(RegExp(r'\*(.+?)\*'), r'$1') + .replaceAll(RegExp(r'`(.+?)`'), r'$1') + .replaceAll(RegExp(r'^\s*[-*+]\s', multiLine: true), '') + .replaceAll(RegExp(r'^\s*>\s', multiLine: true), '') + .replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'$1') + .replaceAll(RegExp(r'!\[([^\]]*)\]\([^)]+\)'), '') + .trim(); + if (cleaned.isEmpty) return '无标题'; + return cleaned.length > 20 ? '${cleaned.substring(0, 20)}…' : cleaned; + } + + void _navigateToEdit(BuildContext context) { + final currentNote = context.read().notes.firstWhere( + (n) => n.id == widget.note.id, + orElse: () => widget.note, + ); + Navigator.pushNamed(context, '/note-form', arguments: currentNote).then((_) async { + await context.read().loadNotes(); + }); + } + + /// 右下角悬浮按钮组 + Widget _buildFloatingActionButtons() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildFloatingButton( + icon: Icons.edit_outlined, + onPressed: () => _navigateToEdit(context), + tooltip: '编辑', + ), + const SizedBox(height: 12), + _buildFloatingButton( + icon: Icons.delete_outline, + onPressed: () => _showDeleteDialog(context), + tooltip: '删除', + backgroundColor: Colors.red, + ), + const SizedBox(height: 12), + _buildFloatingButton( + icon: Icons.share_outlined, + onPressed: _shareNote, + tooltip: '分享', + backgroundColor: const Color(0xFF4CAF50), + ), + ], + ); + } + + Widget _buildFloatingButton({ required IconData icon, - required Color color, - required VoidCallback onTap, + required VoidCallback onPressed, + required String tooltip, + Color backgroundColor = const Color(0xFF1A1A1A), }) { - return Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(10), - child: Icon( - icon, - color: color, - size: 20, - ), + return Material( + color: Colors.transparent, + child: Ink( + width: 40, + height: 40, + decoration: BoxDecoration( + color: backgroundColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: backgroundColor.withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: IconButton( + icon: Icon(icon, size: 18, color: Colors.white), + onPressed: onPressed, + padding: EdgeInsets.zero, + tooltip: tooltip, ), ), ); } - /// 显示删除对话框 void _showDeleteDialog(BuildContext context) { showDialog( context: context, - builder: (context) => AlertDialog( - backgroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: const Text( - '确认删除', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - content: const Text( - '确定要删除这条笔记吗?删除后可在回收站恢复。', - style: TextStyle( - fontSize: 14, - color: Color(0xFF666666), - height: 1.5, - ), - ), + builder: (ctx) => AlertDialog( + title: const Text('删除笔记'), + content: const Text('删除后将移至回收站,确定要删除吗?'), actions: [ TextButton( - onPressed: () => Navigator.pop(context), - style: TextButton.styleFrom( - foregroundColor: const Color(0xFF666666), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), + onPressed: () => Navigator.pop(ctx), child: const Text('取消'), ), - ElevatedButton( - onPressed: () async { - await context.read().removeNote(widget.note.id); - // 刷新笔记列表 - await context.read().loadNotes(); - if (!mounted) return; + TextButton( + onPressed: () { + Navigator.pop(ctx); + context.read().removeNote(widget.note.id); Navigator.pop(context); - Navigator.pop(context); - ToastUtil.show(context, '已删除'); }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - child: const Text('删除'), + child: const Text('删除', style: TextStyle(color: Colors.red)), ), ], - actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), ); } + + void _shareNote() { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => NoteSharePage(note: widget.note)), + ); + } } diff --git a/lib/pages/note/note_form_page.dart b/lib/pages/note/note_form_page.dart index a392c5b..6c783bc 100644 --- a/lib/pages/note/note_form_page.dart +++ b/lib/pages/note/note_form_page.dart @@ -20,124 +20,165 @@ class NoteFormPage extends StatefulWidget { } class _NoteFormPageState extends State { + late TextEditingController _titleController; late TextEditingController _contentController; late DateTime _createdAt; List _tags = []; List _images = []; // 图片路径列表 - String _contentType = 'plain_text'; // markdown / plain_text bool _isEditing = false; final ImagePicker _picker = ImagePicker(); String? _tempNoteId; // 新建模式时使用的临时笔记ID - String _editorMode = 'split'; // 'edit' | 'split' | 'preview' + String _editorMode = 'edit'; // 'edit' | 'preview' + int _charCount = 0; + + static const _weekdays = ['一', '二', '三', '四', '五', '六', '日']; @override void initState() { super.initState(); final note = widget.note; - _contentController = TextEditingController(text: note?.content ?? ''); + _titleController = TextEditingController(text: note?.title ?? ''); + final text = note?.content ?? ''; + _contentController = TextEditingController(text: text); + _charCount = text.length; + _contentController.addListener(() { + setState(() => _charCount = _contentController.text.length); + }); _createdAt = note?.createdAt ?? DateTime.now(); _tags = note != null ? List.from(note.tags) : []; _images = note != null ? List.from(note.images) : []; - _contentType = note?.contentType ?? 'plain_text'; _isEditing = note != null; } @override void dispose() { + _titleController.dispose(); _contentController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + return Scaffold( backgroundColor: Colors.white, + resizeToAvoidBottomInset: false, appBar: AppBar( - title: Text(_isEditing ? '编辑笔记' : '新建笔记'), + title: GestureDetector( + onLongPress: _showTitleDialog, + child: _buildAppBarTitle(), + ), actions: [ - IconButton( - onPressed: _saveNote, - icon: const Icon(Icons.save_outlined), - tooltip: '保存', + Padding( + padding: const EdgeInsets.only(right: 12), + child: TextButton( + onPressed: _saveNote, + style: TextButton.styleFrom( + backgroundColor: const Color(0xFF1A1A1A), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + minimumSize: Size.zero, + ), + child: const Text('保存', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)), + ), ), - const SizedBox(width: 8), ], ), - body: Column( + body: Stack( children: [ - // 顶部信息栏:创建时间 + 格式选择 + 标签 - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Color(0xFFE8E8E8), width: 0.5), - ), - ), + // 主内容区域 — 图片网格固定在内容下方 + Padding( + padding: EdgeInsets.only(bottom: 56 + bottomInset), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - // 创建时间 - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - child: Text( - _formatDateTime(_createdAt), - style: const TextStyle( - fontSize: 12, - color: Color(0xFF666666), - ), - ), + // 顶部信息栏 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFE8E8E8), width: 0.5), ), - const Spacer(), - // 格式选择 - _buildFormatSelector(), - ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 大日号 + Text( + '${_createdAt.day}', + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.w200, + color: Color(0xFF333333), + height: 1.0, + ), + ), + const SizedBox(width: 8), + // 右边:年月 + 时分 纵向排列 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${_createdAt.year}/${_createdAt.month.toString().padLeft(2, '0')} 周${_weekdays[_createdAt.weekday - 1]}', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: Color(0xFF777777), + ), + ), + const SizedBox(height: 1), + Text( + '${_createdAt.hour.toString().padLeft(2, '0')}:${_createdAt.minute.toString().padLeft(2, '0')}', + style: const TextStyle( + fontSize: 11, + color: Color(0xFF999999), + ), + ), + ], + ), + const Spacer(), + // 字数统计 + Text( + '$_charCount 字', + style: const TextStyle( + fontSize: 11, + color: Color(0xFFAAAAAA), + ), + ), + ], + ), + ], + ), ), - const SizedBox(height: 12), - // 标签 - _buildTagSelector(), + + // 标签行 + _buildTagRow(), + + // 编辑 / 预览区域 + Expanded( + child: _buildContentArea(), + ), + + // 图片区域 + _buildImageRow(), ], ), ), - // 编辑 / 预览区域 - Expanded( - child: _contentType == 'markdown' - ? _buildContentArea() - : _buildEditor(), + // 底部浮动工具栏 — 独立于主内容,键盘弹起时单独上移 + Positioned( + left: 0, + right: 0, + bottom: bottomInset, + child: _buildFloatingToolbar(), ), - - // 图片区域(放在内容下方) - if (_contentType == 'plain_text' && _images.isNotEmpty) - Container( - height: 100, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: const BoxDecoration( - border: Border( - top: BorderSide(color: Color(0xFFE8E8E8), width: 0.5), - ), - ), - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: _images.length, - separatorBuilder: (context, index) => const SizedBox(width: 8), - itemBuilder: (context, index) { - return _buildHorizontalImageItem(index); - }, - ), - ), - - // 底部区域:Markdown 浮动工具栏 或 纯文本图片工具栏 - if (_contentType == 'markdown') - _buildFloatingToolbar() - else - _buildPlainTextBottom(), ], ), ); @@ -216,7 +257,6 @@ class _NoteFormPageState extends State { ), const SizedBox(width: 8), _modeSwitch(Icons.edit, 'edit'), - _modeSwitch(Icons.vertical_split, 'split'), _modeSwitch(Icons.visibility, 'preview'), ], ), @@ -323,91 +363,14 @@ class _NoteFormPageState extends State { } } - /// 纯文本模式底部栏 - Widget _buildPlainTextBottom() { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: const BoxDecoration( - border: Border( - top: BorderSide(color: Color(0xFFE8E8E8), width: 0.5), - ), - ), - child: Row( - children: [ - if (_images.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.image_outlined, size: 14, color: Color(0xFF666666)), - const SizedBox(width: 4), - Text('${_images.length}', style: const TextStyle(fontSize: 12, color: Color(0xFF666666))), - ], - ), - ), - const Spacer(), - InkWell( - onTap: _pickImage, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), - borderRadius: BorderRadius.circular(8), - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.add_photo_alternate_outlined, size: 18, color: Colors.white), - SizedBox(width: 6), - Text('添加图片', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white)), - ], - ), - ), - ), - ], - ), - ); - } - /// 根据 _editorMode 构建内容区域 Widget _buildContentArea() { switch (_editorMode) { - case 'edit': - return _buildEditor(); case 'preview': return _buildPreview(); - case 'split': + case 'edit': default: - return Column( - children: [ - Expanded(flex: 1, child: _buildEditor()), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => setState(() => _editorMode = 'preview'), - child: Container( - height: 4, - color: const Color(0xFFF5F5F5), - child: Center( - child: Container( - width: 28, - height: 3, - decoration: BoxDecoration( - color: const Color(0xFFD0D0D0), - borderRadius: BorderRadius.circular(1.5), - ), - ), - ), - ), - ), - Expanded(flex: 1, child: _buildPreview()), - ], - ); + return _buildEditor(); } } @@ -429,9 +392,7 @@ class _NoteFormPageState extends State { height: 1.6, ), decoration: InputDecoration( - hintText: _contentType == 'markdown' - ? '使用 Markdown 格式书写...' - : '开始书写...', + hintText: '使用 Markdown 格式书写...', hintStyle: const TextStyle( fontSize: 16, color: Color(0xFFCCCCCC), @@ -443,10 +404,13 @@ class _NoteFormPageState extends State { ); } - /// 实时 Markdown 预览 + /// 实时 Markdown 预览(点击回到编辑) Widget _buildPreview() { final text = _contentController.text; - return Container( + return GestureDetector( + onTap: () => setState(() => _editorMode = 'edit'), + behavior: HitTestBehavior.opaque, + child: Container( color: const Color(0xFFFAFAFA), child: text.isEmpty ? const Center( @@ -506,262 +470,72 @@ class _NoteFormPageState extends State { ), ), ), - ); - } - - /// 构建格式选择器 - Widget _buildFormatSelector() { - return GestureDetector( - onTap: () => _showFormatSelector(), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _contentType == 'markdown' ? Icons.code : Icons.text_fields, - size: 16, - color: const Color(0xFF666666), - ), - const SizedBox(width: 6), - Text( - _contentType == 'markdown' ? 'Markdown' : '纯文本', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Color(0xFF666666), - ), - ), - const SizedBox(width: 4), - const Icon( - Icons.arrow_drop_down, - size: 18, - color: Color(0xFF999999), - ), - ], - ), - ), - ); - } - - /// 显示格式选择对话框 - void _showFormatSelector() { - showModalBottomSheet( - context: context, - backgroundColor: Colors.white, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (context) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 顶部指示条 - Container( - margin: const EdgeInsets.only(top: 12, bottom: 16), - width: 40, - height: 4, - decoration: BoxDecoration( - color: const Color(0xFFE0E0E0), - borderRadius: BorderRadius.circular(2), - ), - ), - // 标题 - const Padding( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8), - child: Row( - children: [ - Text( - '选择格式', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), - ], - ), - ), - const SizedBox(height: 8), - _buildFormatOption( - icon: Icons.code, - title: 'Markdown', - subtitle: '支持 Markdown 语法', - isSelected: _contentType == 'markdown', - onTap: () { - setState(() => _contentType = 'markdown'); - Navigator.pop(context); - }, - ), - _buildFormatOption( - icon: Icons.text_fields, - title: '纯文本', - subtitle: '普通文本格式,支持图片', - isSelected: _contentType == 'plain_text', - onTap: () { - setState(() => _contentType = 'plain_text'); - Navigator.pop(context); - }, - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } - - /// 构建格式选项 - Widget _buildFormatOption({ - required IconData icon, - required String title, - required String subtitle, - required bool isSelected, - required VoidCallback onTap, - }) { - return InkWell( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Row( - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - icon, - size: 22, - color: const Color(0xFF666666), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A), - ), - ), - const SizedBox(height: 4), - Text( - subtitle, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF999999), - ), - ), - ], - ), - ), - if (isSelected) - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.check, - size: 16, - color: Colors.white, - ), - ), - ], - ), - ), - ); + )); } /// 构建标签选择器 - Widget _buildTagSelector() { - return Wrap( - spacing: 10, - runSpacing: 8, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - ..._tags.asMap().entries.map((entry) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - entry.value, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF666666), - ), - ), - const SizedBox(width: 6), - GestureDetector( - onTap: () => setState(() => _tags.removeAt(entry.key)), - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: const Color(0xFFE8E8E8), - borderRadius: BorderRadius.circular(4), - ), - child: const Icon( - Icons.close, - size: 10, - color: Color(0xFF999999), - ), - ), - ), - ], - ), - ); - }), - // 添加标签按钮 - GestureDetector( - onTap: () => _showAddTagDialog(), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.add, - size: 14, - color: Color(0xFF999999), - ), - SizedBox(width: 4), - Text( - '标签', - style: TextStyle( - fontSize: 13, - color: Color(0xFF999999), - ), - ), - ], - ), + /// 构建标签横向滚动行 + Widget _buildTagRow() { + return Container( + height: 28, + margin: const EdgeInsets.only(top: 6, bottom: 2), + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _tags.length + 1, + separatorBuilder: (_, __) => const SizedBox(width: 6), + itemBuilder: (context, index) { + if (index < _tags.length) { + return _buildTagChip(index); + } + return _buildAddTagButton(); + }, + ), + ); + } + + Widget _buildTagChip(int index) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _tags[index], + style: const TextStyle(fontSize: 11, color: Color(0xFF666666)), ), + const SizedBox(width: 3), + GestureDetector( + onTap: () => setState(() => _tags.removeAt(index)), + child: const Icon(Icons.close, size: 10, color: Color(0xFFBBBBBB)), + ), + ], + ), + ); + } + + Widget _buildAddTagButton() { + return GestureDetector( + onTap: _showAddTagDialog, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all(color: const Color(0xFFDDDDDD), width: 1), ), - ], + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add, size: 12, color: Color(0xFFAAAAAA)), + SizedBox(width: 2), + Text('标签', style: TextStyle(fontSize: 11, color: Color(0xFFAAAAAA))), + ], + ), + ), ); } @@ -837,32 +611,29 @@ class _NoteFormPageState extends State { ), ), const SizedBox(height: 12), - Wrap( - spacing: 10, - runSpacing: 10, - children: availableTags.map((tag) { - return GestureDetector( - onTap: () { - _addTag(tag); - Navigator.pop(context); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - child: Text( - tag, - style: const TextStyle( - fontSize: 14, - color: Color(0xFF666666), + SizedBox( + height: 200, + child: ListView( + shrinkWrap: true, + children: availableTags.map((tag) { + return InkWell( + onTap: () { + _addTag(tag); + Navigator.pop(context); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Text( + tag, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF555555), + ), ), ), - ), - ); - }).toList(), + ); + }).toList(), + ), ), ], ], @@ -916,17 +687,98 @@ class _NoteFormPageState extends State { } } - /// 格式化日期时间 - String _formatDateTime(DateTime date) { - return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; + Widget _buildAppBarTitle() { + final base = _isEditing ? '编辑笔记' : '新建笔记'; + final t = _titleController.text.trim(); + if (t.isEmpty) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(base), + const SizedBox(width: 6), + const Icon(Icons.edit, size: 14, color: Color(0xFF999999)), + ], + ); + } + final display = t.length > 4 ? '${t.substring(0, 4)}…' : t; + return Text('$base($display)'); + } + + /// 长按标题弹出标题编辑窗口 + void _showTitleDialog() { + final controller = TextEditingController(text: _titleController.text); + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: const Text( + '编辑标题', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + content: TextField( + controller: controller, + autofocus: true, + style: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A)), + decoration: InputDecoration( + hintText: '输入标题...', + hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFCCCCCC)), + filled: true, + fillColor: const Color(0xFFFAFAFA), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFFE8E8E8), width: 0.5), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFFE8E8E8), width: 0.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + onSubmitted: (value) { + setState(() => _titleController.text = value.trim()); + Navigator.pop(context); + }, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF666666), + ), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () { + setState(() => _titleController.text = controller.text.trim()); + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1A1A1A), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: const Text('确定'), + ), + ], + actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ); } /// 保存笔记 Future _saveNote() async { final content = _contentController.text.trim(); - - if (content.isEmpty) { - ToastUtil.show(context, '笔记内容不能为空'); + final title = _titleController.text.trim(); + + if (title.isEmpty && content.isEmpty) { + ToastUtil.show(context, '标题或内容不能为空'); return; } @@ -935,8 +787,8 @@ class _NoteFormPageState extends State { if (_isEditing) { // 更新现有笔记 final updatedNote = widget.note!.copyWith( + title: _titleController.text.trim(), content: content, - contentType: _contentType, tags: _tags, images: _images, updatedAt: now, @@ -955,10 +807,12 @@ class _NoteFormPageState extends State { finalImages = await _moveImagesToNewId(oldNoteId, newNoteId); } + final title = _titleController.text.trim(); + final newNote = Note( id: noteId, + title: title, content: content, - contentType: _contentType, tags: _tags, images: finalImages.isNotEmpty ? finalImages : _images, createdAt: _createdAt, @@ -1054,35 +908,84 @@ class _NoteFormPageState extends State { } } - /// 构建图片项 - Widget _buildImageItem(int index) { + /// 构建图片横向滚动行 + Widget _buildImageRow() { + if (_images.isEmpty) { + return Container( + height: 80, + decoration: const BoxDecoration( + color: Colors.white, + ), + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + children: [ + _buildAddImageButton(), + ], + ), + ); + } + + return Container( + height: 88, + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.02), + blurRadius: 4, + offset: const Offset(0, -1), + ), + ], + ), + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + itemCount: _images.length + 1, + separatorBuilder: (_, __) => const SizedBox(width: 10), + itemBuilder: (context, index) { + if (index < _images.length) { + return _buildImageItem(index); + } + return _buildAddImageButton(); + }, + ), + ); + } + + /// 添加图片按钮 + Widget _buildAddImageButton() { return InkWell( - onTap: () => _showImagePreview(index), - onLongPress: () => _showDeleteImageDialog(index), + onTap: _pickImage, + borderRadius: BorderRadius.circular(12), child: Container( + width: 64, + height: 64, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(12), + color: const Color(0xFFF8F8F8), border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), ), - clipBehavior: Clip.antiAlias, - child: Image.file( - File(_images[index]), - fit: BoxFit.cover, + child: const Icon( + Icons.add_photo_alternate_outlined, + size: 24, + color: Color(0xFFBBBBBB), ), ), ); } - /// 构建横向图片项(用于底部图片栏) - Widget _buildHorizontalImageItem(int index) { + /// 构建图片项 + Widget _buildImageItem(int index) { return InkWell( onTap: () => _showImagePreview(index), onLongPress: () => _showDeleteImageDialog(index), + borderRadius: BorderRadius.circular(12), child: Container( - width: 84, - height: 84, + width: 64, + height: 64, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), ), clipBehavior: Clip.antiAlias, diff --git a/lib/pages/note/note_share_page.dart b/lib/pages/note/note_share_page.dart new file mode 100644 index 0000000..06deddd --- /dev/null +++ b/lib/pages/note/note_share_page.dart @@ -0,0 +1,263 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; +import '../../models/data_models.dart'; +import '../../utils/toast_util.dart'; + +/// 笔记分享海报页面 +class NoteSharePage extends StatefulWidget { + final Note note; + + const NoteSharePage({super.key, required this.note}); + + @override + State createState() => _NoteSharePageState(); +} + +class _NoteSharePageState extends State { + final GlobalKey _posterKey = GlobalKey(); + bool _isGenerating = false; + + static const _weekdays = ['一', '二', '三', '四', '五', '六', '日']; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + '分享笔记', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1A1A), + ), + ), + centerTitle: true, + actions: [ + TextButton( + onPressed: _isGenerating ? null : _generateAndShare, + child: _isGenerating + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text( + '分享', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1A1A), + ), + ), + ), + const SizedBox(width: 8), + ], + ), + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: RepaintBoundary( + key: _posterKey, + child: _buildPosterWidget(), + ), + ), + ), + ); + } + + Widget _buildPosterWidget() { + final note = widget.note; + final hasImages = note.images.isNotEmpty; + final dateStr = + '${note.createdAt.year}/${note.createdAt.month.toString().padLeft(2, '0')}/${note.createdAt.day.toString().padLeft(2, '0')} ' + '周${_weekdays[note.createdAt.weekday - 1]} ' + '${note.createdAt.hour.toString().padLeft(2, '0')}:${note.createdAt.minute.toString().padLeft(2, '0')}'; + + return Container( + width: 320, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 首张图片 + if (hasImages && File(note.images.first).existsSync()) + ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + child: Image.file( + File(note.images.first), + width: 320, + height: 200, + fit: BoxFit.cover, + ), + ), + + // 内容区域 + Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题 + if (note.title.isNotEmpty) ...[ + Text( + note.title, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(height: 4), + ], + + // 日期 + Text( + dateStr, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), + ), + + // 标签 + if (note.tags.isNotEmpty) ...[ + const SizedBox(height: 12), + Wrap( + spacing: 6, + runSpacing: 6, + children: note.tags.map((tag) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFFF0F0F0), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + tag, + style: const TextStyle(fontSize: 11, color: Color(0xFF888888)), + ), + ); + }).toList(), + ), + ], + + const SizedBox(height: 16), + Container(height: 0.5, color: const Color(0xFFE8E8E8)), + const SizedBox(height: 16), + + // 正文内容 + Text( + note.content, + maxLines: 12, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF333333), + height: 1.8, + ), + ), + + // 字数和图片数 + const SizedBox(height: 16), + Row( + children: [ + _buildMetaChip(Icons.text_fields_outlined, '${note.content.length} 字'), + if (hasImages) ...[ + const SizedBox(width: 12), + _buildMetaChip(Icons.image_outlined, '${note.images.length} 图'), + ], + const Spacer(), + // Mooknote 品牌 + const Text( + 'Mooknote', + style: TextStyle( + fontSize: 11, + color: Color(0xFFCCCCCC), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildMetaChip(IconData icon, String text) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 13, color: const Color(0xFFAAAAAA)), + const SizedBox(width: 4), + Text( + text, + style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)), + ), + ], + ); + } + + Future _generateAndShare() async { + setState(() => _isGenerating = true); + + try { + final boundary = _posterKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + if (boundary == null) { + throw Exception('无法获取海报边界'); + } + + final image = await boundary.toImage(pixelRatio: 3.0); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + if (byteData == null) { + throw Exception('无法生成图片数据'); + } + + final bytes = byteData.buffer.asUint8List(); + + final tempDir = await getTemporaryDirectory(); + final fileName = 'note_share_${DateTime.now().millisecondsSinceEpoch}.png'; + final file = File('${tempDir.path}/$fileName'); + await file.writeAsBytes(bytes); + + await Share.shareXFiles( + [XFile(file.path)], + text: '分享笔记:${widget.note.title.isNotEmpty ? widget.note.title : '无标题'}', + ); + } catch (e) { + if (mounted) { + ToastUtil.show(context, '生成海报失败:$e'); + } + } finally { + if (mounted) { + setState(() => _isGenerating = false); + } + } + } +} diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index eaa8b0b..4ea8ce6 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -1047,10 +1047,10 @@ class _SettingsPageState extends State { } /// 显示清除缓存对话框 - void _showClearCacheDialog(BuildContext context) { + void _showClearCacheDialog(BuildContext pageContext) { showDialog( - context: context, - builder: (context) => AlertDialog( + context: pageContext, + builder: (dialogContext) => AlertDialog( backgroundColor: Colors.white, elevation: 0, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), @@ -1058,13 +1058,13 @@ class _SettingsPageState extends State { content: const Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?'), actions: [ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.pop(dialogContext), child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), ), TextButton( onPressed: () async { - Navigator.pop(context); - await _clearCacheData(context); + Navigator.pop(dialogContext); + await _clearCacheData(pageContext); }, child: const Text('确定', style: TextStyle(color: Colors.red)), ), diff --git a/lib/pages/recycle_bin_page.dart b/lib/pages/recycle_bin_page.dart index ea9ac60..169d98b 100644 --- a/lib/pages/recycle_bin_page.dart +++ b/lib/pages/recycle_bin_page.dart @@ -12,36 +12,68 @@ class RecycleBinPage extends StatefulWidget { State createState() => _RecycleBinPageState(); } -class _RecycleBinPageState extends State with SingleTickerProviderStateMixin { - late TabController _tabController; - List _deletedMovies = []; - List _deletedBooks = []; - List _deletedNotes = []; +enum _ItemType { movie, book, note } + +class _DeletedItem { + final _ItemType type; + final String id; + final String title; + final String subtitle; + final IconData icon; + final String typeLabel; + + _DeletedItem.movie(Movie m) + : type = _ItemType.movie, + id = m.id, + title = m.title, + subtitle = '删除于 ${m.updatedAt.year}.${m.updatedAt.month.toString().padLeft(2, '0')}.${m.updatedAt.day.toString().padLeft(2, '0')}', + icon = Icons.movie_outlined, + typeLabel = '影视'; + + _DeletedItem.book(Book b) + : type = _ItemType.book, + id = b.id, + title = b.title, + subtitle = '删除于 ${b.updatedAt.year}.${b.updatedAt.month.toString().padLeft(2, '0')}.${b.updatedAt.day.toString().padLeft(2, '0')}', + icon = Icons.menu_book_outlined, + typeLabel = '书籍'; + + _DeletedItem.note(Note n) + : type = _ItemType.note, + id = n.id, + title = n.title.isNotEmpty ? n.title : n.summary, + subtitle = '删除于 ${n.updatedAt.year}.${n.updatedAt.month.toString().padLeft(2, '0')}.${n.updatedAt.day.toString().padLeft(2, '0')}', + icon = Icons.description_outlined, + typeLabel = '笔记'; +} + +class _RecycleBinPageState extends State { + List<_DeletedItem> _allItems = []; + _ItemType? _filterType; // null = 全部 bool _isLoading = true; + List<_DeletedItem> get _filteredItems => + _filterType == null ? _allItems : _allItems.where((i) => i.type == _filterType).toList(); + @override void initState() { super.initState(); - _tabController = TabController(length: 3, vsync: this); _loadDeletedItems(); } - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - Future _loadDeletedItems() async { setState(() => _isLoading = true); final provider = context.read(); final movies = await provider.getDeletedMovies(); final books = await provider.getDeletedBooks(); final notes = await provider.getDeletedNotes(); + if (!mounted) return; setState(() { - _deletedMovies = movies; - _deletedBooks = books; - _deletedNotes = notes; + _allItems = [ + for (final m in movies) _DeletedItem.movie(m), + for (final b in books) _DeletedItem.book(b), + for (final n in notes) _DeletedItem.note(n), + ]; _isLoading = false; }); } @@ -52,380 +84,287 @@ class _RecycleBinPageState extends State with SingleTickerProvid backgroundColor: Colors.white, appBar: AppBar( title: const Text('回收站'), - bottom: TabBar( - controller: _tabController, - labelColor: const Color(0xFF1A1A1A), - unselectedLabelColor: const Color(0xFF999999), - indicatorColor: const Color(0xFF1A1A1A), - indicatorSize: TabBarIndicatorSize.label, - labelStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - unselectedLabelStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - tabs: [ - Tab(text: '影视 (${_deletedMovies.length})'), - Tab(text: '书籍 (${_deletedBooks.length})'), - Tab(text: '笔记 (${_deletedNotes.length})'), - ], - ), actions: [ - // 清空全部 - TextButton.icon( - onPressed: _showClearAllDialog, - icon: const Icon(Icons.delete_sweep, size: 18, color: Colors.red), - label: const Text( - '清空', - style: TextStyle(color: Colors.red), + if (_allItems.isNotEmpty) + Padding( + padding: const EdgeInsets.only(right: 12), + child: TextButton( + onPressed: _showClearAllDialog, + style: TextButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.red, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: const BorderSide(color: Color(0xFFFFDDDD), width: 0.5), + ), + minimumSize: Size.zero, + ), + child: const Text('清空', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), + ), ), - ), - const SizedBox(width: 8), ], ), body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : TabBarView( - controller: _tabController, + ? const Center(child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))) + : Column( children: [ - _buildMovieList(), - _buildBookList(), - _buildNoteList(), + // 筛选标签行 + if (_allItems.isNotEmpty) _buildFilterRow(), + // 列表 + Expanded( + child: _filteredItems.isEmpty + ? _buildEmptyState() + : RefreshIndicator( + onRefresh: _loadDeletedItems, + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: _filteredItems.length, + itemBuilder: (_, i) => _buildCard(_filteredItems[i]), + ), + ), + ), ], ), ); } - /// 影视列表 - Widget _buildMovieList() { - if (_deletedMovies.isEmpty) { - return _buildEmptyState('暂无删除的影视'); - } - return ListView.builder( - padding: const EdgeInsets.only(top: 8), - itemCount: _deletedMovies.length, - itemBuilder: (context, index) { - final movie = _deletedMovies[index]; - return _buildDeletedItemCard( - title: movie.title, - subtitle: '删除于 ${_formatDate(movie.updatedAt)}', - onRestore: () => _restoreMovie(movie), - onDelete: () => _permanentDeleteMovie(movie), - ); - }, - ); - } - - /// 书籍列表 - Widget _buildBookList() { - if (_deletedBooks.isEmpty) { - return _buildEmptyState('暂无删除的书籍'); - } - return ListView.builder( - padding: const EdgeInsets.only(top: 8), - itemCount: _deletedBooks.length, - itemBuilder: (context, index) { - final book = _deletedBooks[index]; - return _buildDeletedItemCard( - title: book.title, - subtitle: '删除于 ${_formatDate(book.updatedAt)}', - onRestore: () => _restoreBook(book), - onDelete: () => _permanentDeleteBook(book), - ); - }, - ); - } - - /// 笔记列表 - Widget _buildNoteList() { - if (_deletedNotes.isEmpty) { - return _buildEmptyState('暂无删除的笔记'); - } - return ListView.builder( - padding: const EdgeInsets.only(top: 8), - itemCount: _deletedNotes.length, - itemBuilder: (context, index) { - final note = _deletedNotes[index]; - return _buildDeletedItemCard( - title: note.summary, - subtitle: '删除于 ${_formatDate(note.updatedAt)}', - onRestore: () => _restoreNote(note), - onDelete: () => _permanentDeleteNote(note), - ); - }, - ); - } - - /// 构建已删除项卡片 - 紧凑列表布局 - Widget _buildDeletedItemCard({ - required String title, - required String subtitle, - required VoidCallback onRestore, - required VoidCallback onDelete, - }) { + Widget _buildFilterRow() { return Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: Color(0xFFEEEEEE), width: 0.5)), ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Row( - children: [ - // 左侧图标 - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - child: const Icon( - Icons.delete_outline, - color: Color(0xFF999999), - size: 20, - ), - ), - const SizedBox(width: 12), - // 中间内容区域 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - subtitle, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF999999), - ), - ), - ], - ), - ), - // 右侧操作按钮 - Row( - mainAxisSize: MainAxisSize.min, - children: [ - // 恢复按钮 - _buildActionButton( - icon: Icons.restore, - color: const Color(0xFF1A1A1A), - tooltip: '恢复', - onTap: onRestore, - ), - const SizedBox(width: 8), - // 彻底删除按钮 - _buildActionButton( - icon: Icons.delete_forever, - color: Colors.red, - tooltip: '彻底删除', - onTap: onDelete, - ), - ], - ), - ], - ), + child: Wrap( + spacing: 8, + children: [ + _filterChip('全部', null), + _filterChip('影视', _ItemType.movie), + _filterChip('书籍', _ItemType.book), + _filterChip('笔记', _ItemType.note), + ], ), ); } - /// 构建操作按钮 - Widget _buildActionButton({ - required IconData icon, - required Color color, - required String tooltip, - required VoidCallback onTap, - }) { - return Tooltip( - message: tooltip, + Widget _filterChip(String label, _ItemType? type) { + final active = _filterType == type; + final count = type == null ? _allItems.length : _allItems.where((i) => i.type == type).length; + return GestureDetector( + onTap: () => setState(() => _filterType = type), child: Container( - width: 36, - height: 36, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), + color: active ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(16), ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(8), - child: Icon( - icon, - color: color, - size: 18, - ), + child: Text( + '$label · $count', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: active ? Colors.white : const Color(0xFF888888), ), ), ), ); } - Widget _buildEmptyState(String message) { + Widget _buildCard(_DeletedItem item) { + return Dismissible( + key: Key('${item.type.name}_${item.id}'), + direction: DismissDirection.endToStart, + background: _buildDismissBackground(), + confirmDismiss: (_) async => _showConfirmDialog('确定要彻底删除吗?此操作不可恢复。'), + onDismissed: (_) => _permanentDelete(item), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFFAFAFA), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5), + ), + child: Icon(item.icon, size: 20, color: const Color(0xFF888888)), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: Text( + item.title, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A), height: 1.3), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3), + border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), + ), + child: Text( + item.typeLabel, + style: const TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: Color(0xFF999999)), + ), + ), + ], + ), + const SizedBox(height: 3), + Text( + item.subtitle, + style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)), + ), + ], + ), + ), + const SizedBox(width: 8), + _actionBtn(Icons.restore, '恢复', const Color(0xFF1A1A1A), () => _restore(item)), + const SizedBox(width: 6), + _actionBtn(Icons.delete_outline, '删除', Colors.red, () => _permanentDelete(item)), + ], + ), + ), + ), + ); + } + + Widget _buildDismissBackground() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 24), + child: const Icon(Icons.delete_forever, color: Colors.white, size: 22), + ); + } + + Widget _actionBtn(IconData icon, String tooltip, Color color, VoidCallback onTap) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(6), + child: Tooltip( + message: tooltip, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5), + ), + child: Icon(icon, size: 16, color: color), + ), + ), + ), + ); + } + + Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - width: 80, - height: 80, + width: 72, + height: 72, decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(20), - ), - child: const Icon( - Icons.delete_outline, - size: 40, - color: Color(0xFFCCCCCC), + color: const Color(0xFFF8F8F8), + borderRadius: BorderRadius.circular(18), ), + child: const Icon(Icons.delete_outline, size: 32, color: Color(0xFFD5D5D5)), ), - const SizedBox(height: 20), + const SizedBox(height: 16), Text( - message, - style: const TextStyle( - fontSize: 15, - color: Color(0xFF999999), - ), - ), - const SizedBox(height: 8), - const Text( - '删除的项目将显示在这里', - style: TextStyle( - fontSize: 13, - color: Color(0xFFBBBBBB), - ), + _filterType == null ? '回收站是空的' : '没有删除的项目', + style: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)), ), + const SizedBox(height: 4), + const Text('删除的项目会显示在这里', style: TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))), ], ), ); } - String _formatDate(DateTime date) { - return '${date.year}.${date.month.toString().padLeft(2, '0')}.${date.day.toString().padLeft(2, '0')}'; - } - - /// 恢复影视 - Future _restoreMovie(Movie movie) async { - await context.read().restoreMovie(movie.id); + Future _restore(_DeletedItem item) async { + final provider = context.read(); + switch (item.type) { + case _ItemType.movie: + await provider.restoreMovie(item.id); + if (mounted) ToastUtil.show(context, '影视已恢复'); + case _ItemType.book: + await provider.restoreBook(item.id); + if (mounted) ToastUtil.show(context, '书籍已恢复'); + case _ItemType.note: + await provider.restoreNote(item.id); + if (mounted) ToastUtil.show(context, '笔记已恢复'); + } _loadDeletedItems(); - if (mounted) { - ToastUtil.show(context, '影视已恢复'); - } } - /// 彻底删除影视 - Future _permanentDeleteMovie(Movie movie) async { - final confirmed = await _showConfirmDialog('确定要彻底删除这部影视吗?此操作不可恢复。'); - if (confirmed) { - await context.read().permanentDeleteMovie(movie.id); - _loadDeletedItems(); - if (mounted) { - ToastUtil.show(context, '已彻底删除'); - } + Future _permanentDelete(_DeletedItem item) async { + final confirmed = await _showConfirmDialog('确定要彻底删除吗?此操作不可恢复。'); + if (!confirmed) return; + final provider = context.read(); + switch (item.type) { + case _ItemType.movie: + await provider.permanentDeleteMovie(item.id); + case _ItemType.book: + await provider.permanentDeleteBook(item.id); + case _ItemType.note: + await provider.permanentDeleteNote(item.id); } - } - - /// 恢复书籍 - Future _restoreBook(Book book) async { - await context.read().restoreBook(book.id); _loadDeletedItems(); - if (mounted) { - ToastUtil.show(context, '书籍已恢复'); - } + if (mounted) ToastUtil.show(context, '已彻底删除'); } - /// 彻底删除书籍 - Future _permanentDeleteBook(Book book) async { - final confirmed = await _showConfirmDialog('确定要彻底删除这本书籍吗?此操作不可恢复。'); - if (confirmed) { - await context.read().permanentDeleteBook(book.id); - _loadDeletedItems(); - if (mounted) { - ToastUtil.show(context, '已彻底删除'); - } - } - } - - /// 恢复笔记 - Future _restoreNote(Note note) async { - await context.read().restoreNote(note.id); - _loadDeletedItems(); - if (mounted) { - ToastUtil.show(context, '笔记已恢复'); - } - } - - /// 彻底删除笔记 - Future _permanentDeleteNote(Note note) async { - final confirmed = await _showConfirmDialog('确定要彻底删除这条笔记吗?此操作不可恢复。'); - if (confirmed) { - await context.read().permanentDeleteNote(note.id); - _loadDeletedItems(); - if (mounted) { - ToastUtil.show(context, '已彻底删除'); - } - } - } - - /// 显示确认对话框 Future _showConfirmDialog(String message) async { final result = await showDialog( context: context, - builder: (context) => AlertDialog( + builder: (ctx) => AlertDialog( backgroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: const Text( - '确认删除', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - content: Text( - message, - style: const TextStyle( - fontSize: 14, - color: Color(0xFF666666), - height: 1.5, - ), - ), + title: const Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), + content: Text(message, style: const TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5)), actions: [ TextButton( - onPressed: () => Navigator.pop(context, false), - style: TextButton.styleFrom( - foregroundColor: const Color(0xFF666666), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), + onPressed: () => Navigator.pop(ctx, false), + style: TextButton.styleFrom(foregroundColor: const Color(0xFF666666)), child: const Text('取消'), ), ElevatedButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.pop(ctx, true), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: const Text('删除'), ), @@ -436,61 +375,43 @@ class _RecycleBinPageState extends State with SingleTickerProvid return result ?? false; } - /// 显示清空全部对话框 void _showClearAllDialog() { + final pageContext = context; showDialog( - context: context, - builder: (context) => AlertDialog( + context: pageContext, + builder: (ctx) => AlertDialog( backgroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), title: const Row( children: [ - Icon(Icons.warning_amber, color: Colors.red, size: 24), + Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22), SizedBox(width: 8), - Text( - '清空回收站', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), + Text('清空回收站', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), ], ), content: const Text( '确定要清空回收站吗?所有项目将被彻底删除,此操作不可恢复。', - style: TextStyle( - fontSize: 14, - color: Color(0xFF666666), - height: 1.5, - ), + style: TextStyle(fontSize: 14, color: Color(0xFF666666), height: 1.5), ), actions: [ TextButton( - onPressed: () => Navigator.pop(context), - style: TextButton.styleFrom( - foregroundColor: const Color(0xFF666666), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), + onPressed: () => Navigator.pop(ctx), + style: TextButton.styleFrom(foregroundColor: const Color(0xFF666666)), child: const Text('取消'), ), ElevatedButton( onPressed: () async { - Navigator.pop(context); - await context.read().clearRecycleBin(); + Navigator.pop(ctx); + await pageContext.read().clearRecycleBin(); _loadDeletedItems(); - if (mounted) { - ToastUtil.show(context, '回收站已清空'); - } + if (mounted) ToastUtil.show(pageContext, '回收站已清空'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: const Text('清空'), ), diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index 3542ed1..61c936e 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -578,8 +578,6 @@ class _SearchPageState extends State { } Widget _buildNoteItem(Note note) { - final isPlainText = note.contentType == 'plain_text'; - return GestureDetector( onTap: () { Navigator.push( @@ -611,7 +609,7 @@ class _SearchPageState extends State { border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), ), child: Text( - isPlainText ? 'TXT' : 'MD', + 'MD', style: const TextStyle( fontSize: 10, fontWeight: FontWeight.w600, diff --git a/lib/pages/statistics_page.dart b/lib/pages/statistics_page.dart index b43e23c..65a9cc6 100644 --- a/lib/pages/statistics_page.dart +++ b/lib/pages/statistics_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; +import '../utils/user_prefs.dart'; /// 数据统计页面 - 多维度数据分析 class StatisticsPage extends StatefulWidget { @@ -14,6 +15,10 @@ class StatisticsPage extends StatefulWidget { class _StatisticsPageState extends State { int _selectedTab = 0; + bool get _showMovies => UserPrefs().showMovieTab; + bool get _showBooks => UserPrefs().showBookTab; + bool get _showNotes => UserPrefs().showNoteTab; + @override Widget build(BuildContext context) { return Scaffold( @@ -29,9 +34,7 @@ class _StatisticsPageState extends State { return Column( children: [ - // 标签切换栏 _buildTabBar(), - // 内容区域 Expanded( child: _selectedTab == 0 ? _buildOverviewTab(movies, books, notes) @@ -44,25 +47,19 @@ class _StatisticsPageState extends State { ); } - /// 构建标签栏 Widget _buildTabBar() { return Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: Row( children: [ - Expanded( - child: _buildTabItem('概览', 0), - ), + Expanded(child: _buildTabItem('概览', 0)), const SizedBox(width: 12), - Expanded( - child: _buildTabItem('趋势', 1), - ), + Expanded(child: _buildTabItem('趋势', 1)), ], ), ); } - /// 构建标签项 Widget _buildTabItem(String label, int index) { final isSelected = _selectedTab == index; return GestureDetector( @@ -86,199 +83,182 @@ class _StatisticsPageState extends State { ); } - /// 概览标签页 Widget _buildOverviewTab(List movies, List books, List notes) { final totalMovies = movies.where((m) => !m.isDeleted).length; final totalBooks = books.length; final totalNotes = notes.length; - + final watchedMovies = movies.where((m) => m.status == 'watched' && !m.isDeleted).length; final watchingMovies = movies.where((m) => m.status == 'watching' && !m.isDeleted).length; final wantToWatchMovies = movies.where((m) => m.status == 'want_to_watch' && !m.isDeleted).length; - + final readBooks = books.where((b) => b.status == 'read').length; final readingBooks = books.where((b) => b.status == 'reading').length; final wantToReadBooks = books.where((b) => b.status == 'want_to_read').length; + final children = []; + + // 数据总览 + children.add(_buildSectionTitle('数据总览')); + children.add(const SizedBox(height: 16)); + children.add(_buildTotalOverview(totalMovies, totalBooks, totalNotes)); + children.add(const SizedBox(height: 32)); + + // 影视状态分布 + if (_showMovies) { + children.add(_buildSectionTitle('影视状态分布')); + children.add(const SizedBox(height: 16)); + children.add(_buildStatusDistribution([ + _StatusData('已看', watchedMovies, const Color(0xFF1A1A1A)), + _StatusData('在看', watchingMovies, const Color(0xFF666666)), + _StatusData('想看', wantToWatchMovies, const Color(0xFF999999)), + ])); + children.add(const SizedBox(height: 32)); + } + + // 书籍状态分布 + if (_showBooks) { + children.add(_buildSectionTitle('书籍状态分布')); + children.add(const SizedBox(height: 16)); + children.add(_buildStatusDistribution([ + _StatusData('已读', readBooks, const Color(0xFF1A1A1A)), + _StatusData('在读', readingBooks, const Color(0xFF666666)), + _StatusData('想读', wantToReadBooks, const Color(0xFF999999)), + ])); + children.add(const SizedBox(height: 32)); + } + + // 最近活动 + children.add(_buildSectionTitle('最近7天活动')); + children.add(const SizedBox(height: 16)); + children.add(_buildRecentActivity(movies, books, notes)); + return ListView( padding: const EdgeInsets.all(24), - children: [ - // 总数据卡片 - _buildSectionTitle('数据总览'), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: const Color(0xFFF8F8F8), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - Expanded(child: _buildStatItem('影视', totalMovies, Icons.movie_outlined)), - Expanded(child: _buildStatItem('书籍', totalBooks, Icons.menu_book_outlined)), - Expanded(child: _buildStatItem('笔记', totalNotes, Icons.note_outlined)), - ], - ), - ), - const SizedBox(height: 32), - - // 影视状态分布 - _buildSectionTitle('影视状态分布'), - const SizedBox(height: 16), - _buildStatusDistribution([ - _StatusData('已看', watchedMovies, const Color(0xFF1A1A1A)), - _StatusData('在看', watchingMovies, const Color(0xFF666666)), - _StatusData('想看', wantToWatchMovies, const Color(0xFF999999)), - ]), - const SizedBox(height: 32), - - // 书籍状态分布 - _buildSectionTitle('书籍状态分布'), - const SizedBox(height: 16), - _buildStatusDistribution([ - _StatusData('已读', readBooks, const Color(0xFF1A1A1A)), - _StatusData('在读', readingBooks, const Color(0xFF666666)), - _StatusData('想读', wantToReadBooks, const Color(0xFF999999)), - ]), - const SizedBox(height: 32), - - // 最近活动 - _buildSectionTitle('最近7天活动'), - const SizedBox(height: 16), - _buildRecentActivity(movies, books, notes), - ], + children: children, + ); + } + + Widget _buildTotalOverview(int totalMovies, int totalBooks, int totalNotes) { + final items = []; + if (_showMovies) items.add(Expanded(child: _buildStatItem('影视', totalMovies, Icons.movie_outlined))); + if (_showBooks) items.add(Expanded(child: _buildStatItem('书籍', totalBooks, Icons.menu_book_outlined))); + if (_showNotes) items.add(Expanded(child: _buildStatItem('笔记', totalNotes, Icons.note_outlined))); + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFFF8F8F8), + borderRadius: BorderRadius.circular(12), + ), + child: Row(children: items), ); } - /// 趋势标签页 Widget _buildTrendTab(List movies, List books, List notes) { final now = DateTime.now(); - final thirtyDaysAgo = now.subtract(const Duration(days: 30)); - - // 生成最近30天的日期列表 - final dates = List.generate(30, (index) { - return now.subtract(Duration(days: 29 - index)); - }); - - // 计算每天的新增数量 + + final dates = List.generate(30, (index) => now.subtract(Duration(days: 29 - index))); + final movieTrend = dates.map((date) { return movies.where((m) { final created = m.createdAt; - return created.year == date.year && - created.month == date.month && - created.day == date.day && - !m.isDeleted; + return created.year == date.year && created.month == date.month && created.day == date.day && !m.isDeleted; }).length; }).toList(); - + final bookTrend = dates.map((date) { return books.where((b) { final created = b.createdAt; - return created.year == date.year && - created.month == date.month && - created.day == date.day; + return created.year == date.year && created.month == date.month && created.day == date.day; }).length; }).toList(); - + final noteTrend = dates.map((date) { return notes.where((n) { final created = n.createdAt; - return created.year == date.year && - created.month == date.month && - created.day == date.day; + return created.year == date.year && created.month == date.month && created.day == date.day; }).length; }).toList(); - - // 计算总数 - final totalMovies = movieTrend.reduce((a, b) => a + b); - final totalBooks = bookTrend.reduce((a, b) => a + b); - final totalNotes = noteTrend.reduce((a, b) => a + b); + + final totalMovies = movieTrend.fold(0, (a, b) => a + b); + final totalBooks = bookTrend.fold(0, (a, b) => a + b); + final totalNotes = noteTrend.fold(0, (a, b) => a + b); + + final children = []; + + children.add(_buildSectionTitle('近30天新增')); + children.add(const SizedBox(height: 16)); + children.add(_buildTrendSummaryRow(totalMovies, totalBooks, totalNotes)); + children.add(const SizedBox(height: 32)); + children.add(_buildSectionTitle('数据趋势')); + children.add(const SizedBox(height: 16)); + children.add(_buildTrendChart(movieTrend, bookTrend, noteTrend)); return ListView( padding: const EdgeInsets.all(24), - children: [ - // 30天统计摘要 - _buildSectionTitle('近30天新增'), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: const Color(0xFFF8F8F8), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - Expanded(child: _buildTrendSummary('影视', totalMovies)), - Expanded(child: _buildTrendSummary('书籍', totalBooks)), - Expanded(child: _buildTrendSummary('笔记', totalNotes)), - ], - ), - ), - const SizedBox(height: 32), - - // 趋势说明 - _buildSectionTitle('数据趋势'), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - children: [ - _buildTrendRow('影视', movieTrend, const Color(0xFF1A1A1A)), - const SizedBox(height: 16), - _buildTrendRow('书籍', bookTrend, const Color(0xFF666666)), - const SizedBox(height: 16), - _buildTrendRow('笔记', noteTrend, const Color(0xFF999999)), - ], - ), - ), - ], + children: children, + ); + } + + Widget _buildTrendSummaryRow(int totalMovies, int totalBooks, int totalNotes) { + final items = []; + if (_showMovies) items.add(Expanded(child: _buildTrendSummary('影视', totalMovies))); + if (_showBooks) items.add(Expanded(child: _buildTrendSummary('书籍', totalBooks))); + if (_showNotes) items.add(Expanded(child: _buildTrendSummary('笔记', totalNotes))); + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFFF8F8F8), + borderRadius: BorderRadius.circular(12), + ), + child: Row(children: items), + ); + } + + Widget _buildTrendChart(List movieTrend, List bookTrend, List noteTrend) { + final rows = []; + if (_showMovies) { + rows.add(_buildTrendRow('影视', movieTrend, const Color(0xFF1A1A1A))); + } + if (_showBooks) { + if (rows.isNotEmpty) rows.add(const SizedBox(height: 16)); + rows.add(_buildTrendRow('书籍', bookTrend, const Color(0xFF666666))); + } + if (_showNotes) { + if (rows.isNotEmpty) rows.add(const SizedBox(height: 16)); + rows.add(_buildTrendRow('笔记', noteTrend, const Color(0xFF999999))); + } + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFFFAFAFA), + borderRadius: BorderRadius.circular(12), + ), + child: Column(children: rows), ); } - /// 构建趋势摘要项 Widget _buildTrendSummary(String label, int count) { return Column( children: [ - Text( - count.toString(), - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.w700, - color: Color(0xFF1A1A1A), - ), - ), + Text(count.toString(), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A))), const SizedBox(height: 4), - Text( - label, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF666666), - ), - ), + Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF666666))), ], ); } - /// 构建趋势行 Widget _buildTrendRow(String label, List data, Color color) { final maxValue = data.isEmpty ? 1 : data.reduce((a, b) => a > b ? a : b); final safeMax = maxValue == 0 ? 1 : maxValue; - + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: color, - ), - ), + Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: color)), const SizedBox(height: 8), SizedBox( height: 40, @@ -303,33 +283,21 @@ class _StatisticsPageState extends State { ); } - /// 构建统计项 Widget _buildStatItem(String title, int count, IconData icon) { return Column( children: [ Icon(icon, size: 24, color: const Color(0xFF666666)), const SizedBox(height: 8), - Text( - count.toString(), - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.w700, - color: Color(0xFF1A1A1A), - ), - ), + Text(count.toString(), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A))), const SizedBox(height: 4), - Text( - title, - style: const TextStyle(fontSize: 13, color: Color(0xFF666666)), - ), + Text(title, style: const TextStyle(fontSize: 13, color: Color(0xFF666666))), ], ); } - /// 构建状态分布 Widget _buildStatusDistribution(List<_StatusData> data) { final total = data.fold(0, (sum, item) => sum + item.count); - + return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -344,38 +312,15 @@ class _StatisticsPageState extends State { child: Row( children: [ Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: item.color, - borderRadius: BorderRadius.circular(2), - ), + width: 10, height: 10, + decoration: BoxDecoration(color: item.color, borderRadius: BorderRadius.circular(2)), ), const SizedBox(width: 12), - Text( - item.label, - style: const TextStyle( - fontSize: 14, - color: Color(0xFF1A1A1A), - ), - ), + Text(item.label, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))), const Spacer(), - Text( - '${item.count}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), + Text('${item.count}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(width: 8), - Text( - '($percentage%)', - style: const TextStyle( - fontSize: 13, - color: Color(0xFF999999), - ), - ), + Text('($percentage%)', style: const TextStyle(fontSize: 13, color: Color(0xFF999999))), ], ), ); @@ -384,30 +329,34 @@ class _StatisticsPageState extends State { ); } - /// 构建最近活动 Widget _buildRecentActivity(List movies, List books, List notes) { final now = DateTime.now(); final sevenDaysAgo = now.subtract(const Duration(days: 7)); - + final recentMovies = movies.where((m) => m.createdAt.isAfter(sevenDaysAgo) && !m.isDeleted).length; final recentBooks = books.where((b) => b.createdAt.isAfter(sevenDaysAgo)).length; final recentNotes = notes.where((n) => n.createdAt.isAfter(sevenDaysAgo)).length; + final items = []; + if (_showMovies) { + items.add(_buildActivityRow('新增影视', recentMovies, Icons.movie_outlined)); + } + if (_showBooks) { + if (items.isNotEmpty) items.add(const Divider(height: 20, color: Color(0xFFE8E8E8))); + items.add(_buildActivityRow('新增书籍', recentBooks, Icons.menu_book_outlined)); + } + if (_showNotes) { + if (items.isNotEmpty) items.add(const Divider(height: 20, color: Color(0xFFE8E8E8))); + items.add(_buildActivityRow('新增笔记', recentNotes, Icons.note_outlined)); + } + return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(12), ), - child: Column( - children: [ - _buildActivityRow('新增影视', recentMovies, Icons.movie_outlined), - const Divider(height: 20, color: Color(0xFFE8E8E8)), - _buildActivityRow('新增书籍', recentBooks, Icons.menu_book_outlined), - const Divider(height: 20, color: Color(0xFFE8E8E8)), - _buildActivityRow('新增笔记', recentNotes, Icons.note_outlined), - ], - ), + child: Column(children: items), ); } @@ -415,8 +364,7 @@ class _StatisticsPageState extends State { return Row( children: [ Container( - width: 36, - height: 36, + width: 36, height: 36, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), @@ -425,30 +373,11 @@ class _StatisticsPageState extends State { child: Icon(icon, size: 18, color: const Color(0xFF666666)), ), const SizedBox(width: 12), - Text( - label, - style: const TextStyle( - fontSize: 14, - color: Color(0xFF1A1A1A), - ), - ), + Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))), const Spacer(), - Text( - '$count', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), + Text('$count', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(width: 4), - const Text( - '个', - style: TextStyle( - fontSize: 13, - color: Color(0xFF999999), - ), - ), + const Text('个', style: TextStyle(fontSize: 13, color: Color(0xFF999999))), ], ); } @@ -457,34 +386,20 @@ class _StatisticsPageState extends State { return Row( children: [ Container( - width: 4, - height: 16, - decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), - borderRadius: BorderRadius.circular(2), - ), + width: 4, height: 16, + decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(2)), ), const SizedBox(width: 8), - Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), + Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), ], ); } } -/// 状态数据 class _StatusData { final String label; final int count; final Color color; - + _StatusData(this.label, this.count, this.color); } - - diff --git a/lib/widgets/note_list_item.dart b/lib/widgets/note_list_item.dart index 1b60056..1cac643 100644 --- a/lib/widgets/note_list_item.dart +++ b/lib/widgets/note_list_item.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; @@ -97,17 +98,18 @@ class _NoteListItemContent extends StatelessWidget { const SizedBox(height: 4), ], - // 内容摘要(去除Markdown标记) - Text( - _cleanMarkdown(note.content).trim(), - style: const TextStyle( - fontSize: 13, - color: Color(0xFF666666), - height: 1.5, + // 内容摘要(去除Markdown标记),内容为空则不显示 + if (_cleanMarkdown(note.content).trim().isNotEmpty) + Text( + _cleanMarkdown(note.content).trim(), + style: const TextStyle( + fontSize: 13, + color: Color(0xFF666666), + height: 1.5, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), // 底部标签 if (note.tags.isNotEmpty) ...[ @@ -115,7 +117,7 @@ class _NoteListItemContent extends StatelessWidget { Wrap( spacing: 6, runSpacing: 4, - children: note.tags.take(3).map((tag) { + children: note.tags.map((tag) { return Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( @@ -134,12 +136,61 @@ class _NoteListItemContent extends StatelessWidget { }).toList(), ), ], + + // 图片预览 + if (note.images.isNotEmpty) ...[ + const SizedBox(height: 8), + _buildImagePreviewRow(), + ], ], ), ), ); } + /// 图片预览行 + Widget _buildImagePreviewRow() { + final images = note.images; + final count = images.length.clamp(0, 3); + return Row( + children: [ + for (int i = 0; i < count; i++) + Container( + width: 48, + height: 48, + margin: const EdgeInsets.only(right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), + ), + clipBehavior: Clip.antiAlias, + child: Image.file( + File(images[i]), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: const Color(0xFFF5F5F5), + ), + ), + ), + if (images.length > 3) + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(6), + ), + child: Center( + child: Text( + '+${images.length - 3}', + style: const TextStyle(fontSize: 12, color: Color(0xFF999999), fontWeight: FontWeight.w500), + ), + ), + ), + ], + ); + } + /// 清理 Markdown 标记,提取纯文本 String _cleanMarkdown(String text) { return text