22 changed files with 732 additions and 74 deletions
@ -0,0 +1,91 @@ |
|||||||
|
import 'package:file_picker/file_picker.dart'; |
||||||
|
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||||
|
import 'package:win_text_editor/modules/xml_search/models/search_result.dart'; |
||||||
|
import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart'; |
||||||
|
import 'package:win_text_editor/modules/xml_search/services/xml_search_service.dart'; |
||||||
|
import 'package:win_text_editor/shared/base/base_content_controller.dart'; |
||||||
|
|
||||||
|
class XmlSearchController extends BaseContentController { |
||||||
|
String _searchDirectory = ''; |
||||||
|
String _fileType = '*.*'; |
||||||
|
final List<SearchResult> _results = []; |
||||||
|
final List<XmlRule> _rules = []; |
||||||
|
final XmlSearchService _searchService = XmlSearchService(); |
||||||
|
|
||||||
|
List<SearchResult> get results => _results; |
||||||
|
List<XmlRule> get rules => _rules; |
||||||
|
|
||||||
|
String get searchDirectory => _searchDirectory; |
||||||
|
String get fileType => _fileType; |
||||||
|
|
||||||
|
bool _isSearching = false; |
||||||
|
|
||||||
|
bool onlyFileName = false; |
||||||
|
bool get isSearching => _isSearching; |
||||||
|
|
||||||
|
Future<void> executeSearching() async { |
||||||
|
Logger().info("开始提取目录:$_searchDirectory, 文件名:$_fileType"); |
||||||
|
if (_searchDirectory.isEmpty || _rules.isEmpty) return; |
||||||
|
|
||||||
|
_isSearching = true; |
||||||
|
notifyListeners(); |
||||||
|
|
||||||
|
_results.clear(); |
||||||
|
notifyListeners(); |
||||||
|
|
||||||
|
try { |
||||||
|
final newResults = await _searchService.searchFromDirectory( |
||||||
|
directory: _searchDirectory, |
||||||
|
fileType: _fileType, |
||||||
|
rule: _rules[0], |
||||||
|
); |
||||||
|
_results.addAll(newResults); |
||||||
|
} catch (e) { |
||||||
|
Logger().error("提取目录出错:$e"); |
||||||
|
_results.add(SearchResult(rowNum: 1, filePath: 'Error', content: 'Searchion failed: $e')); |
||||||
|
} finally { |
||||||
|
_isSearching = false; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
set searchDirectory(String value) { |
||||||
|
_searchDirectory = value; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
set fileType(String value) { |
||||||
|
_fileType = value; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> pickDirectory() async { |
||||||
|
final dir = await FilePicker.platform.getDirectoryPath(); |
||||||
|
if (dir != null) { |
||||||
|
searchDirectory = dir; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void setRule(XmlRule rule) { |
||||||
|
_rules.clear(); |
||||||
|
rules.add(rule); |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
void removeRule(int index) { |
||||||
|
_rules.removeAt(index); |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void onOpenFile(String filePath) { |
||||||
|
// TODO: implement onOpenFile |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void onOpenFolder(String folderPath) { |
||||||
|
searchDirectory = folderPath; |
||||||
|
} |
||||||
|
|
||||||
|
void cancelSearchion() {} |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
// search_result.dart |
||||||
|
class SearchResult { |
||||||
|
final int rowNum; |
||||||
|
final String filePath; |
||||||
|
final String content; |
||||||
|
|
||||||
|
SearchResult({required this.rowNum, required this.filePath, required this.content}); |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
// xml_rule.dart |
||||||
|
class XmlRule { |
||||||
|
final String nodePath; |
||||||
|
final String attributeName; |
||||||
|
final bool isFirstOccurrence; |
||||||
|
final String? namespacePrefix; |
||||||
|
|
||||||
|
XmlRule({ |
||||||
|
required this.nodePath, |
||||||
|
required this.attributeName, |
||||||
|
this.isFirstOccurrence = false, |
||||||
|
this.namespacePrefix, |
||||||
|
}); |
||||||
|
|
||||||
|
String toxPath() { |
||||||
|
return '${namespacePrefix != null ? '$namespacePrefix:' : ''}$nodePath${isFirstOccurrence ? '[0]' : ''}/${attributeName.isNotEmpty ? '@$attributeName' : 'text()'}'; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
// xml_search_service.dart |
||||||
|
import 'dart:io'; |
||||||
|
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||||
|
import 'package:win_text_editor/shared/utils/file_utils.dart'; |
||||||
|
import 'package:xml/xml.dart' as xml; |
||||||
|
import 'package:win_text_editor/modules/xml_search/models/search_result.dart'; |
||||||
|
import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart'; |
||||||
|
|
||||||
|
class XmlSearchService { |
||||||
|
Future<List<SearchResult>> searchFromDirectory({ |
||||||
|
required String directory, |
||||||
|
required String fileType, |
||||||
|
required XmlRule rule, |
||||||
|
}) async { |
||||||
|
final results = <SearchResult>[]; |
||||||
|
final dir = Directory(directory); |
||||||
|
int rowNum = 1; |
||||||
|
|
||||||
|
await for (var entity in dir.list(recursive: true)) { |
||||||
|
if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) { |
||||||
|
try { |
||||||
|
final fileContent = await entity.readAsString(); |
||||||
|
final document = xml.XmlDocument.parse(fileContent); |
||||||
|
final values = _searchWithRule(document, rule); |
||||||
|
for (var value in values) { |
||||||
|
results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: value)); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
Logger().error('xmlSearchService.searchFromDirectory方法执行出错: $e'); |
||||||
|
results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: 'Error: $e')); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return results; |
||||||
|
} |
||||||
|
|
||||||
|
List<String> _searchWithRule(xml.XmlDocument document, XmlRule rule) { |
||||||
|
final nodes = document.findAllElements(rule.nodePath); |
||||||
|
//final nodes = SimpleXPath.query(document, rule.toxPath()); |
||||||
|
if (rule.isFirstOccurrence && nodes.isNotEmpty) { |
||||||
|
final attr = nodes.first.getAttribute(rule.attributeName); |
||||||
|
return attr != null ? [attr] : []; |
||||||
|
} else { |
||||||
|
return nodes |
||||||
|
.map((node) => node.getAttribute(rule.attributeName)) |
||||||
|
.where((attr) => attr != null) |
||||||
|
.cast<String>() |
||||||
|
.toList(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,140 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; |
||||||
|
import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart'; |
||||||
|
import 'package:win_text_editor/shared/components/my_checkbox.dart'; |
||||||
|
|
||||||
|
class ConditionSetting extends StatefulWidget { |
||||||
|
const ConditionSetting({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
State<ConditionSetting> createState() => _ConditionSettingState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _ConditionSettingState extends State<ConditionSetting> { |
||||||
|
final _nodePathController = TextEditingController(); |
||||||
|
final _attributeNameController = TextEditingController(); |
||||||
|
final _namespacePrefixController = TextEditingController(); |
||||||
|
bool _isFirstOccurrence = false; |
||||||
|
bool _isExtracting = false; |
||||||
|
|
||||||
|
@override |
||||||
|
void dispose() { |
||||||
|
_nodePathController.dispose(); |
||||||
|
_attributeNameController.dispose(); |
||||||
|
_namespacePrefixController.dispose(); |
||||||
|
super.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final controller = context.watch<XmlSearchController>(); |
||||||
|
|
||||||
|
return Card( |
||||||
|
child: Padding( |
||||||
|
padding: const EdgeInsets.all(16.0), |
||||||
|
child: Column( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch, |
||||||
|
children: [ |
||||||
|
const Text('数据提取规则设置:', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), |
||||||
|
const SizedBox(height: 16), |
||||||
|
|
||||||
|
// 规则输入表单 |
||||||
|
TextField( |
||||||
|
controller: _nodePathController, |
||||||
|
decoration: const InputDecoration( |
||||||
|
labelText: '节点名称', |
||||||
|
hintText: '如: business:Service', |
||||||
|
border: OutlineInputBorder(), |
||||||
|
), |
||||||
|
), |
||||||
|
const SizedBox(height: 12), |
||||||
|
TextField( |
||||||
|
controller: _attributeNameController, |
||||||
|
decoration: const InputDecoration( |
||||||
|
labelText: '属性名称', |
||||||
|
hintText: '如: chineseName 或 name', |
||||||
|
border: OutlineInputBorder(), |
||||||
|
), |
||||||
|
), |
||||||
|
|
||||||
|
const SizedBox(height: 12), |
||||||
|
|
||||||
|
MyCheckbox( |
||||||
|
title: '仅提取第一个匹配项', |
||||||
|
value: _isFirstOccurrence, |
||||||
|
onChanged: (value) => setState(() => _isFirstOccurrence = value ?? false), |
||||||
|
), |
||||||
|
const SizedBox(height: 16), |
||||||
|
// 操作按钮行 |
||||||
|
Row( |
||||||
|
children: [ |
||||||
|
Expanded( |
||||||
|
child: ElevatedButton.icon( |
||||||
|
icon: const Icon(Icons.play_arrow), |
||||||
|
label: const Text('开始'), |
||||||
|
onPressed: _isExtracting ? null : () => _startSearching(controller), |
||||||
|
), |
||||||
|
), |
||||||
|
const SizedBox(width: 12), |
||||||
|
Expanded( |
||||||
|
child: ElevatedButton.icon( |
||||||
|
icon: const Icon(Icons.stop, color: Colors.red), |
||||||
|
label: const Text('停止', style: TextStyle(color: Colors.red)), |
||||||
|
onPressed: _isExtracting ? _stopExtraction : null, |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
void _setRule() { |
||||||
|
final nodePath = _nodePathController.text.trim(); |
||||||
|
final attributeName = _attributeNameController.text.trim(); |
||||||
|
|
||||||
|
if (nodePath.isNotEmpty && attributeName.isNotEmpty) { |
||||||
|
final controller = Provider.of<XmlSearchController>(context, listen: false); |
||||||
|
controller.setRule( |
||||||
|
XmlRule( |
||||||
|
nodePath: nodePath, |
||||||
|
attributeName: attributeName, |
||||||
|
isFirstOccurrence: _isFirstOccurrence, |
||||||
|
namespacePrefix: |
||||||
|
_namespacePrefixController.text.trim().isNotEmpty |
||||||
|
? _namespacePrefixController.text.trim() |
||||||
|
: null, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> _startSearching(XmlSearchController controller) async { |
||||||
|
if (controller.searchDirectory.isEmpty) { |
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择搜索目录'))); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
_setRule(); |
||||||
|
|
||||||
|
setState(() => _isExtracting = true); |
||||||
|
try { |
||||||
|
await controller.executeSearching(); |
||||||
|
} finally { |
||||||
|
if (mounted) { |
||||||
|
setState(() => _isExtracting = false); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void _stopExtraction() { |
||||||
|
// 这里需要确保控制器中有取消提取的逻辑 |
||||||
|
final controller = Provider.of<XmlSearchController>(context, listen: false); |
||||||
|
// 假设控制器中有cancelExtraction方法 |
||||||
|
controller.executeSearching(); |
||||||
|
setState(() => _isExtracting = false); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,86 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; |
||||||
|
|
||||||
|
class Directory extends StatefulWidget { |
||||||
|
const Directory({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
State<Directory> createState() => _DirectoryState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _DirectoryState extends State<Directory> { |
||||||
|
late TextEditingController _searchDirectoryController; |
||||||
|
late TextEditingController _fileTypeController; |
||||||
|
|
||||||
|
@override |
||||||
|
void initState() { |
||||||
|
super.initState(); |
||||||
|
final controller = context.read<XmlSearchController>(); |
||||||
|
_searchDirectoryController = TextEditingController(text: controller.searchDirectory); |
||||||
|
_fileTypeController = TextEditingController(text: controller.fileType); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void dispose() { |
||||||
|
_searchDirectoryController.dispose(); |
||||||
|
_fileTypeController.dispose(); |
||||||
|
super.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Consumer<XmlSearchController>( |
||||||
|
builder: (context, controller, child) { |
||||||
|
// 同步 TextEditingController 的值 |
||||||
|
if (_searchDirectoryController.text != controller.searchDirectory) { |
||||||
|
_searchDirectoryController.text = controller.searchDirectory; |
||||||
|
} |
||||||
|
if (_fileTypeController.text != controller.fileType) { |
||||||
|
_fileTypeController.text = controller.fileType; |
||||||
|
} |
||||||
|
|
||||||
|
return Card( |
||||||
|
child: Padding( |
||||||
|
padding: const EdgeInsets.all(8.0), |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
Expanded( |
||||||
|
child: TextField( |
||||||
|
controller: _searchDirectoryController, |
||||||
|
decoration: const InputDecoration( |
||||||
|
labelText: '搜索目录', |
||||||
|
border: OutlineInputBorder(), |
||||||
|
), |
||||||
|
onChanged: (value) => controller.searchDirectory = value, |
||||||
|
), |
||||||
|
), |
||||||
|
const SizedBox(width: 8), |
||||||
|
SizedBox( |
||||||
|
width: 100, |
||||||
|
child: TextField( |
||||||
|
controller: _fileTypeController, |
||||||
|
decoration: const InputDecoration( |
||||||
|
labelText: '文件类型', |
||||||
|
border: OutlineInputBorder(), |
||||||
|
), |
||||||
|
onChanged: (value) => controller.fileType = value, |
||||||
|
), |
||||||
|
), |
||||||
|
const SizedBox(width: 8), |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.folder_open), |
||||||
|
onPressed: () async { |
||||||
|
await controller.pickDirectory(); |
||||||
|
// 不需要手动更新 _searchDirectoryController.text, |
||||||
|
// 因为 Consumer 会触发重建并自动同步 |
||||||
|
}, |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,154 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
import 'package:syncfusion_flutter_datagrid/datagrid.dart'; |
||||||
|
import 'package:path/path.dart' as path; |
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart'; |
||||||
|
import 'dart:io'; |
||||||
|
|
||||||
|
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; |
||||||
|
import 'package:win_text_editor/shared/components/my_grid_column.dart'; |
||||||
|
|
||||||
|
class ResultsView extends StatelessWidget { |
||||||
|
const ResultsView({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final controller = context.watch<XmlSearchController>(); |
||||||
|
|
||||||
|
return Card( |
||||||
|
child: GestureDetector( |
||||||
|
onSecondaryTapDown: (details) { |
||||||
|
_showContextMenu(context, details.globalPosition, controller); |
||||||
|
}, |
||||||
|
child: _buildLocateGrid(controller), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> _showContextMenu( |
||||||
|
BuildContext context, |
||||||
|
Offset position, |
||||||
|
XmlSearchController 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: 'exportFileName', child: Text('导出文件名(csv)')), |
||||||
|
const PopupMenuItem<String>(value: 'exportContent', child: Text('导出内容(csv)')), |
||||||
|
const PopupMenuItem<String>(value: 'exportAll', child: Text('导出全部(csv)')), |
||||||
|
], |
||||||
|
); |
||||||
|
|
||||||
|
// 处理菜单选择结果 |
||||||
|
if (result != null && result.startsWith('export') && context.mounted) { |
||||||
|
try { |
||||||
|
await _exportToCsv(controller, result); |
||||||
|
} catch (e) { |
||||||
|
if (context.mounted) { |
||||||
|
ScaffoldMessenger.of( |
||||||
|
context, |
||||||
|
).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}'))); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> _exportToCsv(XmlSearchController controller, String? exportType) async { |
||||||
|
String csvData = ''; |
||||||
|
csvData = |
||||||
|
exportType == 'exportFileName' |
||||||
|
? '文件名称\n' |
||||||
|
: (exportType == 'exportContent' ? '内容\n' : '文件名称\t内容\n'); |
||||||
|
for (var result in controller.results) { |
||||||
|
switch (exportType) { |
||||||
|
case 'exportFileName': |
||||||
|
csvData += '${path.basename(result.filePath)}\n'; |
||||||
|
break; |
||||||
|
case 'exportContent': |
||||||
|
csvData += '${result.content}\n'; |
||||||
|
break; |
||||||
|
default: |
||||||
|
csvData += '${path.basename(result.filePath)}\t${result.content}\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 _buildLocateGrid(XmlSearchController controller) { |
||||||
|
return SfDataGrid( |
||||||
|
rowHeight: 32, |
||||||
|
headerRowHeight: 32, |
||||||
|
source: LocateDataSource(controller), |
||||||
|
columns: [ |
||||||
|
ShortGridColumn(columnName: 'rowNum', label: '序号'), |
||||||
|
MyGridColumn(columnName: 'file', label: '文件名称', minimumWidth: 300), |
||||||
|
MyGridColumn(columnName: 'content', label: '内容'), |
||||||
|
], |
||||||
|
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, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class LocateDataSource extends DataGridSource { |
||||||
|
final XmlSearchController controller; |
||||||
|
|
||||||
|
LocateDataSource(this.controller); |
||||||
|
|
||||||
|
@override |
||||||
|
List<DataGridRow> get rows => |
||||||
|
controller.results.map((result) { |
||||||
|
return DataGridRow( |
||||||
|
cells: [ |
||||||
|
DataGridCell(columnName: 'rowNum', value: result.rowNum), |
||||||
|
DataGridCell(columnName: 'file', value: path.basename(result.filePath)), |
||||||
|
DataGridCell(columnName: 'content', value: result.content), |
||||||
|
], |
||||||
|
); |
||||||
|
}).toList(); |
||||||
|
|
||||||
|
@override |
||||||
|
DataGridRowAdapter? buildRow(DataGridRow row) { |
||||||
|
return DataGridRowAdapter( |
||||||
|
cells: |
||||||
|
row.getCells().map<Widget>((cell) { |
||||||
|
return Container( |
||||||
|
alignment: Alignment.centerLeft, |
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8), |
||||||
|
child: Text(cell.value.toString(), overflow: TextOverflow.ellipsis), |
||||||
|
); |
||||||
|
}).toList(), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; |
||||||
|
import 'package:win_text_editor/modules/xml_search/widgets/condition_setting.dart'; |
||||||
|
import 'results_view.dart'; |
||||||
|
import 'directory.dart'; |
||||||
|
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; |
||||||
|
|
||||||
|
class XmlSearchView extends StatefulWidget { |
||||||
|
final String tabId; |
||||||
|
const XmlSearchView({super.key, required this.tabId}); |
||||||
|
|
||||||
|
@override |
||||||
|
XmlSearchViewState createState() => XmlSearchViewState(); |
||||||
|
} |
||||||
|
|
||||||
|
class XmlSearchViewState extends State<XmlSearchView> { |
||||||
|
late final XmlSearchController _controller; |
||||||
|
|
||||||
|
get tabManager => Provider.of<TabItemsController>(context, listen: false); |
||||||
|
|
||||||
|
@override |
||||||
|
void initState() { |
||||||
|
super.initState(); |
||||||
|
_controller = tabManager.getController(widget.tabId) ?? XmlSearchController(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void dispose() { |
||||||
|
super.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return ChangeNotifierProvider.value( |
||||||
|
value: _controller, |
||||||
|
child: Consumer<XmlSearchController>( |
||||||
|
builder: (context, controller, child) { |
||||||
|
return const Padding( |
||||||
|
padding: EdgeInsets.all(4.0), |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
Directory(), |
||||||
|
Expanded( |
||||||
|
child: Row( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||||
|
children: [ |
||||||
|
// 左侧部分 (50%) |
||||||
|
Expanded(flex: 3, child: ConditionSetting()), |
||||||
|
SizedBox(width: 8), |
||||||
|
// 右侧部分 (50%) |
||||||
|
Expanded(flex: 7, child: ResultsView()), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
}, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue