Files
ble_web_front/lib/views/ControlPanelView.dart
2026-04-25 17:45:10 +08:00

1098 lines
40 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:web_frontend/controllers/ControlPanelController.dart';
import 'package:web_frontend/models/LogType.dart';
import 'package:web_frontend/models/UserInfo.dart';
import 'package:web_frontend/services/WebSocketService.dart';
class ControlPanelView extends StatelessWidget {
ControlPanelView({super.key});
final GlobalKey _dropdownButtonKey = GlobalKey();
@override
Widget build(BuildContext context) {
final controller = Get.put(ControlPanelController());
final webSocketService = Get.find<WebSocketService>();
return Scaffold(
body: Row(
children: [
// 左侧边栏 - 占15%宽度
_buildSidebar(controller, context),
// 右侧主区域 - 占85%宽度
Expanded(
child: _buildMainArea(controller, webSocketService),
),
],
),
);
}
Widget _buildSidebar(
ControlPanelController controller, BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width * 0.15,
color: Colors.grey[900],
child: Column(
children: [
// 用户选择区域
_buildUserSelection(controller, context),
// 模糊查询输入框
_buildSearchField(controller),
// 信号强度过滤
_buildRssiFilter(controller),
// 设备统计
_buildDeviceCount(controller),
const SizedBox(height: 8),
// 设备列表 - 使用 Expanded 让它占据剩余空间
Expanded(
child: _buildDeviceList(controller),
),
],
),
);
}
Widget _buildUserSelection(
ControlPanelController controller, BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'当前用户',
style: TextStyle(color: Colors.white, fontSize: 12),
),
const SizedBox(height: 8),
Obx(() {
if (controller.users.isEmpty) {
return Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'暂无在线设备',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
);
}
// 使用 InkWell 实现可点击的选择器
return InkWell(
key: _dropdownButtonKey,
onTap: () => _showCustomUserMenu(controller, context),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[700]!),
),
child: Row(
children: [
Expanded(
child: Text(
controller.selectedUser.value != null
? '${controller.selectedUser.value!.deviceModel} (${_getShortUuid(controller.selectedUser.value!.uuid)})'
: '选择设备',
style: TextStyle(
color: controller.selectedUser.value != null
? Colors.white
: Colors.grey,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.arrow_drop_down,
color: Colors.grey, size: 20),
],
),
),
);
}),
// 添加连接状态提示
Obx(() => Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
Icon(
controller.webSocketService.isConnected.value
? Icons.wifi
: Icons.wifi_off,
size: 12,
color: controller.webSocketService.isConnected.value
? Colors.green
: Colors.red,
),
const SizedBox(width: 4),
Text(
controller.webSocketService.connectionStatus.value,
style: TextStyle(
color: controller.webSocketService.isConnected.value
? Colors.green
: Colors.red,
fontSize: 10,
),
),
],
),
)),
],
),
);
}
// 构建 PopupMenu 的菜单项
List<PopupMenuEntry<UserInfo>> _buildPopupMenuItems(
ControlPanelController controller, BuildContext context) {
// 使用 StatefulBuilder 来实现搜索功能
final List<PopupMenuEntry<UserInfo>> entries = [];
// 添加搜索框
entries.add(
PopupMenuItem<UserInfo>(
enabled: false,
padding: EdgeInsets.zero,
child: Container(
width: 280,
padding: const EdgeInsets.all(8),
child: TextField(
autofocus: true,
style: const TextStyle(color: Colors.white, fontSize: 12),
decoration: InputDecoration(
hintText: '搜索设备...',
hintStyle: const TextStyle(color: Colors.grey, fontSize: 12),
prefixIcon:
const Icon(Icons.search, size: 18, color: Colors.grey),
filled: true,
fillColor: Colors.grey[800],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide.none,
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
),
onChanged: (value) {
// 这里需要通过 StatefulBuilder 来更新
// 由于 PopupMenuButton 不支持动态更新,我们使用另一种方式
},
),
),
),
);
// 添加分隔线
entries.add(const PopupMenuDivider());
// 添加设备列表
final users = controller.users;
for (var user in users) {
final isSelected = controller.selectedUser.value?.uuid == user.uuid;
entries.add(
PopupMenuItem<UserInfo>(
value: user,
padding: EdgeInsets.zero,
child: Container(
width: 280,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.grey[800] : null,
),
child: Row(
children: [
Icon(
Icons.devices,
size: 16,
color: isSelected ? Colors.green : Colors.grey,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.deviceModel,
style: TextStyle(
color: isSelected ? Colors.green : Colors.grey[500],
fontSize: 12,
),
),
Text(
// _getShortUuid(user.uuid),
user.uuid,
style: TextStyle(
color: Colors.grey[500],
fontSize: 10,
),
),
],
),
),
if (isSelected)
const Icon(Icons.check_circle, size: 16, color: Colors.green),
],
),
),
),
);
}
return entries;
}
String _getShortUuid(String uuid) {
if (uuid.length >= 8) {
return '${uuid.substring(0, 8)}...';
}
return uuid;
}
Widget _buildSearchField(ControlPanelController controller) {
return Padding(
padding: const EdgeInsets.all(16),
child: TextField(
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: '模糊查询...',
hintStyle: const TextStyle(color: Colors.grey),
prefixIcon: const Icon(Icons.search, color: Colors.grey),
filled: true,
fillColor: Colors.grey[800],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
onChanged: (value) => controller.searchKeyword.value = value,
),
);
}
// 信号强度过滤组件
Widget _buildRssiFilter(ControlPanelController controller) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'信号强度过滤',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
const SizedBox(height: 16),
LayoutBuilder(
builder: (context, constraints) {
return Obx(() {
final double minValue = -100;
final double maxValue = 65;
final double valueRange = maxValue - minValue;
final double percent =
(controller.rssiFilterValue.value - minValue) / valueRange;
final double thumbPosition = percent * constraints.maxWidth;
return Column(
children: [
// 滑块区域
GestureDetector(
onHorizontalDragUpdate: (details) {
final newPercent = (thumbPosition + details.delta.dx) /
constraints.maxWidth;
final newPercentClamped = newPercent.clamp(0.0, 1.0);
final newValue =
minValue + (newPercentClamped * valueRange);
controller.updateRssiFilter(newValue.round());
},
onTapDown: (details) {
final localX = details.localPosition.dx;
final newPercent =
(localX / constraints.maxWidth).clamp(0.0, 1.0);
final newValue = minValue + (newPercent * valueRange);
controller.updateRssiFilter(newValue.round());
},
child: Container(
height: 40,
child: Stack(
alignment: Alignment.centerLeft,
children: [
// 背景线(灰色)
Container(
height: 3,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(1.5),
color: Colors.grey[600],
),
),
// 左侧绿色线(从起点到圆点位置)
Container(
width: thumbPosition,
height: 3,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(1.5),
color: Colors.green,
),
),
// 白色实心圆
Positioned(
left: thumbPosition - 12,
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
border: Border.all(
color: Colors.green,
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
),
),
],
),
),
),
const SizedBox(height: 16),
// 强度值显示和范围标签
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'-100 dBm',
style: TextStyle(color: Colors.grey, fontSize: 10),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.green,
width: 1,
),
),
child: Text(
'${controller.rssiFilterValue.value} dBm',
style: const TextStyle(
color: Colors.green,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const Text(
'65 dBm',
style: TextStyle(color: Colors.grey, fontSize: 10),
),
],
),
],
);
});
},
),
],
),
);
}
Color _getRssiColor(int rssi) {
if (rssi >= -50) return Colors.green;
if (rssi >= -70) return Colors.lightGreen;
if (rssi >= -80) return Colors.yellow.shade700;
if (rssi >= -90) return Colors.orange;
return Colors.red;
}
Widget _buildDeviceCount(ControlPanelController controller) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: Alignment.centerLeft,
child: Obx(() => Text(
'匹配出 ${controller.filteredDevices.length} 个设备',
style: const TextStyle(color: Colors.grey, fontSize: 12),
)),
),
);
}
Widget _buildDeviceList(ControlPanelController controller) {
return Obx(() {
if (controller.selectedUser.value == null) {
return const Center(
child: Text(
'请先选择设备',
style: TextStyle(color: Colors.grey),
),
);
}
if (controller.filteredDevices.isEmpty) {
return const Center(
child: Text(
'暂无设备,请点击"扫描"按钮',
style: TextStyle(color: Colors.grey),
),
);
}
return ListView.builder(
itemCount: controller.filteredDevices.length,
itemBuilder: (context, index) {
final device = controller.filteredDevices[index];
return ListTile(
title: Text(
device.name.isEmpty ? '未知设备' : device.name,
style: const TextStyle(color: Colors.white, fontSize: 13),
),
subtitle: Text(
device.id,
style: const TextStyle(color: Colors.grey, fontSize: 10),
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _getRssiColor(device.rssi),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'${device.rssi} dBm',
style: const TextStyle(color: Colors.white, fontSize: 10),
),
),
onTap: () => controller.connectToDevice(device),
);
},
);
});
}
Widget _buildMainArea(
ControlPanelController controller, WebSocketService webSocketService) {
return Column(
children: [
_buildMenuBar(webSocketService),
_buildToolBar(controller),
_buildLogFilterBar(controller),
Expanded(
child: _buildLogArea(controller),
),
],
);
}
Widget _buildMenuBar(WebSocketService webSocketService) {
return Container(
height: 48,
color: Colors.grey[100],
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
const Text(
'指令和日志',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const Spacer(),
Obx(() => Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: webSocketService.isConnected.value
? Colors.green.withOpacity(0.2)
: Colors.red.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: webSocketService.isConnected.value
? Colors.green
: Colors.red,
),
),
const SizedBox(width: 8),
Text(
webSocketService.connectionStatus.value,
style: TextStyle(
color: webSocketService.isConnected.value
? Colors.green
: Colors.red,
fontSize: 12,
),
),
],
),
)),
],
),
);
}
Widget _buildToolBar(ControlPanelController controller) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
),
child: Row(
children: [
// 扫描按钮
ElevatedButton.icon(
onPressed: () {
if (controller.selectedUser.value == null) {
controller.addLog('请先选择设备', LogType.warning);
return;
}
controller.sendScanCommand();
},
icon: const Icon(Icons.bluetooth_searching, size: 18),
label: const Text('扫描'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
const SizedBox(width: 8),
// 停止扫描按钮
OutlinedButton.icon(
onPressed: () {
if (controller.selectedUser.value == null) {
controller.addLog('请先选择设备', LogType.warning);
return;
}
controller.sendStopScanCommand();
},
icon: const Icon(Icons.stop, size: 18),
label: const Text('停止扫描'),
),
const SizedBox(width: 8),
// 断开设备按钮
OutlinedButton.icon(
onPressed: () {
if (controller.selectedUser.value == null) {
controller.addLog('请先选择设备', LogType.warning);
return;
}
controller.disconnectDevice();
},
icon: const Icon(Icons.bluetooth_disabled, size: 18),
label: const Text('断开设备'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
),
),
const Spacer(),
// 命令输入框和执行按钮
SizedBox(
width: 300,
child: TextField(
controller: controller.commandController,
decoration: InputDecoration(
hintText: '输入命令或数据...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon: IconButton(
icon: const Icon(Icons.send),
onPressed: () {
if (controller.selectedUser.value == null) {
controller.addLog('请先选择设备', LogType.warning);
return;
}
controller.sendCustomData();
},
tooltip: '执行',
),
),
onSubmitted: (_) {
if (controller.selectedUser.value == null) {
controller.addLog('请先选择设备', LogType.warning);
return;
}
controller.sendCustomData();
},
),
),
const SizedBox(width: 16),
// 自动滚动开关
Row(
children: [
const Text('自动滚动'),
const SizedBox(width: 8),
Obx(() => Switch(
value: controller.autoScroll.value,
onChanged: controller.toggleAutoScroll,
)),
],
),
const SizedBox(width: 16),
// 清空日志按钮
ElevatedButton(
onPressed: controller.clearLogs,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('清空日志'),
),
],
),
);
}
Widget _buildLogFilterBar(ControlPanelController controller) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[100],
border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
),
child: Row(
children: [
const Text('日志过滤器:', style: TextStyle(fontSize: 12)),
const SizedBox(width: 16),
Obx(() => FilterChip(
label: const Text('全部'),
selected: controller.showAllLogs.value,
onSelected: controller.toggleShowAllLogs,
backgroundColor: Colors.white,
selectedColor: Colors.blue.shade100,
)),
const SizedBox(width: 8),
Obx(() => FilterChip(
label: const Text('连接日志'),
selected: !controller.showAllLogs.value &&
controller.showConnectionLogs.value,
onSelected: (selected) {
if (!controller.showAllLogs.value) {
controller.toggleShowConnectionLogs(selected);
}
},
backgroundColor: Colors.white,
selectedColor: Colors.green.shade100,
)),
const SizedBox(width: 8),
Obx(() => FilterChip(
label: const Text('设备日志'),
selected: !controller.showAllLogs.value &&
controller.showDeviceLogs.value,
onSelected: (selected) {
if (!controller.showAllLogs.value) {
controller.toggleShowDeviceLogs(selected);
}
},
backgroundColor: Colors.white,
selectedColor: Colors.orange.shade100,
)),
],
),
);
}
Widget _buildLogArea(ControlPanelController controller) {
return Container(
color: Colors.grey[50],
child: Obx(() {
final logs = controller.displayLogs;
if (logs.isEmpty) {
return const Center(
child: Text('暂无日志'),
);
}
return ListView.builder(
controller: controller.logScrollController,
padding: const EdgeInsets.all(16),
itemCount: logs.length,
itemBuilder: (context, index) {
final log = logs[index];
return _buildLogItem(log);
},
);
}),
);
}
Widget _buildLogItem(LogEntry log) {
Color textColor;
String prefix;
IconData? icon;
switch (log.type) {
case LogType.success:
textColor = Colors.green;
prefix = '[SUCCESS]';
icon = Icons.check_circle_outline;
break;
case LogType.error:
textColor = Colors.red;
prefix = '[ERROR]';
icon = Icons.error_outline;
break;
case LogType.warning:
textColor = Colors.orange;
prefix = '[WARNING]';
icon = Icons.warning_amber_outlined;
break;
case LogType.device:
textColor = Colors.purple;
prefix = '[DEVICE]';
icon = Icons.devices;
break;
case LogType.connection:
textColor = Colors.blue;
prefix = '[CONN]';
icon = Icons.wifi;
break;
default:
textColor = Colors.black87;
prefix = '[INFO]';
icon = Icons.info_outline;
}
final timeStr =
'${log.time.hour.toString().padLeft(2, '0')}:${log.time.minute.toString().padLeft(2, '0')}:${log.time.second.toString().padLeft(2, '0')}';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (icon != null) ...[
Icon(icon, size: 14, color: textColor),
const SizedBox(width: 8),
],
SizedBox(
width: 80,
child: Text(
timeStr,
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
fontFamily: 'monospace',
),
),
),
const SizedBox(width: 12),
SizedBox(
width: 80,
child: Text(
prefix,
style: TextStyle(
color: textColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
log.message,
style: TextStyle(
color: textColor,
fontSize: 12,
fontFamily: 'monospace',
),
),
),
],
),
);
}
void _showCustomUserMenu(
ControlPanelController controller, BuildContext context) {
// 获取按钮的位置和大小
final RenderBox renderBox =
_dropdownButtonKey.currentContext!.findRenderObject() as RenderBox;
final Offset offset = renderBox.localToGlobal(Offset.zero);
final Size buttonSize = renderBox.size;
// 搜索控制器
final searchController = TextEditingController();
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: "Dismiss",
barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (context, animation, secondaryAnimation) {
// 使用 StatefulBuilder 来管理弹窗内部状态
return StatefulBuilder(
builder: (dialogContext, setDialogState) {
// 初始化过滤用户列表
List<UserInfo> filteredUsers = controller.users.toList();
void updateFilter(String value) {
setDialogState(() {
if (value.isEmpty) {
filteredUsers = controller.users.toList();
} else {
filteredUsers = controller.users
.where((user) =>
user.deviceModel
.toLowerCase()
.contains(value.toLowerCase()) ||
user.uuid.toLowerCase().contains(value.toLowerCase()))
.toList();
}
});
}
return Stack(
children: [
// 点击背景关闭
Positioned.fill(
child: GestureDetector(
onTap: () => Navigator.of(dialogContext).pop(),
child: Container(
color: Colors.transparent,
),
),
),
// 菜单内容 - 宽度与按钮相同
Positioned(
left: offset.dx,
top: offset.dy + buttonSize.height + 2,
width: buttonSize.width,
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(8),
color: Colors.grey[850],
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 搜索框
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey[700]!,
width: 1,
),
),
),
child: TextField(
controller: searchController,
autofocus: true,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
decoration: InputDecoration(
hintText: '搜索设备名称或UUID...',
hintStyle: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
prefixIcon: const Icon(
Icons.search,
size: 18,
color: Colors.grey,
),
filled: true,
fillColor: Colors.grey[800],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 8),
suffixIcon: searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 16),
color: Colors.grey,
onPressed: () {
searchController.clear();
updateFilter('');
},
)
: null,
),
onChanged: (value) {
updateFilter(value);
},
),
),
// 用户列表
Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 300,
minHeight: 100,
),
child: Builder(
builder: (context) {
// 重新获取过滤后的列表
List<UserInfo> currentFilteredUsers;
if (searchController.text.isEmpty) {
currentFilteredUsers =
controller.users.toList();
} else {
currentFilteredUsers = controller.users
.where((user) =>
user.deviceModel
.toLowerCase()
.contains(searchController.text
.toLowerCase()) ||
user.uuid.toLowerCase().contains(
searchController.text
.toLowerCase()))
.toList();
}
if (currentFilteredUsers.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'未找到匹配的设备',
style: TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
),
);
}
return ListView.builder(
shrinkWrap: true,
itemCount: currentFilteredUsers.length,
itemBuilder: (context, index) {
final user = currentFilteredUsers[index];
final isSelected =
controller.selectedUser.value?.uuid ==
user.uuid;
return InkWell(
onTap: () {
controller.selectUser(user);
Navigator.of(dialogContext).pop();
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: isSelected
? Colors.grey[800]
: Colors.transparent,
border: isSelected
? null
: Border(
bottom: BorderSide(
color: Colors.grey[800]!,
width: 0.5,
),
),
),
child: Row(
children: [
Icon(
Icons.devices,
size: 16,
color: isSelected
? Colors.green
: Colors.grey[500],
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
user.deviceModel,
style: TextStyle(
color: isSelected
? Colors.green
: Colors.white,
fontSize: 12,
),
),
Text(
user.uuid,
style: TextStyle(
color: Colors.grey[500],
fontSize: 10,
),
maxLines: 1,
overflow:
TextOverflow.ellipsis,
),
],
),
),
if (isSelected)
const Icon(
Icons.check_circle,
size: 16,
color: Colors.green,
),
],
),
),
);
},
);
},
),
),
),
],
),
),
),
],
);
},
);
},
);
}
}