Browse Source

内容搜索,局部OK

master
hejl 2 months ago
parent
commit
107063e6ad
  1. 67
      win_text_editor/lib/app/components/editor_toolbar.dart
  2. 284
      win_text_editor/lib/app/components/text_editor.dart
  3. 163
      win_text_editor/lib/app/components/text_editor_actions.dart
  4. 42
      win_text_editor/lib/app/components/text_editor_controller.dart
  5. 1
      win_text_editor/lib/app/modules/content_search/content_search_controller.dart
  6. 52
      win_text_editor/lib/app/modules/content_search/directory_settings.dart
  7. 12
      win_text_editor/lib/app/modules/content_search/search_settings.dart
  8. 22
      win_text_editor/lib/app/modules/template_parser/template_parser_view.dart
  9. 1
      win_text_editor/lib/app/providers/logger.dart

67
win_text_editor/lib/app/components/editor_toolbar.dart

@ -0,0 +1,67 @@ @@ -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),
),
),
],
);
}
}

284
win_text_editor/lib/app/components/text_editor.dart

@ -1,119 +1,67 @@ @@ -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<TextEditor> createState() => TextEditorState();
}
class TextEditorState extends State<TextEditor> {
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<TextEditor> { @@ -121,38 +69,158 @@ class TextEditorState extends State<TextEditor> {
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<void> _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<void> _copyToClipboard(BuildContext context) async {
await Clipboard.setData(ClipboardData(text: _textController.text));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板')));
}
}
Future<void> _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<bool>(
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<void> _loadFile(BuildContext context, String filePath) async {
try {
_isLoading = true;
final file = File(filePath);
if (_textController.text.isNotEmpty) {
final confirm = await showDialog<bool>(
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;
}
}
}

163
win_text_editor/lib/app/components/text_editor_actions.dart

@ -1,163 +0,0 @@ @@ -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<void> 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<void> 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<void> 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<bool>(
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<void> _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<bool>(
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);
}
}
}

42
win_text_editor/lib/app/components/text_editor_controller.dart

@ -1,42 +0,0 @@ @@ -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();
}
}

1
win_text_editor/lib/app/modules/content_search/content_search_controller.dart

@ -47,6 +47,7 @@ class ContentSearchController with ChangeNotifier { @@ -47,6 +47,7 @@ class ContentSearchController with ChangeNotifier {
// Setters with notifyListeners
set searchQuery(String value) {
if (_searchQuery == value) return;
_searchQuery = value;
notifyListeners();
}

52
win_text_editor/lib/app/modules/content_search/directory_settings.dart

@ -2,14 +2,46 @@ import 'package:flutter/material.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<ContentSearchController>();
final searchDirectoryController = TextEditingController(text: controller.searchDirectory);
State<DirectorySettings> createState() => _DirectorySettingsState();
}
class _DirectorySettingsState extends State<DirectorySettings> {
late TextEditingController _searchDirectoryController;
late TextEditingController _fileTypeController;
late ContentSearchController _controller;
@override
void initState() {
super.initState();
_controller = context.read<ContentSearchController>();
_searchDirectoryController = TextEditingController(text: _controller.searchDirectory);
_fileTypeController = TextEditingController(text: _controller.fileType);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final newController = context.read<ContentSearchController>();
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 { @@ -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;
},
),
],

12
win_text_editor/lib/app/modules/content_search/search_settings.dart

@ -12,10 +12,6 @@ class SearchSettings extends StatelessWidget { @@ -12,10 +12,6 @@ class SearchSettings extends StatelessWidget {
Widget build(BuildContext context) {
final controller = context.watch<ContentSearchController>();
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 { @@ -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 { @@ -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();
},
),
),
],

22
win_text_editor/lib/app/modules/template_parser/template_parser_view.dart

@ -73,17 +73,6 @@ class TemplateParserViewState extends State<TemplateParserView> { @@ -73,17 +73,6 @@ class TemplateParserViewState extends State<TemplateParserView> {
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<TemplateParserView> { @@ -103,17 +92,6 @@ class TemplateParserViewState extends State<TemplateParserView> {
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) {
//
},
),
),
),

1
win_text_editor/lib/app/providers/logger.dart

@ -53,7 +53,6 @@ class Logger with ChangeNotifier { @@ -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();

Loading…
Cancel
Save