import 'dart:async'; import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; // 复制功能需要 import 'package:win_text_editor/app/providers/editor_provider.dart'; import 'package:file_picker/file_picker.dart'; import 'dart:io'; import 'package:flutter_highlight/flutter_highlight.dart'; import 'package:win_text_editor/app/providers/logger.dart'; import 'package:win_text_editor/app/utils/file_utils.dart'; class TextTab extends StatefulWidget { final String tabId; const TextTab({super.key, required this.tabId}); @override State createState() => _TextTabState(); } class _TextTabState extends State { late TextEditingController _controller; late EditorProvider _provider; late FocusNode _focusNode; String _language = 'plaintext'; bool _showFullContent = false; final int _displayThreshold = 50000; // 显示阈值 final Map _visibleLines = {}; final ScrollController _scrollController = ScrollController(); final double _lineHeight = 20.0; // 单行高度 int _firstVisibleLine = 0; int _lastVisibleLine = 0; @override void initState() { super.initState(); _provider = Provider.of(context, listen: false); _controller = TextEditingController(text: _getCurrentContent()); _focusNode = FocusNode(); _scrollController.addListener(_calculateVisibleLines); _detectLanguage(); // 初始加载可见区域 WidgetsBinding.instance.addPostFrameCallback((_) { _calculateVisibleLines(); }); } void _calculateVisibleLines() { final scrollOffset = _scrollController.offset; final viewportHeight = _scrollController.position.viewportDimension; _firstVisibleLine = (scrollOffset / _lineHeight).floor(); _lastVisibleLine = ((scrollOffset + viewportHeight) / _lineHeight).ceil(); // 初始加载时至少加载前 500 行 if (scrollOffset == 0) { _lastVisibleLine = min(500, _lastVisibleLine); } _preloadLines(); // 预加载附近行 } void _preloadLines() { final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); final lines = tab.fullContent.split('\n'); // 仅缓存可见区域附近的行 final preloadStart = max(0, _firstVisibleLine - 20); final preloadEnd = min(lines.length, _lastVisibleLine + 20); for (int i = preloadStart; i < preloadEnd; i++) { _visibleLines[i] = lines[i]; } } Widget _buildLine(int lineIndex) { final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); final lineContent = _visibleLines[lineIndex] ?? ''; return tab.isHighlightEnabled ? HighlightView( lineContent, language: _language, textStyle: const TextStyle(fontFamily: 'monospace', fontSize: 14), ) : Text(lineContent, style: const TextStyle(fontFamily: 'monospace')); } String _getCurrentContent() { return _provider.tabs.firstWhere((t) => t.id == widget.tabId).fullContent; } void _detectLanguage() { final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); final fileName = tab.title.toLowerCase(); final content = tab.content.trim(); if (fileName.endsWith('.dart')) { _language = 'dart'; } else if (fileName.endsWith('.java')) { _language = 'java'; } else if (fileName.endsWith('.py')) { _language = 'python'; } else if (fileName.endsWith('.js')) { _language = 'javascript'; } else if (fileName.endsWith('.html')) { _language = 'html'; } else if (fileName.endsWith('.css')) { _language = 'css'; } else if (fileName.endsWith('.json')) { _language = 'json'; } else if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) { _language = 'yaml'; } else if (fileName.endsWith('.xml')) { _language = 'xml'; } // 如果扩展名未识别,检查内容特征 else if (_isLikelyXml(content)) { _language = 'xml'; } else { _language = 'plaintext'; } } bool _isLikelyXml(String content) { if (content.isEmpty) return false; // 简单检查:内容以'<'开头,以'>'结尾 final trimmed = content.trim(); if (!trimmed.startsWith('<') || !trimmed.endsWith('>')) { return false; } // 更复杂的检查:包含常见的XML标签模式 final xmlTagRegex = RegExp(r'<[^/>]+>.*<\/[^>]+>|<[^/>]+\/>'); return xmlTagRegex.hasMatch(content); } @override void didUpdateWidget(TextTab oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.tabId != widget.tabId) { _visibleLines.clear(); // 修复问题2:清空缓存 _controller.text = _getCurrentContent(); _detectLanguage(); _calculateVisibleLines(); // 重新加载 } } @override void dispose() { _controller.dispose(); _focusNode.dispose(); _scrollController.dispose(); // 添加这行 // 关闭时取消加载 if (_provider.isTabLoading(widget.tabId)) { _provider.cancelLoading(widget.tabId); } super.dispose(); } @override Widget build(BuildContext context) { final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); final progress = _provider.getLoadedChunks(widget.tabId); final totalChunks = (tab.content.length / 10000).ceil(); final lineCount = tab.fullContent.split('\n').length; return Column( children: [ Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.grey[100], border: Border(bottom: BorderSide(color: Colors.grey[300]!)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '源文本${tab.content.isEmpty ? '' : ' (${tab.fileName!.isEmpty ? '' : '${tab.fileName},'}${tab.content.length}字符)'}', style: const TextStyle(fontWeight: FontWeight.bold), ), Row( children: [ IconButton( icon: const Icon(Icons.folder_open, size: 20), tooltip: '打开文件', onPressed: () => _openFile(context), ), IconButton( icon: const Icon(Icons.content_copy, size: 20), tooltip: '复制内容', onPressed: tab.content.isEmpty ? null : () => _copyToClipboard(context, tab.content), ), IconButton( icon: const Icon(Icons.save, size: 20), tooltip: '保存到文件', onPressed: tab.content.isEmpty ? null : () => _saveFile(context, tab.content), ), DropdownButton( value: _language, icon: const Icon(Icons.code, size: 20), underline: Container(), onChanged: (String? newValue) { setState(() { _language = newValue!; }); }, items: [ 'xml', 'plaintext', 'dart', 'java', 'python', 'javascript', 'html', 'css', 'json', 'yaml', ].map>((String value) { return DropdownMenuItem(value: value, child: Text(value)); }).toList(), ), ], ), ], ), ), if (_provider.isTabLoading(widget.tabId)) LinearProgressIndicator(value: progress / totalChunks, minHeight: 2), Expanded( child: ListView.builder( controller: _scrollController, itemCount: lineCount, itemExtent: _lineHeight, // 固定行高优化性能 itemBuilder: (ctx, index) => _buildLine(index), ), ), ], ); } Future _openFile(BuildContext context) async { final filePath = await FileUtils.pickFile(context); if (filePath == null || !mounted) return; try { final content = await FileUtils.readFileContent(context, filePath); if (content == null || !mounted) return; final fileName = filePath.split('\\').last; // 根据文件大小选择加载方式 if (content.length > 100000) { // 超过100k使用分块加载 await _provider.updateContentInChunks(widget.tabId, content, fileName); } else { await _provider.updateContent(widget.tabId, content, fileName); } if (mounted) { setState(() { _controller.text = content; _detectLanguage(); _showFullContent = content.length <= _displayThreshold; _visibleLines.clear(); // 清空缓存 _calculateVisibleLines(); // 重新计算可见行 }); } } catch (e) { // 错误处理保持不变 } } // 复制到剪贴板 Future _copyToClipboard(BuildContext context, String content) async { await Clipboard.setData(ClipboardData(text: content)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); } } // 保存文件模拟功能 // 保存文件功能 Future _saveFile(BuildContext context, String content) async { try { // 让用户选择保存位置 String? outputPath = await FilePicker.platform.saveFile( dialogTitle: '保存文件', fileName: 'untitled.txt', allowedExtensions: ['txt'], type: FileType.any, ); if (outputPath == null) return; final file = File(outputPath); // 检查文件是否存在 if (await file.exists()) { // 如果存在,显示确认对话框 final shouldOverwrite = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('文件已存在'), content: const Text('要覆盖现有文件吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('覆盖'), ), ], ), ); if (shouldOverwrite != true) return; } // 写入文件 await file.writeAsString(content); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已保存到: ${file.path}'))); } } on FileSystemException catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.message}'))); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'))); } } } }