Browse Source

搜索性能优化

master
hejl 2 months ago
parent
commit
045b19e8a1
  1. 33
      win_text_editor/lib/framework/controllers/tab_items_controller.dart
  2. 2
      win_text_editor/lib/framework/services/file_service.dart
  3. 4
      win_text_editor/lib/menus/app_menu.dart
  4. 1
      win_text_editor/lib/menus/menu_constants.dart
  5. 106
      win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart
  6. 40
      win_text_editor/lib/modules/call_function/models/call_function.dart
  7. 128
      win_text_editor/lib/modules/call_function/services/call_function_service.dart
  8. 70
      win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart
  9. 136
      win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart
  10. 76
      win_text_editor/lib/modules/call_function/widgets/call_function_view.dart
  11. 18
      win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart
  12. 153
      win_text_editor/lib/modules/content_search/services/base_search_service.dart
  13. 405
      win_text_editor/lib/modules/content_search/services/content_search_service.dart
  14. 218
      win_text_editor/lib/modules/content_search/services/count_search_service.dart
  15. 133
      win_text_editor/lib/modules/content_search/services/custom_search_service.dart
  16. 112
      win_text_editor/lib/modules/content_search/services/locate_search_service.dart
  17. 78
      win_text_editor/lib/modules/content_search/widgets/search_settings.dart
  18. 1
      win_text_editor/lib/modules/module_router.dart

33
win_text_editor/lib/framework/controllers/tab_items_controller.dart

@ -112,21 +112,28 @@ class TabItemsController with ChangeNotifier { @@ -112,21 +112,28 @@ class TabItemsController with ChangeNotifier {
}
void handleFileDoubleTap(String filePath) {
if (activeContentController == null) {
final fileName = filePath.split(Platform.pathSeparator).last;
if (fileName == "component.xml") {
openOrActivateTab("标准组件", RouterKey.uftComponent, Icons.extension);
} else {
final fileExtension = _getFileExtension(fileName);
switch (fileExtension) {
case 'uftstructure':
openOrActivateTab("内存表", RouterKey.memoryTable, Icons.list);
break;
default:
Logger().error("没有活动的内容控制器", source: 'TabItemsController');
}
final fileName = filePath.split(Platform.pathSeparator).last;
if (fileName == "component.xml") {
openOrActivateTab("标准组件", RouterKey.uftComponent, Icons.extension);
} else {
final fileExtension = _getFileExtension(fileName);
switch (fileExtension) {
case 'uftstructure':
openOrActivateTab("内存表", RouterKey.memoryTable, Icons.list);
break;
case 'uftfunction':
case 'uftservice':
case 'uftatomfunction':
case 'uftatomservice':
case 'uftfactorfunction':
case 'uftfactorservice':
openOrActivateTab("函数调用", RouterKey.callFunction, Icons.functions);
break;
default:
Logger().error("没有活动的内容控制器", source: 'TabItemsController');
}
}
activeContentController?.onOpenFile(filePath);
}

2
win_text_editor/lib/framework/services/file_service.dart

@ -16,7 +16,7 @@ class FileService { @@ -16,7 +16,7 @@ class FileService {
];
static const Map<String, String> _uftFloders = {
'.settings': '项目设置',
'metadat': '元数据',
'metadata': '元数据',
'tools': '工具资源',
'uftatom': 'UFT原子',
'uftbusiness': 'UFT业务逻辑',

4
win_text_editor/lib/menus/app_menu.dart

@ -60,6 +60,10 @@ class AppMenu extends StatelessWidget { @@ -60,6 +60,10 @@ class AppMenu extends StatelessWidget {
value: MenuConstants.uftComponent,
child: ListTile(leading: Icon(Icons.extension), title: Text('标准组件')),
),
const PopupMenuItem<String>(
value: MenuConstants.callFunction,
child: ListTile(leading: Icon(Icons.functions), title: Text('函数调用')),
),
];
}

1
win_text_editor/lib/menus/menu_constants.dart

@ -25,6 +25,7 @@ class MenuConstants { @@ -25,6 +25,7 @@ class MenuConstants {
//Uft菜单
static const String memoryTable = 'memory_table';
static const String uftComponent = 'uft_component';
static const String callFunction = 'call_function';
static const String uftTools = 'uft_tools';
//

106
win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart

@ -0,0 +1,106 @@ @@ -0,0 +1,106 @@
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/framework/services/macro_template_service.dart';
import 'package:win_text_editor/modules/call_function/models/call_function.dart';
import 'package:win_text_editor/modules/call_function/services/call_function_service.dart';
import 'package:win_text_editor/shared/models/std_filed.dart';
import 'package:win_text_editor/shared/uft_std_fields/field_data_source.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
class CallFunctionController extends BaseContentController {
String? _errorMessage;
String tableName = "";
String objectId = "";
String chineseName = "";
late DataGridSource fieldsSource;
late DataGridSource indexesSource;
final CallFunctionService _service;
final MacroTemplateService templateService = MacroTemplateService();
// CallFunction对象
late CallFunction _memoryTable;
CallFunctionController() : _service = CallFunctionService(Logger()) {
//
final initialFields = [Field('1', '', '', '', false), Field('2', '', '', '', false)];
fieldsSource = FieldsDataSource(
initialFields,
onSelectionChanged: (index, isSelected) {
updateFieldSelection(index, isSelected);
},
);
// CallFunction
_memoryTable = CallFunction(tableName: '', columns: initialFields);
}
String? get errorMessage => _errorMessage;
// CallFunction
CallFunction get memoryTable => _memoryTable;
void initTemplateService() {
if (!templateService.inited) {
templateService.init();
}
}
String? genCodeString(List<String> macroList) {
initTemplateService();
return templateService.renderTemplate(macroList, _memoryTable.toMap());
}
@override
Future<void> onOpenFile(String filePath) async {
Logger().info("Opening file: $filePath");
try {
final tableData = await _service.parseStructureFile(filePath);
// Update controller state
tableName = tableData.tableName;
chineseName = tableData.chineseName;
objectId = tableData.objectId;
// Update data sources
(fieldsSource as FieldsDataSource).updateData(tableData.fields);
// CallFunction对象
_memoryTable = CallFunction(tableName: tableName, columns: tableData.fields);
// Clear any previous error
_errorMessage = null;
// Notify UI to update
notifyListeners();
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
Logger().error("Error opening file: $e");
}
}
//
void updateFieldSelection(int index, bool isSelected) {
final fields = (fieldsSource as FieldsDataSource).data;
if (index >= 0 && index < fields.length) {
fields[index].isSelected = isSelected;
fieldsSource.notifyListeners();
// CallFunction
_memoryTable.columns[index].isSelected = isSelected;
notifyListeners();
}
}
@override
void onOpenFolder(String folderPath) {
//
}
@override
void dispose() {
super.dispose();
}
}

40
win_text_editor/lib/modules/call_function/models/call_function.dart

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
import 'package:win_text_editor/shared/models/std_filed.dart';
class CallFunction {
final String tableName;
final List<Field> columns;
CallFunction({required this.tableName, required this.columns});
List<Field> get selectFields => columns.where((field) => field.isSelected).toList();
Map<String, dynamic> toMap() {
return {
'tableName': tableName,
'fields':
columns
.map(
(field) => {
'id': field.id,
'name': field.name,
'chineseName': field.chineseName,
'type': field.type,
'isLast': columns.indexOf(field) == columns.length - 1,
},
)
.toList(),
'selectedFields':
selectFields
.map(
(field) => {
'id': field.id,
'name': field.name,
'chineseName': field.chineseName,
'type': field.type,
'isLast': selectFields.indexOf(field) == selectFields.length - 1,
},
)
.toList(),
};
}
}

128
win_text_editor/lib/modules/call_function/services/call_function_service.dart

@ -0,0 +1,128 @@ @@ -0,0 +1,128 @@
// memory_table_service.dart
import 'dart:io';
import 'package:win_text_editor/modules/memory_table/models/memory_table.dart';
import 'package:win_text_editor/shared/data/std_fields_cache.dart';
import 'package:win_text_editor/shared/models/std_filed.dart';
import 'package:win_text_editor/shared/uft_std_fields/field_data_service.dart';
import 'package:xml/xml.dart' as xml;
import 'package:path/path.dart' as path;
import 'package:win_text_editor/framework/controllers/logger.dart';
class CallFunctionService {
final Logger _logger;
CallFunctionService(this._logger);
Future<TableData> parseStructureFile(String filePath) async {
try {
// 1. Check file extension
if (!filePath.toLowerCase().endsWith('.uftstructure')) {
throw const FormatException("文件扩展名必须是.uftstructure");
}
// 2. metadata stdfield.stfield
if (await StdFieldsCache.getLength() == 0) {
_logger.info("加载标准字段缓存");
final metadataFile = await FieldDataService.findMetadataFile(filePath);
if (metadataFile != null) {
await FieldDataService.processStdFieldFile(metadataFile);
}
}
// 3. Read and parse structure file content
final file = File(filePath);
final content = await file.readAsString();
final document = xml.XmlDocument.parse(content);
final structureNode = document.findAllElements('structure:Structure').firstOrNull;
if (structureNode == null) {
throw const FormatException("文件格式错误:缺少structure:Structure节点");
}
// 4. Get basic info
final fileNameWithoutExt = path.basenameWithoutExtension(filePath);
final chineseName = structureNode.getAttribute('chineseName') ?? '';
final objectId = structureNode.getAttribute('objectId') ?? '';
// 5. Process properties (fields)
final properties = document.findAllElements('properties');
final fields = <Field>[];
int index = 1;
for (final property in properties) {
final id = property.getAttribute('id') ?? '';
//
final stdField = StdFieldsCache.getData(id);
fields.add(
Field(
(index++).toString(), //
id, //
stdField?.chineseName ?? '', //
stdField?.dateType ?? '', //
),
);
}
// 6. Process indexes
final indexes = document.findAllElements('indexs');
final indexList = <Index>[];
for (final indexNode in indexes) {
final name = indexNode.getAttribute('name') ?? '';
final containerType = indexNode.getAttribute('containerType');
final isPrimary = containerType == null;
final rule = containerType ?? '';
// Get all index fields
final items = indexNode.findAllElements('items');
final fieldsList =
items
.map((item) => item.getAttribute('attrname') ?? '')
.where((f) => f.isNotEmpty)
.toList();
final indexFields = fieldsList.join(',');
indexList.add(
Index(
name, //
isPrimary, //
indexFields, //
rule, //
),
);
}
return TableData(
tableName: fileNameWithoutExt,
chineseName: chineseName,
objectId: objectId,
fields: fields.isNotEmpty ? fields : FieldDataService.getDefaultFields(),
indexes: indexList.isNotEmpty ? indexList : FieldDataService.getDefaultIndexes(),
);
} on xml.XmlParserException catch (e) {
_logger.error("XML解析错误: ${e.message}");
rethrow;
} catch (e) {
_logger.error("处理文件时发生错误: $e");
rethrow;
}
}
}
class TableData {
final String tableName;
final String chineseName;
final String objectId;
final List<Field> fields;
final List<Index> indexes;
TableData({
required this.tableName,
required this.chineseName,
required this.objectId,
required this.fields,
required this.indexes,
});
}

70
win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:win_text_editor/modules/call_function/controllers/call_function_controller.dart';
import 'package:win_text_editor/shared/uft_std_fields/field_data_source.dart';
import 'package:win_text_editor/shared/uft_std_fields/fields_data_grid.dart';
class CallFunctionLeftSide extends StatelessWidget {
final CallFunctionController controller;
const CallFunctionLeftSide({super.key, required this.controller});
Widget _buildTextFieldRow(String label, String value) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('$label:'),
SizedBox(
width: 200,
child: TextField(
controller: TextEditingController(text: value),
readOnly: true,
decoration: const InputDecoration(isDense: true, contentPadding: EdgeInsets.all(8)),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: double.infinity,
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Wrap(
spacing: 16,
runSpacing: 8,
children: [
_buildTextFieldRow('名称', controller.tableName),
_buildTextFieldRow('中文名', controller.chineseName),
_buildTextFieldRow('对象编号', controller.objectId),
],
),
),
),
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text('字段列表', style: TextStyle(fontWeight: FontWeight.bold)),
),
Expanded(
flex: 6,
child: FieldsDataGrid(
fieldsSource: controller.fieldsSource as FieldsDataSource,
onSelectionChanged: (index, isSelected) {
controller.updateFieldSelection(index, isSelected);
},
),
),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text('索引列表', style: TextStyle(fontWeight: FontWeight.bold)),
),
],
);
}
}

136
win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart

@ -0,0 +1,136 @@ @@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:win_text_editor/modules/call_function/controllers/call_function_controller.dart';
class CallFunctionRightSide extends StatefulWidget {
final CallFunctionController controller;
final TextEditingController codeController;
const CallFunctionRightSide({super.key, required this.controller, required this.codeController});
@override
State<CallFunctionRightSide> createState() => _CallFunctionRightSideState();
}
class _CallFunctionRightSideState extends State<CallFunctionRightSide> {
final List<String> _selectedOperations = [];
@override
void initState() {
super.initState();
widget.controller.initTemplateService();
widget.controller.addListener(_updateDisplay);
}
@override
void dispose() {
widget.controller.removeListener(_updateDisplay);
super.dispose();
}
void _updateDisplay() {
widget.codeController.text = widget.controller.genCodeString(_selectedOperations)!;
}
void _toggleOperation(String operation, bool? value) {
setState(() {
if (value == true) {
_selectedOperations.add(operation);
} else {
_selectedOperations.remove(operation);
}
_updateDisplay();
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildCheckboxSection(),
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('生成代码:', style: TextStyle(fontWeight: FontWeight.bold)),
IconButton(
icon: const Icon(Icons.content_copy, size: 20),
tooltip: '复制代码',
onPressed: () {
if (widget.codeController.text.isNotEmpty) {
Clipboard.setData(ClipboardData(text: widget.codeController.text));
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已复制到剪贴板')));
}
},
),
],
),
),
Flexible(child: _buildCodeEditor()),
],
);
}
Widget _buildCheckboxSection() {
final operations = ['获取记录', '获取记录数', '插入记录', '修改记录', '删除记录', '遍历记录'];
return SizedBox(
width: double.infinity,
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 16,
runSpacing: 8,
children: operations.map((op) => _buildCheckbox(op)).toList(),
),
],
),
),
),
);
}
Widget _buildCheckbox(String label) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: _selectedOperations.contains(label),
onChanged: (bool? value) => _toggleOperation(label, value),
),
Text(label),
],
);
}
Widget _buildCodeEditor() {
return Card(
child: Padding(
padding: const EdgeInsets.all(8),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4),
),
child: TextField(
controller: widget.codeController,
maxLines: null,
expands: true,
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.all(8),
),
style: const TextStyle(fontFamily: 'monospace', color: Colors.blueAccent),
),
),
),
);
}
}

76
win_text_editor/lib/modules/call_function/widgets/call_function_view.dart

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart';
import 'package:win_text_editor/modules/call_function/controllers/call_function_controller.dart';
import 'call_function_left_side.dart';
import 'call_function_right_side.dart';
class CallFunctionView extends StatefulWidget {
final String tabId;
const CallFunctionView({super.key, required this.tabId});
@override
State<CallFunctionView> createState() => _CallFunctionViewState();
}
class _CallFunctionViewState extends State<CallFunctionView> {
late final CallFunctionController _controller;
final TextEditingController _codeController = TextEditingController();
bool _isControllerFromTabManager = false;
get tabManager => Provider.of<TabItemsController>(context, listen: false);
@override
void initState() {
super.initState();
final controllerFromManager = tabManager.getController(widget.tabId);
if (controllerFromManager != null) {
_controller = controllerFromManager;
_isControllerFromTabManager = true;
} else {
_controller = CallFunctionController();
_isControllerFromTabManager = false;
tabManager.registerController(widget.tabId, _controller);
}
}
@override
void dispose() {
if (!_isControllerFromTabManager) {
_controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: _controller,
child: Consumer<CallFunctionController>(
builder: (context, controller, child) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// (50%)
Expanded(flex: 5, child: CallFunctionLeftSide(controller: controller)),
const SizedBox(width: 8),
// (50%)
Expanded(
flex: 5,
child: CallFunctionRightSide(
codeController: _codeController,
controller: controller,
),
),
],
),
);
},
),
);
}
}

18
win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart

@ -7,8 +7,10 @@ import 'package:file_picker/file_picker.dart'; @@ -7,8 +7,10 @@ import 'package:file_picker/file_picker.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/content_search/models/search_mode.dart';
import 'package:win_text_editor/modules/content_search/models/search_result.dart';
import 'package:win_text_editor/modules/content_search/services/count_search_service.dart';
import 'package:win_text_editor/modules/content_search/services/locate_search_service.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
import '../services/content_search_service.dart';
import '../services/custom_search_service.dart';
class ContentSearchController extends BaseContentController {
String _searchQuery = '';
@ -44,7 +46,9 @@ class ContentSearchController extends BaseContentController { @@ -44,7 +46,9 @@ class ContentSearchController extends BaseContentController {
//
void _updateProgress(double value) {
_progress = value.clamp(0.0, 100.0);
final newProgress = value.clamp(0.0, 100.0);
if (newProgress <= _progress) return;
_progress = newProgress;
notifyListeners();
}
@ -144,7 +148,7 @@ class ContentSearchController extends BaseContentController { @@ -144,7 +148,7 @@ class ContentSearchController extends BaseContentController {
return;
}
String first50Chars = searchQuery.length > 50 ? searchQuery.substring(0, 50) : searchQuery;
Logger().debug("搜索内容: $first50Chars");
Logger().debug("开始搜索: $first50Chars...");
if (searchDirectory.isEmpty || !Directory(searchDirectory).existsSync()) {
Logger().info("搜索目录不能为空");
@ -153,14 +157,14 @@ class ContentSearchController extends BaseContentController { @@ -153,14 +157,14 @@ class ContentSearchController extends BaseContentController {
try {
if (customRule) {
final validationResult = ContentSearchService.validateJsRule(searchQuery);
final validationResult = CustomSearchService.validateJsRule(searchQuery);
if (validationResult.isError) {
Logger().error('JavaScript 语法错误: ${validationResult.rawResult.toString()}');
return;
}
_allResults.addAll(
await ContentSearchService.performCustomSearch(
await CustomSearchService.performCustomSearch(
directory: searchDirectory,
fileType: fileType,
jsFunction: searchQuery,
@ -172,7 +176,7 @@ class ContentSearchController extends BaseContentController { @@ -172,7 +176,7 @@ class ContentSearchController extends BaseContentController {
} else {
if (searchMode == SearchMode.locate) {
_allResults.addAll(
await ContentSearchService.performLocateSearch(
await LocateSearchService.performSearch(
directory: searchDirectory,
query: searchQuery,
fileType: fileType,
@ -184,7 +188,7 @@ class ContentSearchController extends BaseContentController { @@ -184,7 +188,7 @@ class ContentSearchController extends BaseContentController {
),
);
} else {
final counts = await ContentSearchService.performCountSearch(
final counts = await CountSearchService.performSearch(
directory: searchDirectory,
query: searchQuery,
fileType: fileType,

153
win_text_editor/lib/modules/content_search/services/base_search_service.dart

@ -0,0 +1,153 @@ @@ -0,0 +1,153 @@
// lib/app/modules/content_search/services/base_search_service.dart
import 'dart:io';
import 'package:path/path.dart' as path;
typedef ProgressCallback = void Function(double progress);
typedef FileProcessor = Future<void> Function(File file, String content);
abstract class BaseSearchService {
///
static Future<void> processFiles({
required String directory,
required String fileType,
required FileProcessor onFile,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
final dir = Directory(directory);
final totalFiles = await countFilesInDirectory(dir, fileType, shouldStop);
if (totalFiles == 0) return;
onProgress?.call(1);
int processedFiles = 0;
int oldProgress = 1;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return;
if (entity is! File || !matchesFileType(entity.path, fileType)) continue;
processedFiles++;
final progress = (processedFiles / totalFiles) * 99 + 1;
if (progress - oldProgress >= 1) {
oldProgress = progress.floor();
onProgress?.call(progress);
}
final content = await entity.readAsString();
await onFile(entity, content);
}
onProgress?.call(100);
}
///
static List<String> splitQuery(String query) {
return query.split(',').map((q) => q.trim()).where((q) => q.isNotEmpty).toList();
}
static RegExp buildSearchPattern({
required String query,
required bool caseSensitive,
required bool wholeWord,
required bool useRegex,
}) {
String pattern;
if (useRegex) {
pattern = query;
} else {
pattern = RegExp.escape(query);
if (wholeWord) {
pattern = '\\b$pattern\\b';
}
}
return RegExp(pattern, caseSensitive: caseSensitive, multiLine: true);
}
static Future<int> countFilesInDirectory(
Directory dir,
String fileType,
bool Function()? shouldStop,
) async {
int totalFiles = 0;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return totalFiles;
if (entity is File && matchesFileType(entity.path, fileType)) {
totalFiles++;
}
}
return totalFiles;
}
static bool matchesFileType(String filePath, String fileType) {
//
if (fileType == '*.*') return true;
if (fileType == '*') return true;
//
final parts = fileType.split('.');
final patternName = parts.length > 0 ? parts[0] : '';
final patternExt = parts.length > 1 ? parts.sublist(1).join('.') : '';
//
final fileName = path.basename(filePath);
final fileExt = path.extension(fileName).toLowerCase().replaceFirst('.', '');
//
bool nameMatches = true;
if (patternName.isNotEmpty && patternName != '*') {
nameMatches = matchesWildcard(fileName, patternName);
}
//
bool extMatches = true;
if (patternExt.isNotEmpty) {
if (patternExt == '*') {
extMatches = true;
} else {
extMatches = matchesWildcard(fileExt, patternExt);
}
}
return nameMatches && extMatches;
}
//
static bool matchesWildcard(String input, String pattern) {
//
if (pattern == '*') return true;
//
final m = input.length;
final n = pattern.length;
final dp = List.generate(m + 1, (_) => List.filled(n + 1, false));
//
dp[0][0] = true;
// *
for (int j = 1; j <= n; j++) {
if (pattern[j - 1] == '*') {
dp[0][j] = dp[0][j - 1];
}
}
//
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (pattern[j - 1] == '*') {
// *
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
} else if (pattern[j - 1] == '?' || input[i - 1] == pattern[j - 1]) {
// ?
dp[i][j] = dp[i - 1][j - 1];
} else {
//
dp[i][j] = false;
}
}
}
return dp[m][n];
}
}

405
win_text_editor/lib/modules/content_search/services/content_search_service.dart

@ -1,405 +0,0 @@ @@ -1,405 +0,0 @@
// lib/app/modules/content_search/content_search_service.dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter_js/flutter_js.dart';
import 'package:path/path.dart' as path;
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/content_search/models/match_result.dart';
import 'package:win_text_editor/modules/content_search/models/search_mode.dart';
import 'package:win_text_editor/modules/content_search/models/search_result.dart';
typedef ProgressCallback = void Function(double progress);
class ContentSearchService {
///
static Future<List<SearchResult>> performLocateSearch({
required String directory,
required String query,
required String fileType,
required bool caseSensitive,
required bool wholeWord,
required bool useRegex,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
final results = <SearchResult>[];
final dir = Directory(directory);
final queries = _splitQuery(query);
//
int totalFiles = 0;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return results;
if (entity is File && _matchesFileType(entity.path, fileType)) {
totalFiles++;
}
}
onProgress?.call(1);
int words = 0;
int processedFiles = 0;
int oldProgress = 1;
for (final q in queries) {
if (shouldStop?.call() == true) return results;
final pattern = _buildSearchPattern(
query: q,
caseSensitive: caseSensitive,
wholeWord: wholeWord,
useRegex: useRegex,
);
Logger().info("搜索词 $q 开始[${++words}/${queries.length}]");
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return results;
if (entity is File && _matchesFileType(entity.path, fileType)) {
processedFiles++;
final progress = (processedFiles / (totalFiles * queries.length)) * 99 + 1;
if (progress - oldProgress >= 1) {
oldProgress = progress.floor();
onProgress?.call(progress);
}
await _searchInFile(entity, pattern, results, q);
}
}
Logger().info("搜索词 $q 结束[$words/${queries.length}],搜索文件 $totalFiles 个,");
}
onProgress?.call(100);
return results;
}
///
static Future<Map<String, int>> performCountSearch({
required String directory,
required String query,
required String fileType,
required bool caseSensitive,
required bool wholeWord,
required bool useRegex,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
final counts = <String, int>{};
final dir = Directory(directory);
final queries = _splitQuery(query); //
int totalFiles = 0;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return counts;
if (entity is File && _matchesFileType(entity.path, fileType)) {
totalFiles++;
}
}
onProgress?.call(1);
int words = 0;
int processedFiles = 0;
int oldProgress = 1;
for (final q in queries) {
if (shouldStop?.call() == true) return counts;
final pattern = _buildSearchPattern(
query: q,
caseSensitive: caseSensitive,
wholeWord: wholeWord,
useRegex: useRegex,
);
Logger().info("搜索词 $q 开始[${++words}/${queries.length}]");
counts[q] = 0;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return counts;
if (entity is File && _matchesFileType(entity.path, fileType)) {
processedFiles++;
final progress = (processedFiles / (totalFiles * queries.length)) * 99 + 1;
if (progress - oldProgress >= 1) {
oldProgress = progress.floor();
onProgress?.call(progress);
}
await _countInFile(entity, pattern, counts, q); //
}
}
Logger().info("搜索词 $q 结束[$words/${queries.length}],搜索文件 $totalFiles 个,");
}
return counts;
}
/// JavaScript
static Future<List<SearchResult>> performCustomSearch({
required String directory,
required String fileType,
required String jsFunction,
required SearchMode searchMode,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
final results = <SearchResult>[];
int count = 0;
final dir = Directory(directory);
final jsRuntime = getJavascriptRuntime();
try {
// JavaScript
final jsCode = 'function match(content){$jsFunction};';
jsRuntime.evaluate(jsCode);
int totalFiles = 0;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return results;
if (entity is File && _matchesFileType(entity.path, fileType)) {
totalFiles++;
}
}
onProgress?.call(1);
int processedFiles = 0;
int calledProcessedFiles = 1;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return results;
if (entity is File && _matchesFileType(entity.path, fileType)) {
try {
final lines = await entity.readAsLines();
for (int i = 0; i < lines.length; i++) {
if (shouldStop?.call() == true) return results;
final line = lines[i].trim();
if (line.length < 3) continue; //
final result = jsRuntime.evaluate('match(${jsonEncode(line)});');
if (result.isError) {
throw Exception('JS Error: ${result.stringResult}');
}
if (result.stringResult == 'true') {
if (searchMode == SearchMode.locate) {
results.add(
SearchResult(
filePath: entity.path,
lineNumber: i + 1,
lineContent: line,
matches: [],
queryTerm: "Custom Rule",
),
);
} else {
count++;
}
}
}
} catch (e) {
Logger().error('Error in file ${entity.path}: $e');
}
processedFiles++;
final progress = (processedFiles / totalFiles) * 99 + 1;
if (processedFiles - calledProcessedFiles >= 1) {
calledProcessedFiles = processedFiles;
onProgress?.call(progress);
}
}
}
//
if (searchMode == SearchMode.count) {
results.add(
SearchResult(
filePath: "Custom Rule",
lineNumber: count,
lineContent: '',
matches: [],
queryTerm: "Custom Rule",
),
);
}
} finally {
onProgress?.call(100);
jsRuntime.dispose();
}
return results;
}
///
static List<String> _splitQuery(String query) {
return query.split(',').map((q) => q.trim()).where((q) => q.isNotEmpty).toList();
}
///
static RegExp _buildSearchPattern({
required String query,
required bool caseSensitive,
required bool wholeWord,
required bool useRegex,
}) {
String pattern;
if (useRegex) {
pattern = query;
} else {
pattern = RegExp.escape(query);
if (wholeWord) {
pattern = '\\b$pattern\\b';
}
}
return RegExp(pattern, caseSensitive: caseSensitive, multiLine: true);
}
static bool _matchesFileType(String filePath, String fileType) {
//
if (fileType == '*.*') return true;
if (fileType == '*') return true;
//
final parts = fileType.split('.');
final patternName = parts.length > 0 ? parts[0] : '';
final patternExt = parts.length > 1 ? parts.sublist(1).join('.') : '';
//
final fileName = path.basename(filePath);
final fileExt = path.extension(fileName).toLowerCase().replaceFirst('.', '');
//
bool nameMatches = true;
if (patternName.isNotEmpty && patternName != '*') {
nameMatches = _matchesWildcard(fileName, patternName);
}
//
bool extMatches = true;
if (patternExt.isNotEmpty) {
if (patternExt == '*') {
extMatches = true;
} else {
extMatches = _matchesWildcard(fileExt, patternExt);
}
}
return nameMatches && extMatches;
}
//
static bool _matchesWildcard(String input, String pattern) {
//
if (pattern == '*') return true;
//
final m = input.length;
final n = pattern.length;
final dp = List.generate(m + 1, (_) => List.filled(n + 1, false));
//
dp[0][0] = true;
// *
for (int j = 1; j <= n; j++) {
if (pattern[j - 1] == '*') {
dp[0][j] = dp[0][j - 1];
}
}
//
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (pattern[j - 1] == '*') {
// *
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
} else if (pattern[j - 1] == '?' || input[i - 1] == pattern[j - 1]) {
// ?
dp[i][j] = dp[i - 1][j - 1];
} else {
//
dp[i][j] = false;
}
}
}
return dp[m][n];
}
/// queryTerm
static Future<void> _searchInFile(
File file,
RegExp pattern,
List<SearchResult> results,
String queryTerm, //
) async {
try {
final lines = await file.readAsLines();
for (int i = 0; i < lines.length; i++) {
final line = lines[i].trim();
final matches = pattern.allMatches(line);
if (matches.isNotEmpty) {
results.add(
SearchResult(
filePath: file.path,
lineNumber: i + 1,
lineContent: line,
matches: matches.map((m) => MatchResult(start: m.start, end: m.end)).toList(),
queryTerm: queryTerm, //
),
);
}
}
} catch (e) {
Logger().error('Error reading file ${file.path}: $e');
}
}
/// queryTerm
static Future<void> _countInFile(
File file,
RegExp pattern,
Map<String, int> counts,
String queryTerm, //
) async {
try {
final content = await file.readAsString();
final matches = pattern.allMatches(content);
if (matches.isNotEmpty) {
counts[queryTerm] = (counts[queryTerm] ?? 0) + matches.length;
}
} catch (e) {
Logger().error('Error reading file ${file.path}: $e');
}
}
// ContentSearchService
/// JavaScript
static JsEvalResult validateJsRule(String jsFunction) {
final jsRuntime = getJavascriptRuntime();
try {
final jsCode = 'function match(content) {$jsFunction};';
// JS
final result = jsRuntime.evaluate(jsCode);
if (result.isError) {
return result;
}
//
final test = jsRuntime.evaluate("match('test');");
if (test.stringResult != 'true' && test.stringResult != 'false') {
return JsEvalResult('Custom rule must return boolean (true/false)', null, isError: true);
}
return test;
} finally {
jsRuntime.dispose();
}
}
}

218
win_text_editor/lib/modules/content_search/services/count_search_service.dart

@ -0,0 +1,218 @@ @@ -0,0 +1,218 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:win_text_editor/modules/content_search/services/base_search_service.dart';
class CountSearchService {
static const _maxConcurrentIsolates = 8; // CPU核心数调整
static const _queriesPerBatch = 500; //
static List<List<T>> _chunkList<T>(List<T> list, int chunkSize) {
return List.generate(
(list.length / chunkSize).ceil(),
(i) => list.sublist(
i * chunkSize,
i * chunkSize + chunkSize > list.length ? list.length : i * chunkSize + chunkSize,
),
);
}
static Future<Map<String, int>> performSearch({
required String directory,
required String query,
required String fileType,
required bool caseSensitive,
required bool wholeWord,
required bool useRegex,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
if (shouldStop?.call() == true) return {};
final counts = <String, int>{};
final allQueries = BaseSearchService.splitQuery(query);
//
final queryBatches = _chunkList(allQueries, _queriesPerBatch);
//
final filePaths = await _collectFilePaths(directory, fileType, shouldStop);
if (filePaths.isEmpty) return counts;
// Isolate池
final resultPort = ReceivePort();
final stopPort = ReceivePort();
final completer = Completer<Map<String, int>>();
int activeIsolates = 0;
//
int processedFiles = 0;
void updateProgress() {
final progress = (processedFiles / filePaths.length) * 100;
onProgress?.call(progress);
}
final initIsolates = min(2, _maxConcurrentIsolates); // 2
for (int i = 0; i < initIsolates; i++) {
_spawnIsolateWorker(
filePaths.sublist(i, i + 1), // Isolate处理部分文件
queryBatches,
resultPort.sendPort,
stopPort.sendPort,
caseSensitive,
wholeWord,
useRegex,
);
activeIsolates++;
}
// Isolate
Future.delayed(const Duration(milliseconds: 300), () {
for (int i = activeIsolates; i < min(_maxConcurrentIsolates, filePaths.length); i++) {
_spawnIsolateWorker(
filePaths.sublist(i, i + 1), // Isolate处理部分文件
queryBatches,
resultPort.sendPort,
stopPort.sendPort,
caseSensitive,
wholeWord,
useRegex,
);
activeIsolates++;
}
});
//
resultPort.listen((msg) {
if (msg is Map<String, int>) {
msg.forEach((query, count) {
counts.update(query, (v) => v + count, ifAbsent: () => count);
});
processedFiles++;
updateProgress();
//
if (activeIsolates < filePaths.length) {
_spawnIsolateWorker(
[filePaths[activeIsolates]],
queryBatches,
resultPort.sendPort,
stopPort.sendPort,
caseSensitive,
wholeWord,
useRegex,
);
activeIsolates++;
} else if (processedFiles == filePaths.length) {
completer.complete(counts);
}
}
});
//
stopPort.listen((_) => completer.complete(counts));
return completer.future.whenComplete(() {
resultPort.close();
stopPort.close();
});
}
static Future<List<String>> _collectFilePaths(
String directory,
String fileType,
bool Function()? shouldStop,
) async {
final dir = Directory(directory);
final paths = <String>[];
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) break;
if (entity is File && BaseSearchService.matchesFileType(entity.path, fileType)) {
paths.add(entity.path);
}
}
return paths;
}
static void _spawnIsolateWorker(
List<String> filePaths,
List<List<String>> queryBatches,
SendPort resultPort,
SendPort stopPort,
bool caseSensitive,
bool wholeWord,
bool useRegex,
) async {
await Isolate.spawn(
_isolateEntry,
_IsolateData(
filePaths,
queryBatches,
resultPort,
stopPort,
caseSensitive,
wholeWord,
useRegex,
),
);
}
static void _isolateEntry(_IsolateData data) {
final localCounts = <String, int>{};
for (final filePath in data.filePaths) {
try {
final content = File(filePath).readAsStringSync();
for (final batch in data.queryBatches) {
//
final batchResults = ParallelTask.runSync(() {
final batchCounts = <String, int>{};
for (final query in batch) {
final pattern = BaseSearchService.buildSearchPattern(
query: query,
caseSensitive: data.caseSensitive,
wholeWord: data.wholeWord,
useRegex: data.useRegex,
);
batchCounts[query] = pattern.allMatches(content).length;
}
return batchCounts;
});
batchResults.forEach((query, count) {
localCounts.update(query, (v) => v + count, ifAbsent: () => count);
});
}
} catch (e) {
print('Isolate error: $e');
}
}
data.resultPort.send(localCounts);
}
}
// Isolate通信数据结构
class _IsolateData {
final List<String> filePaths;
final List<List<String>> queryBatches;
final SendPort resultPort;
final SendPort stopPort;
final bool caseSensitive;
final bool wholeWord;
final bool useRegex;
_IsolateData(
this.filePaths,
this.queryBatches,
this.resultPort,
this.stopPort,
this.caseSensitive,
this.wholeWord,
this.useRegex,
);
}
//
class ParallelTask {
static T runSync<T>(T Function() task) => task();
}

133
win_text_editor/lib/modules/content_search/services/custom_search_service.dart

@ -0,0 +1,133 @@ @@ -0,0 +1,133 @@
// lib/app/modules/content_search/content_search_service.dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter_js/flutter_js.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/content_search/models/search_mode.dart';
import 'package:win_text_editor/modules/content_search/models/search_result.dart';
import 'package:win_text_editor/modules/content_search/services/base_search_service.dart';
typedef ProgressCallback = void Function(double progress);
class CustomSearchService {
/// JavaScript
static Future<List<SearchResult>> performCustomSearch({
required String directory,
required String fileType,
required String jsFunction,
required SearchMode searchMode,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
final results = <SearchResult>[];
int count = 0;
final dir = Directory(directory);
final jsRuntime = getJavascriptRuntime();
try {
// JavaScript
final jsCode = 'function match(content){$jsFunction};';
jsRuntime.evaluate(jsCode);
int totalFiles = 0;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return results;
if (entity is File && BaseSearchService.matchesFileType(entity.path, fileType)) {
totalFiles++;
}
}
onProgress?.call(1);
int processedFiles = 0;
int calledProcessedFiles = 1;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return results;
if (entity is File && BaseSearchService.matchesFileType(entity.path, fileType)) {
try {
final lines = await entity.readAsLines();
for (int i = 0; i < lines.length; i++) {
if (shouldStop?.call() == true) return results;
final line = lines[i].trim();
if (line.length < 3) continue; //
final result = jsRuntime.evaluate('match(${jsonEncode(line)});');
if (result.isError) {
throw Exception('JS Error: ${result.stringResult}');
}
if (result.stringResult == 'true') {
if (searchMode == SearchMode.locate) {
results.add(
SearchResult(
filePath: entity.path,
lineNumber: i + 1,
lineContent: line,
matches: [],
queryTerm: "Custom Rule",
),
);
} else {
count++;
}
}
}
} catch (e) {
Logger().error('Error in file ${entity.path}: $e');
}
processedFiles++;
final progress = (processedFiles / totalFiles) * 99 + 1;
if (processedFiles - calledProcessedFiles >= 1) {
calledProcessedFiles = processedFiles;
onProgress?.call(progress);
}
}
}
//
if (searchMode == SearchMode.count) {
results.add(
SearchResult(
filePath: "Custom Rule",
lineNumber: count,
lineContent: '',
matches: [],
queryTerm: "Custom Rule",
),
);
}
} finally {
onProgress?.call(100);
jsRuntime.dispose();
}
return results;
}
/// JavaScript
static JsEvalResult validateJsRule(String jsFunction) {
final jsRuntime = getJavascriptRuntime();
try {
final jsCode = 'function match(content) {$jsFunction};';
// JS
final result = jsRuntime.evaluate(jsCode);
if (result.isError) {
return result;
}
//
final test = jsRuntime.evaluate("match('test');");
if (test.stringResult != 'true' && test.stringResult != 'false') {
return JsEvalResult('Custom rule must return boolean (true/false)', null, isError: true);
}
return test;
} finally {
jsRuntime.dispose();
}
}
}

112
win_text_editor/lib/modules/content_search/services/locate_search_service.dart

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
// lib/app/modules/content_search/services/locate_search_service.dart
import 'dart:io';
import 'package:win_text_editor/modules/content_search/models/match_result.dart';
import '../models/search_result.dart';
import 'base_search_service.dart';
class LocateSearchService {
static Future<List<SearchResult>> performSearch({
required String directory,
required String query,
required String fileType,
required bool caseSensitive,
required bool wholeWord,
required bool useRegex,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
final results = <SearchResult>[];
final queries = BaseSearchService.splitQuery(query);
await BaseSearchService.processFiles(
directory: directory,
fileType: fileType,
onProgress: onProgress,
shouldStop: shouldStop,
onFile: (file, content) async {
for (final q in queries) {
final pattern = BaseSearchService.buildSearchPattern(
query: q,
caseSensitive: caseSensitive,
wholeWord: wholeWord,
useRegex: useRegex,
);
final matches = pattern.allMatches(content);
if (matches.isNotEmpty) {
results.addAll(_convertMatchesToResults(file, content, matches, q));
}
}
},
);
return results;
}
/// SearchResult
static List<SearchResult> _convertMatchesToResults(
File file,
String content,
Iterable<RegExpMatch> matches,
String queryTerm,
) {
final results = <SearchResult>[];
final lines = content.split('\n');
// ->
final lineMatches = <int, List<RegExpMatch>>{};
for (final match in matches) {
//
final lineNumber = _getLineNumber(lines, match.start);
lineMatches.putIfAbsent(lineNumber, () => []).add(match);
}
//
lineMatches.forEach((lineNumber, matchesInLine) {
final lineContent = lines[lineNumber - 1]; // 1
final lineResults =
matchesInLine.map((m) {
//
final lineStartPos = _getLineStartPos(lines, lineNumber - 1);
final startInLine = m.start - lineStartPos;
final endInLine = m.end - lineStartPos;
return MatchResult(start: startInLine, end: endInLine);
}).toList();
results.add(
SearchResult(
filePath: file.path,
lineNumber: lineNumber,
lineContent: lineContent,
matches: lineResults,
queryTerm: queryTerm,
),
);
});
return results;
}
///
static int _getLineNumber(List<String> lines, int charPos) {
int currentPos = 0;
for (int i = 0; i < lines.length; i++) {
currentPos += lines[i].length + 1; // +1
if (charPos < currentPos) {
return i + 1; // 1
}
}
return lines.length; //
}
///
static int _getLineStartPos(List<String> lines, int lineIndex) {
int pos = 0;
for (int i = 0; i < lineIndex; i++) {
pos += lines[i].length + 1; // +1
}
return pos;
}
}

78
win_text_editor/lib/modules/content_search/widgets/search_settings.dart

@ -1,16 +1,72 @@ @@ -1,16 +1,72 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/shared/components/text_editor.dart';
import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart';
import 'package:win_text_editor/modules/content_search/models/search_mode.dart';
class SearchSettings extends StatelessWidget {
class SearchSettings extends StatefulWidget {
const SearchSettings({super.key});
@override
State<SearchSettings> createState() => _SearchSettingsState();
}
class _SearchSettingsState extends State<SearchSettings> {
Duration _elapsedTime = Duration.zero;
Stopwatch _stopwatch = Stopwatch();
Timer? _updateTimer;
@override
void dispose() {
_updateTimer?.cancel();
_stopwatch.stop();
super.dispose();
}
void _startTimer() {
_elapsedTime = Duration.zero;
_stopwatch
..reset()
..start();
// 100UI
_updateTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
setState(() {
_elapsedTime = _stopwatch.elapsed;
});
});
}
void _stopTimer() {
_updateTimer?.cancel();
_updateTimer = null;
_stopwatch.stop();
//
_elapsedTime = _stopwatch.elapsed;
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
String twoDigitMillis = twoDigits(duration.inMilliseconds.remainder(1000) ~/ 10);
return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds.$twoDigitMillis";
}
@override
Widget build(BuildContext context) {
final controller = context.watch<ContentSearchController>();
//
if (controller.isSearching && _updateTimer == null) {
_startTimer();
} else if (!controller.isSearching && _updateTimer != null) {
_stopTimer();
}
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
@ -227,17 +283,29 @@ class SearchSettings extends StatelessWidget { @@ -227,17 +283,29 @@ class SearchSettings extends StatelessWidget {
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.stop, size: 20),
label: const Text('停止'),
label: const Row(
mainAxisSize: MainAxisSize.min,
children: [Text('停止')],
),
onPressed:
controller.isSearching ? () => controller.stopSearch() : null,
controller.isSearching
? () {
controller.stopSearch();
_stopTimer();
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: controller.isSearching ? Colors.red : null,
foregroundColor: controller.isSearching ? Colors.white : null,
),
),
),
],
),
Text(
' 搜索计时:[${_formatDuration(_elapsedTime)}]',
style: const TextStyle(fontSize: 12),
),
],
),
),

1
win_text_editor/lib/modules/module_router.dart

@ -25,6 +25,7 @@ class RouterKey { @@ -25,6 +25,7 @@ class RouterKey {
static const String dataCompare = 'data_compare';
static const String memoryTable = 'memory_table';
static const String uftComponent = 'uft_component';
static const String callFunction = 'call_function';
static const String demo = 'demo';
}

Loading…
Cancel
Save