Browse Source

增加进度条和停止搜索开关

master
hejl 2 months ago
parent
commit
73e6077d4b
  1. BIN
      documents/~$ UFT模块迁移方案.docx
  2. 78
      win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart
  3. 104
      win_text_editor/lib/modules/content_search/services/content_search_service.dart
  4. 24
      win_text_editor/lib/modules/content_search/widgets/content_search_view.dart
  5. 38
      win_text_editor/lib/modules/content_search/widgets/search_settings.dart

BIN
documents/~$ UFT模块迁移方案.docx

Binary file not shown.

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

@ -4,6 +4,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:win_text_editor/framework/controllers/logger.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_mode.dart';
import 'package:win_text_editor/modules/content_search/models/search_result.dart'; import 'package:win_text_editor/modules/content_search/models/search_result.dart';
@ -20,8 +21,10 @@ class ContentSearchController extends BaseContentController {
bool _customRule = false; bool _customRule = false;
SearchMode _searchMode = SearchMode.locate; SearchMode _searchMode = SearchMode.locate;
final List<SearchResult> _results = []; final List<SearchResult> _results = [];
List<SearchResult> _allResults = []; final List<SearchResult> _allResults = [];
bool _includeZeroCounts = false; bool _includeZeroCounts = false;
bool _isSearching = false;
bool _shouldStop = false;
// Getters // Getters
String get searchQuery => _searchQuery; String get searchQuery => _searchQuery;
@ -35,6 +38,24 @@ class ContentSearchController extends BaseContentController {
List<SearchResult> get results => _results; List<SearchResult> get results => _results;
bool get includeZeroCounts => _includeZeroCounts; bool get includeZeroCounts => _includeZeroCounts;
double _progress = 0;
double get progress => _progress;
bool get isSearching => _isSearching;
//
void _updateProgress(double value) {
_progress = value.clamp(0.0, 100.0);
notifyListeners();
}
//
void stopSearch() {
_shouldStop = true;
_isSearching = false;
notifyListeners();
}
set customRule(bool value) { set customRule(bool value) {
_customRule = value; _customRule = value;
notifyListeners(); notifyListeners();
@ -111,8 +132,12 @@ class ContentSearchController extends BaseContentController {
} }
Future<void> startSearch() async { Future<void> startSearch() async {
_shouldStop = false;
_isSearching = true;
_progress = 0;
_results.clear(); _results.clear();
_allResults.clear(); _allResults.clear();
notifyListeners();
// //
if (searchQuery.isEmpty) { if (searchQuery.isEmpty) {
@ -127,23 +152,25 @@ class ContentSearchController extends BaseContentController {
return; return;
} }
if (customRule) { try {
final validationResult = ContentSearchService.validateJsRule(searchQuery); if (customRule) {
if (validationResult.isError) { final validationResult = ContentSearchService.validateJsRule(searchQuery);
Logger().error('JavaScript 语法错误: ${validationResult.rawResult.toString()}'); if (validationResult.isError) {
return; Logger().error('JavaScript 语法错误: ${validationResult.rawResult.toString()}');
} return;
}
_allResults.addAll( _allResults.addAll(
await ContentSearchService.performCustomSearch( await ContentSearchService.performCustomSearch(
directory: searchDirectory, directory: searchDirectory,
fileType: fileType, fileType: fileType,
jsFunction: searchQuery, jsFunction: searchQuery,
searchMode: searchMode, searchMode: searchMode,
), onProgress: _updateProgress,
); shouldStop: () => _shouldStop,
} else { ),
try { );
} else {
if (searchMode == SearchMode.locate) { if (searchMode == SearchMode.locate) {
_allResults.addAll( _allResults.addAll(
await ContentSearchService.performLocateSearch( await ContentSearchService.performLocateSearch(
@ -153,6 +180,8 @@ class ContentSearchController extends BaseContentController {
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
wholeWord: wholeWord, wholeWord: wholeWord,
useRegex: useRegex, useRegex: useRegex,
onProgress: _updateProgress,
shouldStop: () => _shouldStop,
), ),
); );
} else { } else {
@ -163,6 +192,8 @@ class ContentSearchController extends BaseContentController {
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
wholeWord: wholeWord, wholeWord: wholeWord,
useRegex: useRegex, useRegex: useRegex,
onProgress: _updateProgress,
shouldStop: () => _shouldStop,
); );
counts.forEach((keyword, count) { counts.forEach((keyword, count) {
@ -177,12 +208,15 @@ class ContentSearchController extends BaseContentController {
); );
}); });
} }
} catch (e) {
Logger().error("搜索出错: $e");
} finally {
_filterResults();
notifyListeners();
} }
} catch (e) {
Logger().error("搜索出错: $e");
} finally {
_isSearching = false;
_progress = 100;
_shouldStop = false;
_filterResults();
notifyListeners();
} }
} }

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

@ -9,6 +9,8 @@ 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_mode.dart';
import 'package:win_text_editor/modules/content_search/models/search_result.dart'; import 'package:win_text_editor/modules/content_search/models/search_result.dart';
typedef ProgressCallback = void Function(double progress);
class ContentSearchService { class ContentSearchService {
/// ///
static Future<List<SearchResult>> performLocateSearch({ static Future<List<SearchResult>> performLocateSearch({
@ -18,29 +20,57 @@ class ContentSearchService {
required bool caseSensitive, required bool caseSensitive,
required bool wholeWord, required bool wholeWord,
required bool useRegex, required bool useRegex,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async { }) async {
final results = <SearchResult>[]; final results = <SearchResult>[];
final dir = Directory(directory); final dir = Directory(directory);
final queries = _splitQuery(query); // final queries = _splitQuery(query);
int speed = 0;
//
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) { for (final q in queries) {
if (shouldStop?.call() == true) return results;
final pattern = _buildSearchPattern( final pattern = _buildSearchPattern(
query: q, query: q,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
wholeWord: wholeWord, wholeWord: wholeWord,
useRegex: useRegex, useRegex: useRegex,
); );
Logger().info("搜索词 $q 开始[${++speed}/${queries.length}]");
int fileCount = 0; Logger().info("搜索词 $q 开始[${++words}/${queries.length}]");
await for (final entity in dir.list(recursive: true)) { await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return results;
if (entity is File && _matchesFileType(entity.path, fileType)) { if (entity is File && _matchesFileType(entity.path, fileType)) {
fileCount++; processedFiles++;
await _searchInFile(entity, pattern, results, q); // 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 结束[$speed/${queries.length}],搜索文件 $fileCount 个,"); Logger().info("搜索词 $q 结束[$words/${queries.length}],搜索文件 $totalFiles 个,");
} }
onProgress?.call(100);
return results; return results;
} }
@ -52,12 +82,29 @@ class ContentSearchService {
required bool caseSensitive, required bool caseSensitive,
required bool wholeWord, required bool wholeWord,
required bool useRegex, required bool useRegex,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async { }) async {
final counts = <String, int>{}; final counts = <String, int>{};
final dir = Directory(directory); final dir = Directory(directory);
final queries = _splitQuery(query); // final queries = _splitQuery(query); //
int speed = 0;
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) { for (final q in queries) {
if (shouldStop?.call() == true) return counts;
final pattern = _buildSearchPattern( final pattern = _buildSearchPattern(
query: q, query: q,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
@ -65,17 +112,25 @@ class ContentSearchService {
useRegex: useRegex, useRegex: useRegex,
); );
Logger().info("搜索词 $q 开始[${++speed}/${queries.length}]"); Logger().info("搜索词 $q 开始[${++words}/${queries.length}]");
counts[q] = 0; counts[q] = 0;
int fileCount = 0;
await for (final entity in dir.list(recursive: true)) { await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return counts;
if (entity is File && _matchesFileType(entity.path, fileType)) { if (entity is File && _matchesFileType(entity.path, fileType)) {
fileCount++; 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); // await _countInFile(entity, pattern, counts, q); //
} }
} }
Logger().info("搜索词 $q 结束[$speed/${queries.length}],搜索文件 $fileCount 个,"); Logger().info("搜索词 $q 结束[$words/${queries.length}],搜索文件 $totalFiles 个,");
} }
return counts; return counts;
@ -87,6 +142,8 @@ class ContentSearchService {
required String fileType, required String fileType,
required String jsFunction, required String jsFunction,
required SearchMode searchMode, required SearchMode searchMode,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async { }) async {
final results = <SearchResult>[]; final results = <SearchResult>[];
int count = 0; int count = 0;
@ -99,11 +156,26 @@ class ContentSearchService {
jsRuntime.evaluate(jsCode); jsRuntime.evaluate(jsCode);
int totalFiles = 0;
await for (final entity in dir.list(recursive: true)) { 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)) { if (entity is File && _matchesFileType(entity.path, fileType)) {
try { try {
final lines = await entity.readAsLines(); final lines = await entity.readAsLines();
for (int i = 0; i < lines.length; i++) { for (int i = 0; i < lines.length; i++) {
if (shouldStop?.call() == true) return results;
final line = lines[i].trim(); final line = lines[i].trim();
if (line.length < 3) continue; // if (line.length < 3) continue; //
@ -131,6 +203,13 @@ class ContentSearchService {
} catch (e) { } catch (e) {
Logger().error('Error in file ${entity.path}: $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);
}
} }
} }
@ -147,6 +226,7 @@ class ContentSearchService {
); );
} }
} finally { } finally {
onProgress?.call(100);
jsRuntime.dispose(); jsRuntime.dispose();
} }

24
win_text_editor/lib/modules/content_search/widgets/content_search_view.dart

@ -34,13 +34,27 @@ class ContentSearchViewState extends State<ContentSearchView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: _controller, value: _controller,
child: const Padding( child: Padding(
padding: EdgeInsets.all(4.0), padding: const EdgeInsets.all(4.0),
child: Column( child: Column(
children: [ children: [
DirectorySettings(), // controller const DirectorySettings(), // controller
SearchSettings(), const SearchSettings(),
Expanded(child: ResultsView()), Consumer<ContentSearchController>(
builder: (context, controller, _) {
return Column(
children: [
LinearProgressIndicator(
value: controller.progress / 100,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(Colors.green),
minHeight: 8,
),
],
);
},
),
const Expanded(child: ResultsView()),
], ],
), ),
), ),

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

@ -20,7 +20,7 @@ class SearchSettings extends StatelessWidget {
// () // ()
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width * 0.5, width: MediaQuery.of(context).size.width * 0.5,
height: 300, height: 360,
child: TextEditor( child: TextEditor(
tabId: 'search_content_${controller.hashCode}', tabId: 'search_content_${controller.hashCode}',
title: '搜索内容[列表以半角逗号分隔]', title: '搜索内容[列表以半角逗号分隔]',
@ -34,7 +34,7 @@ class SearchSettings extends StatelessWidget {
// //
Expanded( Expanded(
child: Container( child: Container(
height: 300, height: 360,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.grey), border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
@ -212,15 +212,31 @@ class SearchSettings extends StatelessWidget {
), ),
), ),
// //
Align( Row(
alignment: Alignment.bottomCenter, mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: ElevatedButton.icon( children: [
icon: const Icon(Icons.search, size: 20), Expanded(
label: const Text('开始搜索'), child: ElevatedButton.icon(
onPressed: () { icon: const Icon(Icons.search, size: 20),
controller.startSearch(); label: const Text('开始搜索'),
}, onPressed:
), controller.isSearching ? null : () => controller.startSearch(),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.stop, size: 20),
label: const Text('停止'),
onPressed:
controller.isSearching ? () => controller.stopSearch() : null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
),
],
), ),
], ],
), ),

Loading…
Cancel
Save