diff --git a/win_text_editor/lib/app/components/editor_toolbar.dart b/win_text_editor/lib/app/components/editor_toolbar.dart new file mode 100644 index 0000000..6a8a7cd --- /dev/null +++ b/win_text_editor/lib/app/components/editor_toolbar.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +class EditorToolbar extends StatelessWidget { + final String title; + final String text; + final bool isLoading; + final VoidCallback onOpenFile; + final VoidCallback onCopyToClipboard; + final VoidCallback onSaveFile; + + const EditorToolbar({ + super.key, + required this.title, + required this.text, + required this.isLoading, + required this.onOpenFile, + required this.onCopyToClipboard, + required this.onSaveFile, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration(color: Colors.grey[100]), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + _buildActionButtons(context), + ], + ), + ); + } + + Widget _buildActionButtons(BuildContext context) { + return Row( + children: [ + IconButton( + icon: const Icon(Icons.folder_open, size: 20), + tooltip: '打开文件', + onPressed: isLoading ? null : onOpenFile, + ), + IconButton( + icon: const Icon(Icons.content_copy, size: 20), + tooltip: '复制内容', + onPressed: text.isEmpty ? null : onCopyToClipboard, + ), + IconButton( + icon: const Icon(Icons.save, size: 20), + tooltip: '保存到文件', + onPressed: text.isEmpty ? null : onSaveFile, + ), + if (isLoading) + const Padding( + padding: EdgeInsets.only(left: 8), + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ], + ); + } +} diff --git a/win_text_editor/lib/app/components/text_editor.dart b/win_text_editor/lib/app/components/text_editor.dart index 712ed16..5beb641 100644 --- a/win_text_editor/lib/app/components/text_editor.dart +++ b/win_text_editor/lib/app/components/text_editor.dart @@ -1,119 +1,67 @@ -import 'dart:ui'; +import 'dart:convert'; +import 'dart:io'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'text_editor_controller.dart'; -import 'text_editor_actions.dart'; +import 'package:flutter/services.dart'; + +import 'editor_toolbar.dart'; // 添加这行导入 class TextEditor extends StatefulWidget { final String tabId; final String? initialContent; - final String? fileName; final String title; - final Function(String, String?)? onContentChanged; - final Function(String)? onFileLoaded; - - const TextEditor({ - super.key, - required this.tabId, - this.initialContent, - this.fileName, - this.title = '未命名', - this.onContentChanged, - this.onFileLoaded, - }); + + const TextEditor({super.key, required this.tabId, this.initialContent, this.title = '未命名'}); @override State createState() => TextEditorState(); } class TextEditorState extends State { - late TextEditorController _editorController; + final TextEditingController _textController = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + bool _isLoading = false; @override void initState() { super.initState(); - _editorController = TextEditorController( - initialContent: widget.initialContent, - onContentChanged: (content, fileName) { - widget.onContentChanged?.call(content, fileName); - }, - onFileLoaded: widget.onFileLoaded, - ); + _textController.text = widget.initialContent ?? ''; + // 移除监听器,改为在需要时直接获取内容 } @override void didUpdateWidget(TextEditor oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.initialContent != widget.initialContent) { - _editorController.updateContent(widget.initialContent ?? ''); + _textController.text = widget.initialContent ?? ''; } } + String getContent() { + return _textController.text; + } + @override void dispose() { - _editorController.dispose(); + _textController.dispose(); + _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return Column(children: [_buildToolbar(context), Expanded(child: _buildEditorField(context))]); - } - - Widget _buildToolbar(BuildContext context) { - return Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration(color: Colors.grey[100]), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${widget.title}${_editorController.isEmpty ? '' : ' (${widget.fileName ?? ''}${_editorController.contentLength}字符)'}', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - _buildActionButtons(context), - ], - ), - ); - } - - Widget _buildActionButtons(BuildContext context) { - return Row( + return Column( children: [ - IconButton( - icon: const Icon(Icons.folder_open, size: 20), - tooltip: '打开文件', - onPressed: - _editorController.isLoading - ? null - : () => TextEditorActions.openFile(context, _editorController), - ), - IconButton( - icon: const Icon(Icons.content_copy, size: 20), - tooltip: '复制内容', - onPressed: - _editorController.isEmpty - ? null - : () => TextEditorActions.copyToClipboard(context, _editorController.content), - ), - IconButton( - icon: const Icon(Icons.save, size: 20), - tooltip: '保存到文件', - onPressed: - _editorController.isEmpty - ? null - : () => TextEditorActions.saveFile(context, _editorController.content), + EditorToolbar( + title: widget.title, + text: _textController.text, + isLoading: _isLoading, + onOpenFile: () => _openFile(context), + onCopyToClipboard: () => _copyToClipboard(context), + onSaveFile: () => _saveFile(context), ), - if (_editorController.isLoading) - const Padding( - padding: EdgeInsets.only(left: 8), - child: SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), + Expanded(child: _buildEditorField(context)), ], ); } @@ -121,38 +69,158 @@ class TextEditorState extends State { Widget _buildEditorField(BuildContext context) { return Container( decoration: BoxDecoration( - color: _editorController.hasFocus ? Colors.blue[50] : Colors.white, - border: Border.all( - color: _editorController.hasFocus ? Colors.blue : Colors.grey[300]!, - width: _editorController.hasFocus ? 2.0 : 1.0, - ), + color: _focusNode.hasFocus ? Colors.blue[50] : Colors.white, + border: Border.all(color: Colors.grey[300]!, width: 1.0), ), - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - scrollbars: true, - dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, - ), - child: SingleChildScrollView( - controller: _editorController.scrollController, - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: MediaQuery.of(context).size.height - 40, // 减去工具栏高度 - ), - child: TextField( - controller: _editorController.textController, - focusNode: _editorController.focusNode, - maxLines: null, - onChanged: _editorController.handleContentChanged, - decoration: InputDecoration.collapsed(hintText: ''), - style: const TextStyle(fontFamily: 'Courier New', fontSize: 16, color: Colors.black), - ), - ), + child: TextField( + controller: _textController, + focusNode: _focusNode, + maxLines: null, + expands: true, + keyboardType: TextInputType.multiline, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.all(8), ), + style: const TextStyle(fontFamily: 'Courier New', fontSize: 16, color: Colors.black), ), ); } - void setContent(String content) { - _editorController.updateContent(content); + 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) async { + await Clipboard.setData(ClipboardData(text: _textController.text)); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); + } + } + + Future _saveFile(BuildContext context) 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(_textController.text); + + 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 { + _isLoading = true; + final file = File(filePath); + + if (_textController.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; + } + + _textController.text = ''; + + final stream = file.openRead(); + final lines = stream.transform(utf8.decoder).transform(const LineSplitter()); + + int lineCount = 0; + + await for (final line in lines) { + _textController.text += '$line\n'; + + lineCount++; + if (lineCount >= 300) { + return; //仅加载前300行 + } + + 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 { + _isLoading = false; + } } } diff --git a/win_text_editor/lib/app/components/text_editor_actions.dart b/win_text_editor/lib/app/components/text_editor_actions.dart deleted file mode 100644 index 456b646..0000000 --- a/win_text_editor/lib/app/components/text_editor_actions.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:file_picker/file_picker.dart'; -import 'text_editor_controller.dart'; - -class TextEditorActions { - static Future openFile(BuildContext context, TextEditorController controller) async { - final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); - - if (result != null && result.files.single.path != null) { - await _loadFile(context, controller, result.files.single.path!); - } - } - - static Future copyToClipboard(BuildContext context, String content) async { - await Clipboard.setData(ClipboardData(text: content)); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); - } - } - - static 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()}'))); - } - } - } - - static Future _loadFile( - BuildContext context, - TextEditorController controller, - String filePath, - ) async { - try { - controller.setLoading(true); - final file = File(filePath); - final fileSize = await file.length(); - - if (fileSize > controller.maxFileSize) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('文件过大(超过1MB),无法处理'))); - } - return; - } - - if (!controller.isEmpty) { - 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.updateContent(''); - controller.onContentChanged?.call('', fileName); - - final stream = file.openRead(); - final lines = stream.transform(utf8.decoder).transform(const LineSplitter()); - - await for (final line in lines) { - if (!controller.textController.hasListeners) break; - - controller.textController.text += '$line\n'; - controller.onContentChanged?.call(controller.content, fileName); - - WidgetsBinding.instance.addPostFrameCallback((_) { - controller.scrollController.jumpTo(controller.scrollController.position.maxScrollExtent); - }); - - await Future.delayed(const Duration(milliseconds: 10)); - } - - controller.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 { - controller.setLoading(false); - } - } -} diff --git a/win_text_editor/lib/app/components/text_editor_controller.dart b/win_text_editor/lib/app/components/text_editor_controller.dart deleted file mode 100644 index 03f4db1..0000000 --- a/win_text_editor/lib/app/components/text_editor_controller.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -class TextEditorController { - final TextEditingController textController; - final FocusNode focusNode; - final ScrollController scrollController; - final Function(String, String?)? onContentChanged; - final Function(String)? onFileLoaded; - - bool _isLoading = false; - static const int _maxFileSize = 1024 * 1024; // 1MB - - TextEditorController({String? initialContent, this.onContentChanged, this.onFileLoaded}) - : textController = TextEditingController(text: initialContent ?? ''), - focusNode = FocusNode(), - scrollController = ScrollController(); - - bool get isLoading => _isLoading; - bool get hasFocus => focusNode.hasFocus; - bool get isEmpty => textController.text.isEmpty; - String get content => textController.text; - int get contentLength => textController.text.length; - int get maxFileSize => _maxFileSize; - - void updateContent(String content) { - textController.text = content; - } - - void handleContentChanged(String text) { - onContentChanged?.call(text, null); - } - - void setLoading(bool loading) { - _isLoading = loading; - } - - void dispose() { - textController.dispose(); - focusNode.dispose(); - scrollController.dispose(); - } -} diff --git a/win_text_editor/lib/app/modules/content_search/content_search_controller.dart b/win_text_editor/lib/app/modules/content_search/content_search_controller.dart index c928c5a..99235d1 100644 --- a/win_text_editor/lib/app/modules/content_search/content_search_controller.dart +++ b/win_text_editor/lib/app/modules/content_search/content_search_controller.dart @@ -47,6 +47,7 @@ class ContentSearchController with ChangeNotifier { // Setters with notifyListeners set searchQuery(String value) { + if (_searchQuery == value) return; _searchQuery = value; notifyListeners(); } diff --git a/win_text_editor/lib/app/modules/content_search/directory_settings.dart b/win_text_editor/lib/app/modules/content_search/directory_settings.dart index de11d4e..4b7c320 100644 --- a/win_text_editor/lib/app/modules/content_search/directory_settings.dart +++ b/win_text_editor/lib/app/modules/content_search/directory_settings.dart @@ -2,14 +2,46 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:win_text_editor/app/modules/content_search/content_search_controller.dart'; -class DirectorySettings extends StatelessWidget { +class DirectorySettings extends StatefulWidget { const DirectorySettings({super.key}); @override - Widget build(BuildContext context) { - final controller = context.watch(); - final searchDirectoryController = TextEditingController(text: controller.searchDirectory); + State createState() => _DirectorySettingsState(); +} + +class _DirectorySettingsState extends State { + late TextEditingController _searchDirectoryController; + late TextEditingController _fileTypeController; + late ContentSearchController _controller; + + @override + void initState() { + super.initState(); + _controller = context.read(); + _searchDirectoryController = TextEditingController(text: _controller.searchDirectory); + _fileTypeController = TextEditingController(text: _controller.fileType); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final newController = context.read(); + if (_controller != newController) { + _controller = newController; + _searchDirectoryController.text = _controller.searchDirectory; + _fileTypeController.text = _controller.fileType; + } + } + @override + void dispose() { + _searchDirectoryController.dispose(); + _fileTypeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(8.0), @@ -17,26 +49,26 @@ class DirectorySettings extends StatelessWidget { children: [ Expanded( child: TextField( - controller: searchDirectoryController, + controller: _searchDirectoryController, decoration: const InputDecoration(labelText: '搜索目录', border: OutlineInputBorder()), - onChanged: (value) => controller.searchDirectory = value, + onChanged: (value) => _controller.searchDirectory = value, ), ), const SizedBox(width: 8), SizedBox( width: 100, child: TextField( + controller: _fileTypeController, decoration: const InputDecoration(labelText: '文件类型', border: OutlineInputBorder()), - controller: TextEditingController(text: controller.fileType), - onChanged: (value) => controller.fileType = value, + onChanged: (value) => _controller.fileType = value, ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.folder_open), onPressed: () async { - await controller.pickDirectory(); - searchDirectoryController.text = controller.searchDirectory; + await _controller.pickDirectory(); + _searchDirectoryController.text = _controller.searchDirectory; }, ), ], diff --git a/win_text_editor/lib/app/modules/content_search/search_settings.dart b/win_text_editor/lib/app/modules/content_search/search_settings.dart index 7799da1..f9cfb77 100644 --- a/win_text_editor/lib/app/modules/content_search/search_settings.dart +++ b/win_text_editor/lib/app/modules/content_search/search_settings.dart @@ -12,10 +12,6 @@ class SearchSettings extends StatelessWidget { Widget build(BuildContext context) { final controller = context.watch(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _searchEditorKey.currentState?.setContent(controller.searchQuery); - }); - return Card( child: Padding( padding: const EdgeInsets.all(8.0), @@ -30,7 +26,6 @@ class SearchSettings extends StatelessWidget { key: _searchEditorKey, tabId: 'search_content', title: '搜索内容[列表以半角逗号分隔]', - onContentChanged: (content, _) => controller.searchQuery = content, ), ), const SizedBox(width: 8), @@ -198,7 +193,12 @@ class SearchSettings extends StatelessWidget { child: ElevatedButton.icon( icon: const Icon(Icons.search, size: 20), label: const Text('开始搜索'), - onPressed: controller.startSearch, + onPressed: () { + // 点击搜索时获取当前内容 + final content = _searchEditorKey.currentState?.getContent() ?? ''; + controller.searchQuery = content; + controller.startSearch(); + }, ), ), ], diff --git a/win_text_editor/lib/app/modules/template_parser/template_parser_view.dart b/win_text_editor/lib/app/modules/template_parser/template_parser_view.dart index 780b369..6c700c0 100644 --- a/win_text_editor/lib/app/modules/template_parser/template_parser_view.dart +++ b/win_text_editor/lib/app/modules/template_parser/template_parser_view.dart @@ -73,17 +73,6 @@ class TemplateParserViewState extends State { tabId: '${widget.tabId}_1', title: '源文本', // 可配置的标题 initialContent: _editor1Content, - fileName: _editor1FileName, - onContentChanged: (content, fileName) { - setState(() { - _editor1Content = content; - _editor1FileName = fileName; - }); - _provider.updateContent(widget.tabId, content, fileName); - }, - onFileLoaded: (filePath) { - // 文件加载处理 - }, ), ), ), @@ -103,17 +92,6 @@ class TemplateParserViewState extends State { tabId: '${widget.tabId}_2', title: '目标文本', // 可配置的标题 initialContent: _editor2Content, - fileName: _editor2FileName, - onContentChanged: (content, fileName) { - setState(() { - _editor2Content = content; - _editor2FileName = fileName; - }); - _provider.updateContent(widget.tabId, content, fileName); - }, - onFileLoaded: (filePath) { - // 文件加载处理 - }, ), ), ), diff --git a/win_text_editor/lib/app/providers/logger.dart b/win_text_editor/lib/app/providers/logger.dart index 97aafba..30a6373 100644 --- a/win_text_editor/lib/app/providers/logger.dart +++ b/win_text_editor/lib/app/providers/logger.dart @@ -53,7 +53,6 @@ class Logger with ChangeNotifier { } void _addLog(LogEntry entry) { - print('Adding log: $entry'); if (entry.level.index >= _minimumLevel.index) { _logs.add(entry); notifyListeners();