|
|
|
@ -6,10 +6,13 @@ import 'package:path/path.dart' as path;
@@ -6,10 +6,13 @@ import 'package:path/path.dart' as path;
|
|
|
|
|
import 'package:win_text_editor/framework/controllers/logger.dart'; |
|
|
|
|
import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart'; |
|
|
|
|
import 'package:file_picker/file_picker.dart'; |
|
|
|
|
import 'package:win_text_editor/modules/content_search/models/count_result.dart'; |
|
|
|
|
import 'dart:io'; |
|
|
|
|
|
|
|
|
|
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/shared/base/my_sf_data_grid.dart'; |
|
|
|
|
import 'package:win_text_editor/shared/base/my_sf_data_source.dart'; |
|
|
|
|
import 'package:win_text_editor/shared/components/my_grid_column.dart'; |
|
|
|
|
// import 'package:recycle_bin/recycle_bin.dart'; |
|
|
|
|
|
|
|
|
@ -29,88 +32,15 @@ class ResultsView extends StatelessWidget {
@@ -29,88 +32,15 @@ class ResultsView extends StatelessWidget {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return Card( |
|
|
|
|
child: GestureDetector( |
|
|
|
|
onSecondaryTapDown: (details) { |
|
|
|
|
_showContextMenu(context, details.globalPosition, controller); |
|
|
|
|
}, |
|
|
|
|
child: _buildResultsGrid(controller, context), |
|
|
|
|
), |
|
|
|
|
child: |
|
|
|
|
controller.searchMode == SearchMode.locate |
|
|
|
|
? _buildLocateGrid(controller, context) |
|
|
|
|
: _buildCountGrid(controller, context), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Future<void> _showContextMenu( |
|
|
|
|
BuildContext context, |
|
|
|
|
Offset position, |
|
|
|
|
ContentSearchController controller, |
|
|
|
|
) async { |
|
|
|
|
// 获取渲染对象以正确定位菜单 |
|
|
|
|
final renderBox = context.findRenderObject() as RenderBox; |
|
|
|
|
final localPosition = renderBox.globalToLocal(position); |
|
|
|
|
|
|
|
|
|
// 显示菜单并等待选择结果 |
|
|
|
|
final result = await showMenu<String>( |
|
|
|
|
context: context, |
|
|
|
|
position: RelativeRect.fromLTRB( |
|
|
|
|
position.dx, |
|
|
|
|
position.dy, |
|
|
|
|
position.dx + renderBox.size.width - localPosition.dx, |
|
|
|
|
position.dy + renderBox.size.height - localPosition.dy, |
|
|
|
|
), |
|
|
|
|
items: [const PopupMenuItem<String>(value: 'export', child: Text('导出(csv)'))], |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// 处理菜单选择结果 |
|
|
|
|
if (result == 'export' && context.mounted) { |
|
|
|
|
try { |
|
|
|
|
await _exportToCsv(controller); |
|
|
|
|
} catch (e) { |
|
|
|
|
if (context.mounted) { |
|
|
|
|
ScaffoldMessenger.of( |
|
|
|
|
context, |
|
|
|
|
).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}'))); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Future<void> _exportToCsv(ContentSearchController controller) async { |
|
|
|
|
String csvData = ''; |
|
|
|
|
if (controller.searchMode == SearchMode.locate) { |
|
|
|
|
csvData = '文件\t行号\t内容\n'; |
|
|
|
|
for (var result in controller.results) { |
|
|
|
|
csvData += |
|
|
|
|
'${path.basename(result.filePath)}\t${result.lineNumber}\t${result.lineContent}\n'; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
csvData = '关键词\t匹配数量\n'; |
|
|
|
|
for (var result in controller.results) { |
|
|
|
|
csvData += '${result.filePath}\t${result.lineNumber}\n'; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
final filePath = await FilePicker.platform.saveFile( |
|
|
|
|
dialogTitle: '保存导出结果', |
|
|
|
|
fileName: 'search_results.csv', |
|
|
|
|
type: FileType.custom, |
|
|
|
|
allowedExtensions: ['csv'], |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (filePath != null) { |
|
|
|
|
final file = File(filePath); |
|
|
|
|
await file.writeAsString(csvData); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Widget _buildResultsGrid(ContentSearchController controller, BuildContext context) { |
|
|
|
|
return controller.searchMode == SearchMode.locate |
|
|
|
|
? _buildLocateGrid(controller, context) |
|
|
|
|
: _buildCountGrid(controller); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Widget _buildLocateGrid(ContentSearchController controller, BuildContext context) { |
|
|
|
|
return SfDataGrid( |
|
|
|
|
rowHeight: 32, |
|
|
|
|
headerRowHeight: 32, |
|
|
|
|
return MySfDataGrid<SearchResult>( |
|
|
|
|
source: LocateDataSource(controller, context), |
|
|
|
|
columns: [ |
|
|
|
|
ShortGridColumn(columnName: 'index', label: '序号'), |
|
|
|
@ -118,45 +48,29 @@ class ResultsView extends StatelessWidget {
@@ -118,45 +48,29 @@ class ResultsView extends StatelessWidget {
|
|
|
|
|
MyGridColumn(columnName: 'content', label: '内容'), |
|
|
|
|
ShortGridColumn(columnName: 'action', label: '操作', width: 90), |
|
|
|
|
], |
|
|
|
|
selectionMode: SelectionMode.multiple, |
|
|
|
|
navigationMode: GridNavigationMode.cell, |
|
|
|
|
gridLinesVisibility: GridLinesVisibility.both, |
|
|
|
|
headerGridLinesVisibility: GridLinesVisibility.both, |
|
|
|
|
allowSorting: false, |
|
|
|
|
allowFiltering: false, |
|
|
|
|
columnWidthMode: ColumnWidthMode.fill, |
|
|
|
|
isScrollbarAlwaysShown: true, |
|
|
|
|
allowColumnsResizing: true, // 关键开关 |
|
|
|
|
columnResizeMode: ColumnResizeMode.onResizeEnd, |
|
|
|
|
selectable: false, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Widget _buildCountGrid(ContentSearchController controller) { |
|
|
|
|
return SfDataGrid( |
|
|
|
|
rowHeight: 32, |
|
|
|
|
headerRowHeight: 32, |
|
|
|
|
source: CountDataSource(controller), |
|
|
|
|
Widget _buildCountGrid(ContentSearchController controller, BuildContext context) { |
|
|
|
|
return MySfDataGrid<CountResult>( |
|
|
|
|
source: CountDataSource(controller, context), |
|
|
|
|
columns: [ |
|
|
|
|
ShortGridColumn(columnName: 'index', label: '序号'), |
|
|
|
|
MyGridColumn(columnName: 'keyword', label: '关键词', minimumWidth: 300), |
|
|
|
|
MyGridColumn(columnName: 'count', label: '匹配数量'), |
|
|
|
|
MyGridColumn(columnName: 'matchCount', label: '匹配数量'), |
|
|
|
|
], |
|
|
|
|
selectionMode: SelectionMode.multiple, |
|
|
|
|
navigationMode: GridNavigationMode.cell, |
|
|
|
|
allowColumnsResizing: true, |
|
|
|
|
gridLinesVisibility: GridLinesVisibility.both, |
|
|
|
|
headerGridLinesVisibility: GridLinesVisibility.both, |
|
|
|
|
columnWidthMode: ColumnWidthMode.fill, |
|
|
|
|
isScrollbarAlwaysShown: true, |
|
|
|
|
selectable: false, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class LocateDataSource extends DataGridSource { |
|
|
|
|
class LocateDataSource extends MySfDataSource<SearchResult> { |
|
|
|
|
final ContentSearchController controller; |
|
|
|
|
final BuildContext context; |
|
|
|
|
|
|
|
|
|
LocateDataSource(this.controller, this.context); |
|
|
|
|
LocateDataSource(this.controller, this.context) |
|
|
|
|
: super(controller.results, onSelectionChanged: (index, isSelected) {}); |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
List<DataGridRow> get rows => |
|
|
|
@ -182,7 +96,7 @@ class LocateDataSource extends DataGridSource {
@@ -182,7 +96,7 @@ class LocateDataSource extends DataGridSource {
|
|
|
|
|
}).toList(); |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
DataGridRowAdapter? buildRow(DataGridRow row) { |
|
|
|
|
DataGridRowAdapter buildRow(DataGridRow row) { |
|
|
|
|
final cells = row.getCells(); |
|
|
|
|
final result = cells[2].value as SearchResult; |
|
|
|
|
|
|
|
|
@ -357,11 +271,13 @@ class LocateDataSource extends DataGridSource {
@@ -357,11 +271,13 @@ class LocateDataSource extends DataGridSource {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class CountDataSource extends DataGridSource { |
|
|
|
|
class CountDataSource extends MySfDataSource<CountResult> { |
|
|
|
|
final ContentSearchController controller; |
|
|
|
|
final BuildContext context; |
|
|
|
|
late final List<MapEntry<String, int>> _counts; |
|
|
|
|
|
|
|
|
|
CountDataSource(this.controller) { |
|
|
|
|
CountDataSource(this.controller, this.context) |
|
|
|
|
: super([], onSelectionChanged: (index, isSelected) {}) { |
|
|
|
|
final counts = <String, int>{}; |
|
|
|
|
for (var result in controller.results) { |
|
|
|
|
counts[result.filePath] = (counts[result.filePath] ?? 0) + result.lineNumber; |
|
|
|
@ -383,7 +299,7 @@ class CountDataSource extends DataGridSource {
@@ -383,7 +299,7 @@ class CountDataSource extends DataGridSource {
|
|
|
|
|
}).toList(); |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
DataGridRowAdapter? buildRow(DataGridRow row) { |
|
|
|
|
DataGridRowAdapter buildRow(DataGridRow row) { |
|
|
|
|
return DataGridRowAdapter( |
|
|
|
|
cells: |
|
|
|
|
row.getCells().map((cell) { |
|
|
|
@ -397,7 +313,6 @@ class CountDataSource extends DataGridSource {
@@ -397,7 +313,6 @@ class CountDataSource extends DataGridSource {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 自定义Action标识类 |
|
|
|
|
class _ConfirmAction extends Intent { |
|
|
|
|
const _ConfirmAction(); |
|
|
|
|
} |
|
|
|
|