import 'package:flutter/material.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../widgets/fade_in_local_image.dart'; import '../../models/data_models.dart'; import 'note_share_page.dart'; /// 笔记详情页 class NoteDetailPage extends StatefulWidget { final Note note; final bool embedded; const NoteDetailPage({super.key, required this.note, this.embedded = false}); @override State createState() => _NoteDetailPageState(); } class _NoteDetailPageState extends State { static const _weekdays = ['一', '二', '三', '四', '五', '六', '日']; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final note = context.watch().notes.firstWhere( (n) => n.id == widget.note.id, orElse: () => widget.note, ); return Scaffold( backgroundColor: colors.surface, appBar: AppBar( leading: widget.embedded ? IconButton( icon: const Icon(Icons.close), onPressed: () => context.read().selectNote(null), ) : null, titleSpacing: 0, title: Text( note.title.isNotEmpty ? note.title : _truncateContent(note.content), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), body: Stack( children: [ Column( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: colors.outlineVariant, width: 0.5), ), ), child: Row( children: [ Text( '${note.createdAt.day}', style: TextStyle( fontSize: 30, fontWeight: FontWeight.w200, color: colors.onSurface.withValues(alpha: 0.75), 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: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.55)), ), const SizedBox(height: 1), Text( '${note.createdAt.hour.toString().padLeft(2, '0')}:${note.createdAt.minute.toString().padLeft(2, '0')}', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)), ), ], ), const Spacer(), Text( '${note.content.length} 字', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35)), ), ], ), ), if (note.tags.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 6), child: _buildTagRow(note.tags), ), Expanded( child: Markdown( data: note.content, styleSheet: _buildMarkdownStyleSheet(colors), 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(), ), ], ), ); } Widget _buildTagRow(List tags) { final colors = Theme.of(context).colorScheme; 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: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), ), child: Text( tags[index], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6)), ), ); }, ), ); } Widget _buildImageRow(List images) { final colors = Theme.of(context).colorScheme; return Container( height: 80, decoration: BoxDecoration( color: colors.surface, border: Border( top: BorderSide(color: colors.outlineVariant, 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: colors.outlineVariant, width: 0.5), ), clipBehavior: Clip.antiAlias, child: FadeInLocalImage( path: images[index], fit: BoxFit.cover, errorWidget: Container( color: colors.surfaceContainerHighest, child: Icon(Icons.broken_image_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.25)), ), ), ), ); }, ), ); } MarkdownStyleSheet _buildMarkdownStyleSheet(ColorScheme colors) { return MarkdownStyleSheet( h1: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4), h2: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4), h3: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4), h4: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.4), p: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.75), height: 1.8), code: TextStyle(fontSize: 14, color: colors.onSurface, backgroundColor: colors.surfaceContainerHighest, fontFamily: 'monospace'), codeblockDecoration: BoxDecoration( color: colors.surfaceContainerHigh, border: Border.all(color: colors.outline), borderRadius: BorderRadius.circular(6), ), codeblockPadding: const EdgeInsets.all(12), blockquote: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.6), fontStyle: FontStyle.italic, height: 1.8), blockquoteDecoration: BoxDecoration( border: Border(left: BorderSide(color: colors.onSurface.withValues(alpha: 0.4), width: 4)), ), blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), listBullet: TextStyle(fontSize: 15, color: colors.onSurface), listIndent: 24, a: const TextStyle(fontSize: 15, color: Color(0xFF4A90D9), decoration: TextDecoration.underline), tableHead: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface), tableBody: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.75)), tableBorder: TableBorder.all(color: colors.outline, width: 0.5), tableColumnWidth: const FlexColumnWidth(), tableCellsDecoration: BoxDecoration(color: colors.surface), tablePadding: const EdgeInsets.all(8), strong: TextStyle(fontWeight: FontWeight.w600, color: colors.onSurface), em: TextStyle(fontStyle: FontStyle.italic, color: colors.onSurface.withValues(alpha: 0.75)), del: TextStyle(decoration: TextDecoration.lineThrough, color: colors.onSurface.withValues(alpha: 0.4)), ); } Widget _buildMarkdownImage(Uri uri, Note note) { final path = uri.toString(); if (path.isEmpty) return const SizedBox.shrink(); for (final imgPath in note.images) { if (imgPath.contains(path) || path.contains(imgPath)) { return ClipRRect( borderRadius: BorderRadius.circular(8), child: FadeInLocalImage(path: imgPath, fit: BoxFit.cover), ); } } if (path.startsWith('http')) { return ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network(path, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const SizedBox.shrink()), ); } return const SizedBox.shrink(); } void _showImagePreview(List images, int initialIndex) { showDialog( context: context, barrierDismissible: true, builder: (context) => GestureDetector( onTap: () => Navigator.pop(context), child: Container( color: Colors.black.withValues(alpha: 0.9), child: Center( child: InteractiveViewer( panEnabled: true, boundaryMargin: const EdgeInsets.all(20), minScale: 0.5, maxScale: 4, child: FadeInLocalImage(path: images[initialIndex], fit: BoxFit.contain), ), ), ), ), ); } 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, ); final provider = context.read(); Navigator.pushNamed(context, '/note-form', arguments: currentNote).then((_) async { await provider.loadNotes(); }); } Widget _buildFloatingActionButtons() { final colors = Theme.of(context).colorScheme; return Column( mainAxisSize: MainAxisSize.min, children: [ _buildFloatingButton( icon: Icons.edit_outlined, onPressed: () => _navigateToEdit(context), tooltip: '编辑', backgroundColor: colors.primary, foregroundColor: colors.onPrimary, ), const SizedBox(height: 12), _buildFloatingButton( icon: Icons.delete_outline, onPressed: () => _showDeleteDialog(context), tooltip: '删除', backgroundColor: colors.error, foregroundColor: colors.onError, ), const SizedBox(height: 12), _buildFloatingButton( icon: Icons.share_outlined, onPressed: _shareNote, tooltip: '分享', backgroundColor: const Color(0xFF4CAF50), foregroundColor: Colors.white, ), ], ); } Widget _buildFloatingButton({ required IconData icon, required VoidCallback onPressed, required String tooltip, required Color backgroundColor, required Color foregroundColor, }) { return Tooltip( message: tooltip, child: GestureDetector( onTap: onPressed, child: Container( 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: Icon(icon, size: 18, color: foregroundColor), ), ), ); } void _showDeleteDialog(BuildContext context) { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('删除笔记'), content: const Text('删除后将移至回收站,确定要删除吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('取消'), ), TextButton( onPressed: () { Navigator.pop(ctx); context.read().removeNote(widget.note.id); if (widget.embedded) { context.read().selectNote(null); } else { Navigator.pop(context); } }, child: Text('删除', style: TextStyle(color: Theme.of(context).colorScheme.error)), ), ], ), ); } void _shareNote() { Navigator.push( context, MaterialPageRoute(builder: (context) => NoteSharePage(note: widget.note)), ); } }