Browse Source

大文件分批加载

master
hejl 2 months ago
parent
commit
5176e99285
  1. 95
      win_text_editor/lib/app/providers/editor_provider.dart
  2. 59
      win_text_editor/lib/app/utils/file_utils.dart
  3. 37
      win_text_editor/lib/app/widgets/file_explorer.dart
  4. 162
      win_text_editor/lib/app/widgets/text_tab.dart

95
win_text_editor/lib/app/providers/editor_provider.dart

@ -1,15 +1,25 @@ @@ -1,15 +1,25 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:win_text_editor/app/providers/logger.dart';
class EditorProvider with ChangeNotifier {
final List<EditorTab> _tabs = [];
String? _activeTabId;
bool _isLoading = false;
List<EditorTab> get tabs => _tabs;
String? get activeTabId => _activeTabId;
bool get isLoading => _isLoading;
int _templateTabCounter = 1;
final Map<String, bool> _tabLoadingStates = {};
final Map<String, int> _tabLoadedChunks = {};
bool isTabLoading(String tabId) => _tabLoadingStates[tabId] ?? false;
int getLoadedChunks(String tabId) => _tabLoadedChunks[tabId] ?? 0;
EditorTab? get activeTab {
if (_activeTabId == null) return null;
try {
@ -22,7 +32,14 @@ class EditorProvider with ChangeNotifier { @@ -22,7 +32,14 @@ class EditorProvider with ChangeNotifier {
void addTab() {
final tabId = DateTime.now().millisecondsSinceEpoch.toString();
_tabs.add(EditorTab(id: tabId, title: '模板解析[$_templateTabCounter]', content: ''));
_tabs.add(
EditorTab(
id: tabId,
title: '模板解析[$_templateTabCounter]',
chunks: [], //
content: '',
),
);
_templateTabCounter++;
_activeTabId = tabId;
notifyListeners();
@ -43,27 +60,83 @@ class EditorProvider with ChangeNotifier { @@ -43,27 +60,83 @@ class EditorProvider with ChangeNotifier {
notifyListeners();
}
void updateContent(String tabId, String content, String? name) {
Future<void> updateContent(String tabId, String content, String? name) async {
_isLoading = true;
notifyListeners();
try {
final tab = _tabs.firstWhere((t) => t.id == tabId);
tab.content = content;
final index = _tabs.indexWhere((t) => t.id == tabId);
if (index == -1) throw Exception("Tab not found");
_tabs[index] = EditorTab(
id: _tabs[index].id,
title: _tabs[index].title,
chunks: content.split('\n'), // chunks
fileName: name ?? _tabs[index].fileName,
content: content,
);
} finally {
_isLoading = false;
notifyListeners();
}
}
// EditorProvider
Future<void> updateContentInChunks(String tabId, String fullContent, String? fileName) async {
final lines = fullContent.split('\n');
const linesPerChunk = 1000; // 1000
_tabLoadingStates[tabId] = true;
notifyListeners();
if (name != null) {
tab.fileName = name;
try {
//
final index = _tabs.indexWhere((t) => t.id == tabId);
_tabs[index] = EditorTab(
id: tabId,
title: fileName ?? _tabs[index].title,
chunks: [],
fileName: fileName,
);
//
for (int i = 0; i < lines.length; i += linesPerChunk) {
if (!_tabLoadingStates[tabId]!) break; //
final chunkLines = lines.sublist(i, min(i + linesPerChunk, lines.length));
_tabs[index].chunks!.addAll(chunkLines);
notifyListeners();
await Future.delayed(Duration.zero); // UI更新
}
Logger().debug("内容更新成功,文件:${tab.fileName}, ${tab.content.length}");
} finally {
_tabLoadingStates.remove(tabId);
notifyListeners();
} catch (e) {
Logger().error("更新内容失败: ${e.toString()}", source: 'EditorProvider');
}
}
void cancelLoading(String tabId) {
_tabLoadingStates[tabId] = false;
notifyListeners();
}
}
class EditorTab {
final String id;
String title;
String content;
List<String> chunks; //
String? fileName;
bool isHighlightEnabled;
String content;
EditorTab({
required this.id,
required this.title,
List<String>? chunks, //
this.fileName,
this.isHighlightEnabled = true,
this.content = '',
}) : chunks = chunks ?? []; //
EditorTab({required this.id, required this.title, required this.content, this.fileName});
String get fullContent => chunks.join('\n'); //
}

59
win_text_editor/lib/app/utils/file_utils.dart

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
// file_utils.dart
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:win_text_editor/app/providers/logger.dart';
class FileUtils {
static Future<String?> pickFile(BuildContext context) async {
try {
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false);
return result?.files.single.path;
} catch (e) {
Logger().error('选择文件失败: ${e.toString()}');
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('选择文件失败: ${e.toString()}')));
}
return null;
}
}
static Future<String?> readFileContent(
BuildContext context,
String filePath, {
Duration timeout = const Duration(seconds: 30),
}) async {
try {
final content = await File(filePath).readAsString().timeout(
timeout,
onTimeout: () {
throw TimeoutException('文件加载超时,可能文件过大');
},
);
return content;
} on FormatException {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件')));
}
return null;
} on FileSystemException catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}')));
}
return null;
} catch (e) {
Logger().error('读取文件失败: ${e.toString()}');
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}')));
}
return null;
}
}
// showLoadingDialog方法
}

37
win_text_editor/lib/app/widgets/file_explorer.dart

@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart'; @@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/app/providers/logger.dart';
import 'package:win_text_editor/app/utils/file_utils.dart';
import '../models/file_node.dart';
import '../providers/file_provider.dart';
@ -64,11 +65,26 @@ class _FileExplorerState extends State<FileExplorer> { @@ -64,11 +65,26 @@ class _FileExplorerState extends State<FileExplorer> {
}
}
// _file_explorer.dart中修改_openFileInEditor方法
Future<void> _openFileInEditor(BuildContext context, FileNode node) async {
Logger().info('尝试打开文件: ${node.path}');
final editorProvider = Provider.of<EditorProvider>(context, listen: false);
showDialog(
context: context,
barrierDismissible: false,
builder:
(context) => const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [CircularProgressIndicator(), SizedBox(height: 16), Text('正在加载文件...')],
),
),
);
try {
Logger().info('尝试打开文件: ${node.path}');
final editorProvider = Provider.of<EditorProvider>(context, listen: false);
final content = await File(node.path).readAsString();
final content = await FileUtils.readFileContent(context, node.path);
if (content == null) return;
Logger().debug('文件内容读取成功,长度: ${content.length}');
Logger().debug('当前活动选项卡ID: ${editorProvider.activeTabId}');
@ -79,6 +95,7 @@ class _FileExplorerState extends State<FileExplorer> { @@ -79,6 +95,7 @@ class _FileExplorerState extends State<FileExplorer> {
Logger().info('没有活动选项卡,创建新选项卡');
editorProvider.addTab();
}
//
Logger().info('准备更新选项卡内容');
editorProvider.updateContent(editorProvider.activeTabId!, content, node.name);
@ -86,18 +103,8 @@ class _FileExplorerState extends State<FileExplorer> { @@ -86,18 +103,8 @@ class _FileExplorerState extends State<FileExplorer> {
if (context.mounted) {
Logger().debug('已加载: ${node.name}');
}
} on FormatException {
Logger().warning('文件格式异常: ${node.path}');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件')));
}
} catch (e) {
Logger().error('打开文件失败: ${e.toString()}', source: 'FileExplorer');
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}')));
}
} finally {
if (context.mounted) Navigator.of(context).pop();
}
}

162
win_text_editor/lib/app/widgets/text_tab.dart

@ -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()}')));
}
//
}
}

Loading…
Cancel
Save