|
|
|
@ -1,3 +1,5 @@
@@ -1,3 +1,5 @@
|
|
|
|
|
import 'dart:async'; |
|
|
|
|
import 'dart:math'; |
|
|
|
|
import 'dart:ui'; |
|
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
@ -7,8 +9,8 @@ import 'package:win_text_editor/app/providers/editor_provider.dart';
@@ -7,8 +9,8 @@ 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:flutter_highlight/themes/github.dart'; |
|
|
|
|
import 'package:flutter_highlight/themes/monokai-sublime.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; |
|
|
|
@ -23,21 +25,76 @@ class _TextTabState extends State<TextTab> {
@@ -23,21 +25,76 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
late TextEditingController _controller; |
|
|
|
|
late EditorProvider _provider; |
|
|
|
|
late FocusNode _focusNode; |
|
|
|
|
late ScrollController _scrollController; |
|
|
|
|
|
|
|
|
|
String _language = 'plaintext'; |
|
|
|
|
|
|
|
|
|
bool _showFullContent = false; |
|
|
|
|
final int _displayThreshold = 50000; // 显示阈值 |
|
|
|
|
|
|
|
|
|
final Map<int, String> _visibleLines = {}; |
|
|
|
|
final ScrollController _scrollController = ScrollController(); |
|
|
|
|
final double _lineHeight = 20.0; // 单行高度 |
|
|
|
|
int _firstVisibleLine = 0; |
|
|
|
|
int _lastVisibleLine = 0; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
void initState() { |
|
|
|
|
super.initState(); |
|
|
|
|
_provider = Provider.of<EditorProvider>(context, listen: false); |
|
|
|
|
_controller = TextEditingController(text: _getCurrentContent()); |
|
|
|
|
_focusNode = FocusNode(); |
|
|
|
|
_scrollController = ScrollController(); // 添加这行 |
|
|
|
|
_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).content; |
|
|
|
|
return _provider.tabs.firstWhere((t) => t.id == widget.tabId).fullContent; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void _detectLanguage() { |
|
|
|
@ -90,8 +147,10 @@ class _TextTabState extends State<TextTab> {
@@ -90,8 +147,10 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
void didUpdateWidget(TextTab oldWidget) { |
|
|
|
|
super.didUpdateWidget(oldWidget); |
|
|
|
|
if (oldWidget.tabId != widget.tabId) { |
|
|
|
|
_visibleLines.clear(); // 修复问题2:清空缓存 |
|
|
|
|
_controller.text = _getCurrentContent(); |
|
|
|
|
_detectLanguage(); |
|
|
|
|
_calculateVisibleLines(); // 重新加载 |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -100,14 +159,19 @@ class _TextTabState extends State<TextTab> {
@@ -100,14 +159,19 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
_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 isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|
|
|
|
|
|
|
|
|
final progress = _provider.getLoadedChunks(widget.tabId); |
|
|
|
|
final totalChunks = (tab.content.length / 10000).ceil(); |
|
|
|
|
final lineCount = tab.fullContent.split('\n').length; |
|
|
|
|
return Column( |
|
|
|
|
children: [ |
|
|
|
|
Container( |
|
|
|
@ -172,81 +236,49 @@ class _TextTabState extends State<TextTab> {
@@ -172,81 +236,49 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
], |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
if (_provider.isTabLoading(widget.tabId)) |
|
|
|
|
LinearProgressIndicator(value: progress / totalChunks, minHeight: 2), |
|
|
|
|
Expanded( |
|
|
|
|
child: ScrollConfiguration( |
|
|
|
|
behavior: ScrollConfiguration.of(context).copyWith( |
|
|
|
|
scrollbars: true, |
|
|
|
|
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, |
|
|
|
|
), |
|
|
|
|
child: SingleChildScrollView( |
|
|
|
|
controller: _scrollController, // 添加滚动控制器 |
|
|
|
|
child: Stack( |
|
|
|
|
children: [ |
|
|
|
|
// 高亮显示的背景文本 |
|
|
|
|
HighlightView( |
|
|
|
|
tab.content, |
|
|
|
|
language: _language, |
|
|
|
|
theme: isDarkMode ? monokaiSublimeTheme : githubTheme, |
|
|
|
|
padding: const EdgeInsets.all(16), |
|
|
|
|
textStyle: const TextStyle(fontFamily: 'monospace', fontSize: 14), |
|
|
|
|
), |
|
|
|
|
// 实际可编辑的TextField |
|
|
|
|
TextField( |
|
|
|
|
controller: _controller, |
|
|
|
|
focusNode: _focusNode, |
|
|
|
|
maxLines: null, |
|
|
|
|
//scrollController: _scrollController, // 使用同一个滚动控制器 |
|
|
|
|
onChanged: (text) => _provider.updateContent(widget.tabId, text, tab.fileName), |
|
|
|
|
decoration: const InputDecoration( |
|
|
|
|
border: InputBorder.none, |
|
|
|
|
contentPadding: EdgeInsets.all(16), |
|
|
|
|
), |
|
|
|
|
style: const TextStyle( |
|
|
|
|
fontFamily: 'monospace', |
|
|
|
|
fontSize: 14, |
|
|
|
|
color: Colors.transparent, |
|
|
|
|
), |
|
|
|
|
cursorColor: Colors.black, |
|
|
|
|
), |
|
|
|
|
], |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
child: ListView.builder( |
|
|
|
|
controller: _scrollController, |
|
|
|
|
itemCount: lineCount, |
|
|
|
|
itemExtent: _lineHeight, // 固定行高优化性能 |
|
|
|
|
itemBuilder: (ctx, index) => _buildLine(index), |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
], |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// _openFile方法现在需要更新控制器 |
|
|
|
|
Future<void> _openFile(BuildContext context) async { |
|
|
|
|
final filePath = await FileUtils.pickFile(context); |
|
|
|
|
if (filePath == null || !mounted) return; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); |
|
|
|
|
final content = await FileUtils.readFileContent(context, filePath); |
|
|
|
|
if (content == null || !mounted) return; |
|
|
|
|
|
|
|
|
|
final fileName = filePath.split('\\').last; |
|
|
|
|
|
|
|
|
|
if (result != null && result.files.single.path != null) { |
|
|
|
|
final file = File(result.files.single.path!); |
|
|
|
|
final content = await file.readAsString(); |
|
|
|
|
// 根据文件大小选择加载方式 |
|
|
|
|
if (content.length > 100000) { |
|
|
|
|
// 超过100k使用分块加载 |
|
|
|
|
await _provider.updateContentInChunks(widget.tabId, content, fileName); |
|
|
|
|
} else { |
|
|
|
|
await _provider.updateContent(widget.tabId, content, fileName); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_provider.updateContent(widget.tabId, content, result.files.first.name); |
|
|
|
|
if (mounted) { |
|
|
|
|
setState(() { |
|
|
|
|
_controller.text = content; |
|
|
|
|
_detectLanguage(); |
|
|
|
|
_showFullContent = content.length <= _displayThreshold; |
|
|
|
|
_visibleLines.clear(); // 清空缓存 |
|
|
|
|
_calculateVisibleLines(); // 重新计算可见行 |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// 强制刷新文本控制器 |
|
|
|
|
if (context.mounted) { |
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加载: ${file.path}'))); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} on FormatException { |
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); |
|
|
|
|
} on FileSystemException catch (e) { |
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}'))); |
|
|
|
|
} catch (e) { |
|
|
|
|
if (context.mounted) { |
|
|
|
|
ScaffoldMessenger.of( |
|
|
|
|
context, |
|
|
|
|
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}'))); |
|
|
|
|
} |
|
|
|
|
// 错误处理保持不变 |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|