From afbfe975882b09f2db3da883ac990f8fd56c2e0a Mon Sep 17 00:00:00 2001 From: hejl Date: Mon, 12 May 2025 17:02:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=96=87=E6=9C=AC=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E7=9A=84=E5=89=A5=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- win_text_editor/lib/app/app.dart | 4 +- .../lib/app/menus/menu_actions.dart | 4 +- win_text_editor/lib/app/models/tab_model.dart | 4 +- ...editor_provider.dart => tab_provider.dart} | 20 +- .../lib/app/widgets/editor_pane.dart | 4 +- .../lib/app/widgets/file_explorer.dart | 4 +- .../lib/app/widgets/template_parser_tab.dart | 301 +---------------- .../lib/app/widgets/text_editor.dart | 313 ++++++++++++++++++ 8 files changed, 349 insertions(+), 305 deletions(-) rename win_text_editor/lib/app/providers/{editor_provider.dart => tab_provider.dart} (87%) create mode 100644 win_text_editor/lib/app/widgets/text_editor.dart diff --git a/win_text_editor/lib/app/app.dart b/win_text_editor/lib/app/app.dart index fe18da7..442c7c4 100644 --- a/win_text_editor/lib/app/app.dart +++ b/win_text_editor/lib/app/app.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:win_text_editor/app/menus/app_menu.dart'; -import 'package:win_text_editor/app/providers/editor_provider.dart'; +import 'package:win_text_editor/app/providers/tab_provider.dart'; import 'package:win_text_editor/app/providers/file_provider.dart'; import 'package:win_text_editor/app/widgets/editor_pane.dart'; import 'package:win_text_editor/app/widgets/file_explorer.dart'; @@ -15,7 +15,7 @@ class AppScaffold extends StatelessWidget { return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => FileProvider()), - ChangeNotifierProvider(create: (_) => EditorProvider()), + ChangeNotifierProvider(create: (_) => TabProvider()), ], child: const Scaffold( body: Column( diff --git a/win_text_editor/lib/app/menus/menu_actions.dart b/win_text_editor/lib/app/menus/menu_actions.dart index 22ed275..c807c24 100644 --- a/win_text_editor/lib/app/menus/menu_actions.dart +++ b/win_text_editor/lib/app/menus/menu_actions.dart @@ -3,7 +3,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:provider/provider.dart'; import 'package:win_text_editor/app/menus/menu_constants.dart'; import 'package:win_text_editor/app/providers/file_provider.dart'; -import 'package:win_text_editor/app/providers/editor_provider.dart'; +import 'package:win_text_editor/app/providers/tab_provider.dart'; import 'package:collection/collection.dart'; import 'dart:io'; @@ -39,7 +39,7 @@ class MenuActions { } static Future _openTemplateParser(BuildContext context) async { - final editorProvider = Provider.of(context, listen: false); + final editorProvider = Provider.of(context, listen: false); // 使用 firstWhereOrNull 查找选项卡 final existingTab = editorProvider.tabs.firstWhereOrNull( diff --git a/win_text_editor/lib/app/models/tab_model.dart b/win_text_editor/lib/app/models/tab_model.dart index b95610c..b4e6ef9 100644 --- a/win_text_editor/lib/app/models/tab_model.dart +++ b/win_text_editor/lib/app/models/tab_model.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -class EditorTab { +class ContentTab { final String id; final String title; final String? type; // 新增类型字段 @@ -8,7 +8,7 @@ class EditorTab { String content; String? fileName; - EditorTab({ + ContentTab({ required this.id, required this.title, this.type, diff --git a/win_text_editor/lib/app/providers/editor_provider.dart b/win_text_editor/lib/app/providers/tab_provider.dart similarity index 87% rename from win_text_editor/lib/app/providers/editor_provider.dart rename to win_text_editor/lib/app/providers/tab_provider.dart index 39e4520..170c000 100644 --- a/win_text_editor/lib/app/providers/editor_provider.dart +++ b/win_text_editor/lib/app/providers/tab_provider.dart @@ -3,11 +3,11 @@ import 'package:win_text_editor/app/models/tab_model.dart'; import 'package:win_text_editor/app/providers/logger.dart'; import 'package:win_text_editor/app/widgets/template_parser_tab.dart'; -class EditorProvider with ChangeNotifier { - final List _tabs = []; +class TabProvider with ChangeNotifier { + final List _tabs = []; String? _activeTabId; - List get tabs => _tabs; + List get tabs => _tabs; String? get activeTabId => _activeTabId; final Map _tabControllers = {}; @@ -20,7 +20,7 @@ class EditorProvider with ChangeNotifier { _tabControllers.remove(tabId); } - EditorTab? get activeTab { + ContentTab? get activeTab { if (_activeTabId == null) return null; try { return _tabs.firstWhere((tab) => tab.id == _activeTabId); @@ -36,7 +36,7 @@ class EditorProvider with ChangeNotifier { IconData? icon, String content = '', }) async { - final newTab = EditorTab( + final newTab = ContentTab( id: DateTime.now().millisecondsSinceEpoch.toString(), title: title, type: type, @@ -49,7 +49,7 @@ class EditorProvider with ChangeNotifier { notifyListeners(); } - EditorTab? getTabById(String tabId) { + ContentTab? getTabById(String tabId) { try { return _tabs.firstWhere((tab) => tab.id == tabId); } catch (e) { @@ -94,19 +94,19 @@ class EditorProvider with ChangeNotifier { return; } - final textTabState = _tabControllers[_activeTabId]; - if (textTabState == null) { + final parserTabState = _tabControllers[_activeTabId]; + if (parserTabState == null) { Logger().warning("找不到 TextTab 状态"); return; } - if (!textTabState.mounted) { + if (!parserTabState.mounted) { Logger().warning("TextTab 状态组件未挂载"); return; } try { - await textTabState.loadFile(context, filePath); + await parserTabState.loadFile(context, filePath); } catch (e) { Logger().error("加载文件失败: ${e.toString()}"); if (context.mounted) { diff --git a/win_text_editor/lib/app/widgets/editor_pane.dart b/win_text_editor/lib/app/widgets/editor_pane.dart index 4905284..d2a46f9 100644 --- a/win_text_editor/lib/app/widgets/editor_pane.dart +++ b/win_text_editor/lib/app/widgets/editor_pane.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:win_text_editor/app/providers/editor_provider.dart'; +import 'package:win_text_editor/app/providers/tab_provider.dart'; import 'template_parser_tab.dart'; class EditorPane extends StatelessWidget { @@ -8,7 +8,7 @@ class EditorPane extends StatelessWidget { @override Widget build(BuildContext context) { - final provider = Provider.of(context); + final provider = Provider.of(context); return Column( children: [ diff --git a/win_text_editor/lib/app/widgets/file_explorer.dart b/win_text_editor/lib/app/widgets/file_explorer.dart index 92ec33e..af9ea21 100644 --- a/win_text_editor/lib/app/widgets/file_explorer.dart +++ b/win_text_editor/lib/app/widgets/file_explorer.dart @@ -1,7 +1,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/editor_provider.dart'; +import 'package:win_text_editor/app/providers/tab_provider.dart'; import 'package:win_text_editor/app/providers/logger.dart'; import '../models/file_node.dart'; @@ -62,7 +62,7 @@ class _FileExplorerState extends State { Future _openFileInEditor(BuildContext context, FileNode node) async { if (!node.isDirectory) { try { - final editorProvider = Provider.of(context, listen: false); + final editorProvider = Provider.of(context, listen: false); await editorProvider.requestLoadFile(context, node.path); } catch (e) { Logger().error("打开文件失败: ${e.toString()}"); diff --git a/win_text_editor/lib/app/widgets/template_parser_tab.dart b/win_text_editor/lib/app/widgets/template_parser_tab.dart index 0f5ca2b..495d4b5 100644 --- a/win_text_editor/lib/app/widgets/template_parser_tab.dart +++ b/win_text_editor/lib/app/widgets/template_parser_tab.dart @@ -1,12 +1,7 @@ -import 'dart:convert'; -import 'dart:ui'; -import 'dart:io'; -import 'package:collection/collection.dart'; 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 'package:win_text_editor/app/providers/tab_provider.dart'; +import 'package:win_text_editor/app/widgets/text_editor.dart'; class TemplateParserTab extends StatefulWidget { final String tabId; @@ -18,298 +13,34 @@ class TemplateParserTab extends StatefulWidget { } class TemplateParserTabState extends State { - late TextEditingController _controller; - late EditorProvider _provider; - late FocusNode _focusNode; - late ScrollController _scrollController; - bool _isLoading = false; - static const int maxFileSize = 1024 * 1024; // 1MB + late TabProvider _provider; @override void initState() { super.initState(); - _provider = Provider.of(context, listen: false); + _provider = Provider.of(context, listen: false); _provider.registerTextTabController(widget.tabId, this); - _controller = TextEditingController(text: _getCurrentContent()); - _focusNode = FocusNode(); - _scrollController = ScrollController(); - WidgetsBinding.instance.addPostFrameCallback((_) { - FocusScope.of(context).requestFocus(_focusNode); - }); } - String _getCurrentContent() { - return _provider.tabs.firstWhere((t) => t.id == widget.tabId).content; - } - - @override - void didUpdateWidget(TemplateParserTab oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.tabId != widget.tabId) { - _controller.text = _getCurrentContent(); - } - } - - @override - void dispose() { - _controller.dispose(); - _focusNode.dispose(); - _scrollController.dispose(); - super.dispose(); - } + Future loadFile(BuildContext context, String filePath) async {} @override Widget build(BuildContext context) { - final tab = _provider.tabs.firstWhereOrNull((t) => t.id == widget.tabId); + final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); if (tab == null) { return const Center(child: Text('选项卡不存在')); } - String fileNameText = - tab.fileName != null && tab.fileName!.isNotEmpty ? '${tab.fileName},' : ''; - - 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 ? '' : ' ($fileNameText${tab.content.length}字符)'}', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Row( - children: [ - IconButton( - icon: const Icon(Icons.folder_open, size: 20), - tooltip: '打开文件', - onPressed: _isLoading ? null : () => _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), - ), - if (_isLoading) - const Padding( - padding: EdgeInsets.only(left: 8), - child: SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), - ], - ), - ], - ), - ), - Expanded( - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - scrollbars: true, - dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, - ), - child: SingleChildScrollView( - controller: _scrollController, - child: Stack( - children: [ - TextField( - controller: _controller, - focusNode: _focusNode, - maxLines: null, - onChanged: (text) => _provider.updateContent(widget.tabId, text, tab.fileName), - decoration: const InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsets.all(16), - ), - style: TextStyle( - fontFamily: 'monospace', - fontSize: 14, - color: Theme.of(context).textTheme.bodyLarge?.color, - ), - ), - ], - ), - ), - ), - ), - ], + return TextEditor( + tabId: widget.tabId, + initialContent: tab.content, + fileName: tab.fileName, + onContentChanged: (content, fileName) { + _provider.updateContent(widget.tabId, content, fileName); + }, + onFileLoaded: (filePath) { + // 可以在这里添加文件加载后的额外处理 + }, ); } - - Future _openFile(BuildContext context) async { - final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); - if (result != null && result.files.single.path != null) { - await loadFile(context, result.files.single.path!); - } - } - - 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()}'))); - } - } - } - - // 新增公共方法,处理文件加载逻辑 - Future loadFile(BuildContext context, String filePath) async { - try { - setState(() => _isLoading = true); - final file = File(filePath); - final fileSize = await file.length(); - - // 检查文件大小 - if (fileSize > maxFileSize) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('文件过大(超过1MB),无法处理'))); - } - return; - } - - // 判断活动的文本编辑框中是否有内容 - final activeTab = _provider.getTabById(widget.tabId); - if (activeTab != null && activeTab.content.isNotEmpty) { - final confirm = 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 (confirm != true) { - return; - } - } - - final _fileName = file.path.split('\\').last; - - // 清空当前内容 - _provider.updateContent(widget.tabId, '', _fileName); - _controller.text = ''; - - // 逐行读取文件 - final stream = file.openRead(); - final lines = stream.transform(utf8.decoder).transform(const LineSplitter()); - - await for (final line in lines) { - if (!mounted) break; // 如果组件已卸载,停止处理 - - setState(() { - _controller.text += '$line\n'; - _provider.updateContent(widget.tabId, _controller.text, _fileName); - }); - - // 自动滚动到底部 - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollController.jumpTo(_scrollController.position.maxScrollExtent); - }); - - // 添加微小延迟,让用户能看到逐行加载效果 - await Future.delayed(const Duration(milliseconds: 10)); - } - - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加载: ${file.path}'))); - } - } on FormatException { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); - } - } 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()}'))); - } - } finally { - if (mounted) { - setState(() => _isLoading = false); - } - } - } } diff --git a/win_text_editor/lib/app/widgets/text_editor.dart b/win_text_editor/lib/app/widgets/text_editor.dart new file mode 100644 index 0000000..9b0ae7f --- /dev/null +++ b/win_text_editor/lib/app/widgets/text_editor.dart @@ -0,0 +1,313 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:file_picker/file_picker.dart'; + +class TextEditor extends StatefulWidget { + final String tabId; + final String? initialContent; + final String? fileName; + final Function(String, String?)? onContentChanged; + final Function(String)? onFileLoaded; + + const TextEditor({ + super.key, + required this.tabId, + this.initialContent, + this.fileName, + this.onContentChanged, + this.onFileLoaded, + }); + + @override + State createState() => _TextEditorState(); +} + +class _TextEditorState extends State { + late TextEditingController _controller; + late FocusNode _focusNode; + late ScrollController _scrollController; + bool _isLoading = false; + static const int maxFileSize = 1024 * 1024; // 1MB + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.initialContent ?? ''); + _focusNode = FocusNode(); + _scrollController = ScrollController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + FocusScope.of(context).requestFocus(_focusNode); + }); + } + + @override + void didUpdateWidget(TextEditor oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.initialContent != widget.initialContent) { + _controller.text = widget.initialContent ?? ''; + } + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + 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( + '源文本${_controller.text.isEmpty ? '' : ' (${widget.fileName ?? ''}${_controller.text.length}字符)'}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Row( + children: [ + IconButton( + icon: const Icon(Icons.folder_open, size: 20), + tooltip: '打开文件', + onPressed: _isLoading ? null : () => _openFile(context), + ), + IconButton( + icon: const Icon(Icons.content_copy, size: 20), + tooltip: '复制内容', + onPressed: + _controller.text.isEmpty + ? null + : () => _copyToClipboard(context, _controller.text), + ), + IconButton( + icon: const Icon(Icons.save, size: 20), + tooltip: '保存到文件', + onPressed: + _controller.text.isEmpty + ? null + : () => _saveFile(context, _controller.text), + ), + if (_isLoading) + const Padding( + padding: EdgeInsets.only(left: 8), + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ], + ), + ], + ), + ), + Expanded( + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + scrollbars: true, + dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, + ), + child: SingleChildScrollView( + controller: _scrollController, + child: Stack( + children: [ + TextField( + controller: _controller, + focusNode: _focusNode, + maxLines: null, + onChanged: (text) { + widget.onContentChanged?.call(text, widget.fileName); + }, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.all(16), + ), + style: TextStyle( + fontFamily: 'monospace', + fontSize: 14, + color: Theme.of(context).textTheme.bodyLarge?.color, + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + + Future _openFile(BuildContext context) async { + final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); + if (result != null && result.files.single.path != null) { + await loadFile(context, result.files.single.path!); + } + } + + 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()}'))); + } + } + } + + Future loadFile(BuildContext context, String filePath) async { + try { + setState(() => _isLoading = true); + final file = File(filePath); + final fileSize = await file.length(); + + // 检查文件大小 + if (fileSize > maxFileSize) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('文件过大(超过1MB),无法处理'))); + } + return; + } + + // 判断当前是否有内容 + if (_controller.text.isNotEmpty) { + final confirm = 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 (confirm != true) { + return; + } + } + + final fileName = file.path.split('\\').last; + + // 清空当前内容 + _controller.text = ''; + widget.onContentChanged?.call('', fileName); + + // 逐行读取文件 + final stream = file.openRead(); + final lines = stream.transform(utf8.decoder).transform(const LineSplitter()); + + await for (final line in lines) { + if (!mounted) break; + + setState(() { + _controller.text += '$line\n'; + widget.onContentChanged?.call(_controller.text, fileName); + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + }); + + await Future.delayed(const Duration(milliseconds: 10)); + } + + widget.onFileLoaded?.call(file.path); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加载: ${file.path}'))); + } + } on FormatException { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); + } + } 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()}'))); + } + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } +}