From 940aa9466bcdbea7e50b2d7e49ba9c777eb059df Mon Sep 17 00:00:00 2001 From: hejl Date: Mon, 12 May 2025 16:32:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8A=A0=E8=BD=BD=E6=AD=A3?= =?UTF-8?q?=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/app/menus/menu_actions.dart | 31 ++- win_text_editor/lib/app/models/tab_model.dart | 19 ++ .../lib/app/providers/editor_provider.dart | 76 ++++++-- .../lib/app/widgets/console_panel.dart | 1 - .../lib/app/widgets/editor_pane.dart | 7 +- .../lib/app/widgets/file_explorer.dart | 52 ++--- win_text_editor/lib/app/widgets/text_tab.dart | 182 +++++++++++------- win_text_editor/pubspec.lock | 2 +- win_text_editor/pubspec.yaml | 3 +- 9 files changed, 242 insertions(+), 131 deletions(-) create mode 100644 win_text_editor/lib/app/models/tab_model.dart diff --git a/win_text_editor/lib/app/menus/menu_actions.dart b/win_text_editor/lib/app/menus/menu_actions.dart index de857f8..22ed275 100644 --- a/win_text_editor/lib/app/menus/menu_actions.dart +++ b/win_text_editor/lib/app/menus/menu_actions.dart @@ -4,16 +4,23 @@ 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:collection/collection.dart'; import 'dart:io'; class MenuActions { + // 定义模板解析选项卡的常量 + static const String templateParserTabType = "template_parser"; + static const String templateParserTabTitle = "模板解析"; + static const IconData templateParserTabIcon = Icons.auto_awesome_mosaic; + static const String templateParserDefaultContent = ""; + static Future handleMenuAction(String value, BuildContext context) async { switch (value) { case MenuConstants.openFolder: await _openFolder(context); break; case MenuConstants.templateParser: - _openTemplateParser(context); + await _openTemplateParser(context); break; case MenuConstants.exit: _exitApplication(); @@ -31,8 +38,26 @@ class MenuActions { } } - static void _openTemplateParser(BuildContext context) { - Provider.of(context, listen: false).addTab(); + static Future _openTemplateParser(BuildContext context) async { + final editorProvider = Provider.of(context, listen: false); + + // 使用 firstWhereOrNull 查找选项卡 + final existingTab = editorProvider.tabs.firstWhereOrNull( + (tab) => tab.type == templateParserTabType, + ); + + if (existingTab != null) { + // 如果存在,激活该选项卡 + editorProvider.setActiveTab(existingTab.id); + } else { + // 如果不存在,创建新选项卡 + await editorProvider.addTab( + title: templateParserTabTitle, + type: templateParserTabType, + icon: templateParserTabIcon, + content: templateParserDefaultContent, + ); + } } static void _exitApplication() { diff --git a/win_text_editor/lib/app/models/tab_model.dart b/win_text_editor/lib/app/models/tab_model.dart new file mode 100644 index 0000000..b95610c --- /dev/null +++ b/win_text_editor/lib/app/models/tab_model.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class EditorTab { + final String id; + final String title; + final String? type; // 新增类型字段 + final IconData? icon; // 新增图标字段 + String content; + String? fileName; + + EditorTab({ + required this.id, + required this.title, + this.type, + this.icon, + this.content = '', + this.fileName, + }); +} diff --git a/win_text_editor/lib/app/providers/editor_provider.dart b/win_text_editor/lib/app/providers/editor_provider.dart index c861572..edc9966 100644 --- a/win_text_editor/lib/app/providers/editor_provider.dart +++ b/win_text_editor/lib/app/providers/editor_provider.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +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/text_tab.dart'; class EditorProvider with ChangeNotifier { final List _tabs = []; @@ -8,7 +10,15 @@ class EditorProvider with ChangeNotifier { List get tabs => _tabs; String? get activeTabId => _activeTabId; - int _templateTabCounter = 1; + final Map _tabControllers = {}; + + void registerTextTabController(String tabId, TextTabState controller) { + _tabControllers[tabId] = controller; + } + + void unregisterTextTabController(String tabId) { + _tabControllers.remove(tabId); + } EditorTab? get activeTab { if (_activeTabId == null) return null; @@ -20,14 +30,34 @@ class EditorProvider with ChangeNotifier { } } - void addTab() { - final tabId = DateTime.now().millisecondsSinceEpoch.toString(); - _tabs.add(EditorTab(id: tabId, title: '模板解析[$_templateTabCounter]', content: '')); - _templateTabCounter++; - _activeTabId = tabId; + Future addTab({ + String title = '未命名', + String? type, + IconData? icon, + String content = '', + }) async { + final newTab = EditorTab( + id: DateTime.now().millisecondsSinceEpoch.toString(), + title: title, + type: type, + icon: icon, + content: content, + ); + + _tabs.add(newTab); + _activeTabId = newTab.id; notifyListeners(); } + EditorTab? getTabById(String tabId) { + try { + return _tabs.firstWhere((tab) => tab.id == tabId); + } catch (e) { + Logger().error("找不到选项卡: ${tabId}", source: 'EditorProvider'); + return null; + } + } + void closeTab(String tabId) { Logger().info('关闭选项卡: $tabId'); _tabs.removeWhere((tab) => tab.id == tabId); @@ -57,13 +87,33 @@ class EditorProvider with ChangeNotifier { Logger().error("更新内容失败: ${e.toString()}", source: 'EditorProvider'); } } -} -class EditorTab { - final String id; - String title; - String content; - String? fileName; + Future requestLoadFile(BuildContext context, String filePath) async { + if (_activeTabId == null) { + Logger().warning("没有活动选项卡,无法加载文件"); + return; + } + + final textTabState = _tabControllers[_activeTabId]; + if (textTabState == null) { + Logger().warning("找不到 TextTab 状态"); + return; + } + + if (!textTabState.mounted) { + Logger().warning("TextTab 状态组件未挂载"); + return; + } - EditorTab({required this.id, required this.title, required this.content, this.fileName}); + try { + await textTabState.loadFile(context, filePath); + } catch (e) { + Logger().error("加载文件失败: ${e.toString()}"); + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('加载文件失败: ${e.toString()}'))); + } + } + } } diff --git a/win_text_editor/lib/app/widgets/console_panel.dart b/win_text_editor/lib/app/widgets/console_panel.dart index 9bbccdf..50c6c0c 100644 --- a/win_text_editor/lib/app/widgets/console_panel.dart +++ b/win_text_editor/lib/app/widgets/console_panel.dart @@ -15,7 +15,6 @@ class _ConsolePanelState extends State { final double _minHeight = 50; final double _maxHeight = 300; final ScrollController _scrollController = ScrollController(); - String? _selectedLog; // 当前选中的日志内容 @override void dispose() { diff --git a/win_text_editor/lib/app/widgets/editor_pane.dart b/win_text_editor/lib/app/widgets/editor_pane.dart index af99b9b..50ac1c8 100644 --- a/win_text_editor/lib/app/widgets/editor_pane.dart +++ b/win_text_editor/lib/app/widgets/editor_pane.dart @@ -22,6 +22,7 @@ class EditorPane extends StatelessWidget { final tab = provider.tabs[index]; return _TabItem( title: tab.title, + icon: tab.icon, // 添加图标支持 isActive: tab.id == provider.activeTabId, onClose: () => provider.closeTab(tab.id), onTap: () => provider.setActiveTab(tab.id), @@ -32,7 +33,7 @@ class EditorPane extends StatelessWidget { // 内容区 Expanded( child: - provider.activeTabId != null + provider.activeTabId != null && provider.tabs.any((t) => t.id == provider.activeTabId) ? TextTab(tabId: provider.activeTabId!) : const Center(child: Text('无活动标签页')), ), @@ -43,12 +44,14 @@ class EditorPane extends StatelessWidget { class _TabItem extends StatelessWidget { final String title; + final IconData? icon; // 添加图标 final bool isActive; final VoidCallback onClose; final VoidCallback onTap; const _TabItem({ required this.title, + this.icon, required this.isActive, required this.onClose, required this.onTap, @@ -68,6 +71,8 @@ class _TabItem extends StatelessWidget { ), child: Row( children: [ + if (icon != null) Icon(icon, size: 16), + if (icon != null) const SizedBox(width: 4), Text(title), const SizedBox(width: 8), IconButton(icon: const Icon(Icons.close, size: 16), onPressed: onClose), diff --git a/win_text_editor/lib/app/widgets/file_explorer.dart b/win_text_editor/lib/app/widgets/file_explorer.dart index 14fa021..92ec33e 100644 --- a/win_text_editor/lib/app/widgets/file_explorer.dart +++ b/win_text_editor/lib/app/widgets/file_explorer.dart @@ -1,13 +1,11 @@ -import 'dart:io'; - 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/logger.dart'; import '../models/file_node.dart'; import '../providers/file_provider.dart'; -import '../providers/editor_provider.dart'; import 'dart:math'; @@ -57,46 +55,22 @@ class _FileExplorerState extends State { final fileProvider = Provider.of(context, listen: false); if (node.isDirectory) { await fileProvider.loadDirectoryContents(node); - } else { - // Handle file opening - print("No active tab found"); - _openFileInEditor(context, node); } + // 单击文件节点时不处理 } Future _openFileInEditor(BuildContext context, FileNode node) async { - try { - Logger().info('尝试打开文件: ${node.path}'); - final editorProvider = Provider.of(context, listen: false); - final content = await File(node.path).readAsString(); - - Logger().debug('文件内容读取成功,长度: ${content.length}'); - Logger().debug('当前活动选项卡ID: ${editorProvider.activeTabId}'); - Logger().debug('现有选项卡数量: ${editorProvider.tabs.length}'); - - // 如果没有活动选项卡,先创建一个 - if (editorProvider.activeTabId == null) { - Logger().info('没有活动选项卡,创建新选项卡'); - editorProvider.addTab(); - } - // 更新内容 - Logger().info('准备更新选项卡内容'); - editorProvider.updateContent(editorProvider.activeTabId!, content, node.name); - - 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()}'))); + if (!node.isDirectory) { + try { + final editorProvider = Provider.of(context, listen: false); + await editorProvider.requestLoadFile(context, node.path); + } catch (e) { + Logger().error("打开文件失败: ${e.toString()}"); + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('打开文件失败: ${e.toString()}'))); + } } } } diff --git a/win_text_editor/lib/app/widgets/text_tab.dart b/win_text_editor/lib/app/widgets/text_tab.dart index 40544cb..52a9876 100644 --- a/win_text_editor/lib/app/widgets/text_tab.dart +++ b/win_text_editor/lib/app/widgets/text_tab.dart @@ -1,6 +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'; @@ -13,21 +14,22 @@ class TextTab extends StatefulWidget { const TextTab({super.key, required this.tabId}); @override - State createState() => _TextTabState(); + State createState() => TextTabState(); } -class _TextTabState extends State { +class TextTabState extends State { late TextEditingController _controller; late EditorProvider _provider; late FocusNode _focusNode; late ScrollController _scrollController; bool _isLoading = false; - static const int maxFileSize = 10 * 1024 * 1024; // 10MB + static const int maxFileSize = 1024 * 1024; // 1MB @override void initState() { super.initState(); _provider = Provider.of(context, listen: false); + _provider.registerTextTabController(widget.tabId, this); _controller = TextEditingController(text: _getCurrentContent()); _focusNode = FocusNode(); _scrollController = ScrollController(); @@ -58,7 +60,13 @@ class _TextTabState extends State { @override Widget build(BuildContext context) { - final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); + final tab = _provider.tabs.firstWhereOrNull((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: [ @@ -73,7 +81,7 @@ class _TextTabState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '源文本${tab.content.isEmpty ? '' : ' (${tab.fileName!.isEmpty ? '' : '${tab.fileName},'}${tab.content.length}字符)'}', + '源文本${tab.content.isEmpty ? '' : ' ($fileNameText${tab.content.length}字符)'}', style: const TextStyle(fontWeight: FontWeight.bold), ), Row( @@ -143,72 +151,9 @@ class _TextTabState extends State { } Future _openFile(BuildContext context) async { - try { - setState(() => _isLoading = true); - - final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); - - if (result != null && result.files.single.path != null) { - final file = File(result.files.single.path!); - final fileSize = await file.length(); - - // 检查文件大小 - if (fileSize > maxFileSize) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('文件过大(超过10MB),无法处理'))); - } - return; - } - - // 清空当前内容 - _provider.updateContent(widget.tabId, '', result.files.first.name); - _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, result.files.first.name); - }); - - // 自动滚动到底部 - 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); - } + 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!); } } @@ -272,4 +217,99 @@ class _TextTabState extends State { } } } + + // 新增公共方法,处理文件加载逻辑 + 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/pubspec.lock b/win_text_editor/pubspec.lock index b887ca7..472d483 100644 --- a/win_text_editor/pubspec.lock +++ b/win_text_editor/pubspec.lock @@ -74,7 +74,7 @@ packages: source: hosted version: "1.1.2" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" diff --git a/win_text_editor/pubspec.yaml b/win_text_editor/pubspec.yaml index 2d9820a..9f92ed6 100644 --- a/win_text_editor/pubspec.yaml +++ b/win_text_editor/pubspec.yaml @@ -16,8 +16,7 @@ dependencies: bitsdojo_window: ^0.1.1+2 flutter_syntax_view: ^4.1.7 expandable: ^5.0.1 - - + collection: ^1.17.0 dev_dependencies: flutter_test: