progress_bar.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. part of 'control_board.dart';
  2. class _AIKeyFrame {
  3. final double index;
  4. final String name;
  5. _AIKeyFrame({
  6. required this.index,
  7. required this.name,
  8. });
  9. }
  10. class _ProgressBar extends StatefulWidget {
  11. @override
  12. State<StatefulWidget> createState() => _ProgressBarState();
  13. }
  14. class _ProgressBarState extends State<_ProgressBar> {
  15. final playerController = Get.find<IPlayerController>() as VidPlayerController;
  16. /// 测量AI数据
  17. final measureData = Get.find<MeasureDataController>();
  18. /// ai结果
  19. late final List<AIDiagnosisPerImageDTO> aiResult = [];
  20. late List<_AIKeyFrame> aiKeyFrame = [];
  21. late DiagnosisOrganEnum diagnosisOrgan = DiagnosisOrganEnum.Null;
  22. late String diagnosisOrganName = '';
  23. late final aiPatintController = Get.find<AiPatintController>();
  24. double curCursorIndex = -10;
  25. static const marginSpace = 10.0;
  26. /// 获取关键帧
  27. void getKeyFrame() {
  28. bool canShowAI = [
  29. DiagnosisConclusionEnum.Benign,
  30. DiagnosisConclusionEnum.Malignant,
  31. DiagnosisConclusionEnum.BenignAndMalignant
  32. ].contains(measureData.diagnosisConclusion);
  33. if (canShowAI) {
  34. final measureDataAIResults = jsonDecode(
  35. measureData.aiResults,
  36. );
  37. aiResult.clear();
  38. for (int i = 0; i < (measureDataAIResults as List).length; i++) {
  39. aiResult.add(
  40. AIDiagnosisPerImageDTO.fromJson(
  41. measureDataAIResults[i],
  42. ),
  43. );
  44. }
  45. for (int j = 0; j < aiResult.length; j++) {
  46. List<AIDiagnosisResultPerOrgan> diagResultsForEachOrgan =
  47. aiResult[j].diagResultsForEachOrgan ?? [];
  48. if (diagResultsForEachOrgan.isNotEmpty) {
  49. List<AIDetectedObject> detectedObjects =
  50. diagResultsForEachOrgan[0].detectedObjects ?? [];
  51. diagnosisOrgan = diagResultsForEachOrgan[0].organ;
  52. bool isLiver = diagnosisOrgan == DiagnosisOrganEnum.Liver;
  53. bool isThyroid = diagnosisOrgan == DiagnosisOrganEnum.Thyroid;
  54. diagnosisOrganName = '';
  55. List<String> diagnosisOrganLabel = [];
  56. for (int m = 0; m < detectedObjects.length; m++) {
  57. if (detectedObjects[m].label != 0) {
  58. diagnosisOrganLabel.add(
  59. _buildAITitle(detectedObjects[m].label),
  60. );
  61. if (isLiver) {
  62. diagnosisOrganName = diagnosisOrganLabel.toSet().join(",");
  63. if (detectedObjects[m].label < 5) {
  64. aiKeyFrame.add(
  65. _AIKeyFrame(
  66. name: diagnosisOrganName,
  67. index: j.toDouble(),
  68. ),
  69. );
  70. }
  71. } else if (isThyroid) {
  72. diagnosisOrganName = diagnosisOrganLabel.toSet().join(",");
  73. if (detectedObjects[m].label < 7) {
  74. aiKeyFrame.add(
  75. _AIKeyFrame(
  76. name: diagnosisOrganName,
  77. index: j.toDouble(),
  78. ),
  79. );
  80. }
  81. } else {
  82. diagnosisOrganName = diagnosisOrganLabel.toSet().join(",");
  83. aiKeyFrame.add(
  84. _AIKeyFrame(
  85. name: diagnosisOrganName,
  86. index: j.toDouble(),
  87. ),
  88. );
  89. }
  90. }
  91. }
  92. }
  93. }
  94. }
  95. }
  96. void onMeasuredAIResultsInfoChanged(Object sender, String e) {
  97. aiKeyFrame = [];
  98. if (e.isNotEmpty && e != "[]") {
  99. getKeyFrame();
  100. } else {
  101. setState(() {
  102. aiResult.clear();
  103. });
  104. }
  105. }
  106. @override
  107. void initState() {
  108. WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
  109. if (mounted) {
  110. playerController.eventHandler.addListener(onControllerEvent);
  111. getKeyFrame();
  112. }
  113. });
  114. measureData.aiResultsInfoChanged
  115. .addListener(onMeasuredAIResultsInfoChanged);
  116. super.initState();
  117. }
  118. @override
  119. void dispose() {
  120. measureData.aiResultsInfoChanged
  121. .removeListener(onMeasuredAIResultsInfoChanged);
  122. super.dispose();
  123. }
  124. @override
  125. Widget build(BuildContext context) {
  126. final controller = playerController;
  127. var index = controller.currentFrameIndex.toDouble();
  128. var max = 100.0;
  129. if (index < 0) {
  130. index = 0;
  131. } else {
  132. /// 第一帧下标为0 ,所以max要减1
  133. max = controller.totalFramesCount.toDouble() - 1;
  134. }
  135. return SliderTheme(
  136. data: const SliderThemeData(
  137. trackHeight: 12,
  138. thumbColor: Colors.white,
  139. trackShape: _FullWidthTrackShape(),
  140. ),
  141. child: LayoutBuilder(builder: (
  142. BuildContext layoutBuilderContext,
  143. BoxConstraints constraints,
  144. ) {
  145. final BoxConstraints insideConstraints = constraints.copyWith(
  146. maxWidth: constraints.maxWidth - marginSpace,
  147. );
  148. return Stack(
  149. alignment: Alignment.center,
  150. children: [
  151. // const CrossFrameAnchorBar(),
  152. const StreamingProgressBarWithCrossFrame(),
  153. Container(
  154. padding: const EdgeInsets.only(
  155. left: marginSpace / 2,
  156. ),
  157. child: Stack(
  158. alignment: Alignment.center,
  159. children: [
  160. ...aiKeyFrame.map((i) {
  161. return Obx(() {
  162. if (aiPatintController.state.ifShowAi) {
  163. return _PanelPoint(
  164. max: max,
  165. index: index,
  166. constraints: insideConstraints,
  167. currentIndex: i.index,
  168. onChanged: () {
  169. controller.locateTo(i.index.toInt());
  170. },
  171. aiDiagnosticOrganName: i.name,
  172. curCursorIndex: curCursorIndex,
  173. );
  174. } else {
  175. return const SizedBox();
  176. }
  177. });
  178. }).toList(),
  179. _CursorTrack(
  180. constraints: insideConstraints,
  181. cursorUpdateCallback: (localPositionX) {
  182. setState(() {
  183. curCursorIndex =
  184. (localPositionX / insideConstraints.maxWidth * max)
  185. .roundToDouble();
  186. });
  187. },
  188. cursorExit: () {
  189. setState(() {
  190. curCursorIndex = -10;
  191. });
  192. },
  193. )
  194. ],
  195. ),
  196. ),
  197. ],
  198. );
  199. }),
  200. );
  201. }
  202. void onControllerEvent(Object sender, VidPlayerEvent e) {
  203. if (e is VidPlayerFrameIndexChangeEvent) {
  204. onPlayFrameIndexChanged(e);
  205. }
  206. }
  207. void onPlayFrameIndexChanged(VidPlayerFrameIndexChangeEvent e) {
  208. if (mounted) {
  209. setState(() {});
  210. }
  211. }
  212. String _buildAITitle(int label) {
  213. switch (diagnosisOrgan) {
  214. case DiagnosisOrganEnum.Breast:
  215. return _buildBreastDescription(label);
  216. case DiagnosisOrganEnum.Liver:
  217. return _buildLiverDescription(label);
  218. case DiagnosisOrganEnum.Thyroid:
  219. return _buildThyroidDescription(label);
  220. default:
  221. return '';
  222. }
  223. }
  224. String _buildBreastDescription(int label) {
  225. switch (label) {
  226. case 0:
  227. return i18nBook.measure.noSignificantAbnormalitiesWereSeen.t;
  228. case 1:
  229. return i18nBook.measure.lipoma.t;
  230. case 2:
  231. return 'BI-RADS 2';
  232. case 3:
  233. return 'BI-RADS 3';
  234. case 4:
  235. return 'BI-RADS 4a';
  236. case 5:
  237. return 'BI-RADS 4b';
  238. case 6:
  239. return 'BI-RADS 4c';
  240. case 7:
  241. return 'BI-RADS 5';
  242. case 8:
  243. return i18nBook.measure.noSignificantAbnormalitiesWereSeen.t;
  244. default:
  245. return '';
  246. }
  247. }
  248. String _buildLiverDescription(int label) {
  249. switch (label) {
  250. case 0:
  251. return i18nBook.measure.noSignificantAbnormalitiesWereSeen.t;
  252. case 1:
  253. return i18nBook.measure.intrahepaticStrongEchoFoci.t;
  254. case 2:
  255. return i18nBook.measure.hepaticHemangioma.t;
  256. case 3:
  257. return i18nBook.measure.liverCysts.t;
  258. case 4:
  259. return i18nBook.measure.liverCancerMayOccur.t;
  260. case 5:
  261. return i18nBook.measure.fattyLiver.t;
  262. case 6:
  263. return i18nBook.measure.panisodicChangesLiverDiffuseLesions.t;
  264. case 7:
  265. return i18nBook.measure.cirrhosis.t;
  266. case 8:
  267. return i18nBook.measure.polycysticLiver.t;
  268. default:
  269. return '';
  270. }
  271. }
  272. String _buildThyroidDescription(int label) {
  273. switch (label) {
  274. case 0:
  275. return i18nBook.measure.noSignificantAbnormalitiesWereSeen.t;
  276. case 1:
  277. return 'TIRADS2';
  278. case 2:
  279. return 'TIRADS3';
  280. case 3:
  281. return 'TIRADS4a';
  282. case 4:
  283. return 'TIRADS4b';
  284. case 5:
  285. return 'TIRADS4c';
  286. case 6:
  287. return 'TIRADS5';
  288. case 7:
  289. return i18nBook.measure.presenceDiffuseDisease.t;
  290. default:
  291. return '';
  292. }
  293. }
  294. Widget _buildDescription(
  295. String? title,
  296. ) {
  297. return Column(
  298. crossAxisAlignment: CrossAxisAlignment.start,
  299. children: [
  300. if (title != null) ...[
  301. const SizedBox(
  302. height: 5,
  303. ),
  304. Text(
  305. title,
  306. style: const TextStyle(color: Colors.white),
  307. ),
  308. ],
  309. ],
  310. );
  311. }
  312. // String _buildAIDiagnosticOrgans() {
  313. // switch (diagnosisOrgan) {
  314. // case DiagnosisOrganEnum.Breast:
  315. // return i18nBook.remedical.breast.t;
  316. // case DiagnosisOrganEnum.Abdomen:
  317. // return i18nBook.bodyParts.abdomen.t;
  318. // case DiagnosisOrganEnum.Liver:
  319. // return i18nBook.remedical.liver.t;
  320. // case DiagnosisOrganEnum.Cholecyst:
  321. // return i18nBook.remedical.cholecyst.t;
  322. // case DiagnosisOrganEnum.Kidney:
  323. // return i18nBook.remedical.kidney.t;
  324. // case DiagnosisOrganEnum.Spleen:
  325. // return i18nBook.remedical.spleen.t;
  326. // default:
  327. // return '';
  328. // }
  329. // }
  330. }
  331. // https://juejin.cn/post/6959703051586240549
  332. class _FullWidthTrackShape extends RoundedRectSliderTrackShape {
  333. const _FullWidthTrackShape();
  334. @override
  335. Rect getPreferredRect({
  336. required RenderBox parentBox,
  337. Offset offset = Offset.zero,
  338. required SliderThemeData sliderTheme,
  339. bool isEnabled = false,
  340. bool isDiscrete = false,
  341. }) {
  342. final double trackHeight = sliderTheme.trackHeight ?? 2;
  343. final double trackLeft = offset.dx;
  344. final double trackTop =
  345. offset.dy + (parentBox.size.height - trackHeight) / 2;
  346. // 让轨道宽度等于 Slider 宽度
  347. final double trackWidth = parentBox.size.width;
  348. return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
  349. }
  350. }
  351. class MeasureTooltip extends StatelessWidget {
  352. final String tooltips;
  353. final FWidget child;
  354. const MeasureTooltip({
  355. Key? key,
  356. required this.tooltips,
  357. required this.child,
  358. }) : super(key: key);
  359. @override
  360. Widget build(BuildContext context) {
  361. return FMaterialTooltip(
  362. message: tooltips,
  363. preferBelow: false,
  364. padding: const EdgeInsets.all(8),
  365. textStyle: const TextStyle(
  366. fontSize: 16,
  367. color: Colors.white,
  368. ),
  369. triggerMode: TooltipTriggerMode.tap, // 单击 显示Tooltip
  370. verticalOffset: 30,
  371. child: child,
  372. );
  373. }
  374. }
  375. /// AI节点
  376. class _PanelPoint extends StatefulWidget {
  377. const _PanelPoint({
  378. required this.max,
  379. required this.index,
  380. required this.constraints,
  381. required this.currentIndex,
  382. required this.onChanged,
  383. required this.curCursorIndex,
  384. required this.aiDiagnosticOrganName,
  385. });
  386. final double max;
  387. final double index;
  388. final BoxConstraints constraints;
  389. final double currentIndex;
  390. final VoidCallback onChanged;
  391. final double curCursorIndex;
  392. final String aiDiagnosticOrganName;
  393. @override
  394. State<_PanelPoint> createState() => _PanelPointState();
  395. }
  396. class _PanelPointState extends State<_PanelPoint> {
  397. FWidget marker({
  398. required double size,
  399. required Color color,
  400. }) {
  401. return FInkWell(
  402. onTap: widget.onChanged,
  403. child: FContainer(
  404. height: size,
  405. width: size,
  406. decoration: BoxDecoration(
  407. color: color,
  408. borderRadius: BorderRadius.circular(size / 2),
  409. ),
  410. ),
  411. );
  412. }
  413. @override
  414. Widget build(BuildContext context) {
  415. double size =
  416. max(8 - (widget.curCursorIndex - widget.currentIndex).abs(), 3);
  417. return Positioned(
  418. left: widget.currentIndex / widget.max * widget.constraints.maxWidth -
  419. (size / 2),
  420. top: widget.constraints.maxHeight / 2 + 15 + size,
  421. child: MeasureTooltip(
  422. tooltips: widget.aiDiagnosticOrganName,
  423. child: marker(
  424. size: size,
  425. color: widget.currentIndex == widget.index
  426. ? const Color.fromARGB(255, 255, 111, 45)
  427. : const Color.fromARGB(255, 252, 255, 45),
  428. ),
  429. ),
  430. );
  431. }
  432. }
  433. class _CursorTrack extends StatelessWidget {
  434. const _CursorTrack({
  435. required this.constraints,
  436. required this.cursorUpdateCallback,
  437. required this.cursorExit,
  438. });
  439. final BoxConstraints constraints;
  440. final ValueCallback<double> cursorUpdateCallback;
  441. final VoidCallback cursorExit;
  442. @override
  443. Widget build(BuildContext context) {
  444. return Positioned(
  445. top: constraints.maxHeight / 2 + 10,
  446. child: MouseRegion(
  447. onHover: (e) {
  448. cursorUpdateCallback(e.localPosition.dx);
  449. },
  450. onExit: (e) => {cursorExit()},
  451. opaque: false,
  452. child: SizedBox(
  453. width: constraints.maxWidth,
  454. height: 40,
  455. child: Container(),
  456. ),
  457. ),
  458. );
  459. }
  460. }
  461. class _CrossFrameAnchorProgress extends StatefulWidget {
  462. @override
  463. State<StatefulWidget> createState() => _CrossFrameAnchorProgressState();
  464. }
  465. class _CrossFrameAnchorProgressState extends State<_CrossFrameAnchorProgress> {
  466. final controller = Get.find<IPlayerController>() as VidPlayerController;
  467. @override
  468. void initState() {
  469. // TODO: implement initState
  470. super.initState();
  471. }
  472. @override
  473. Widget build(BuildContext context) {
  474. return SizedBox();
  475. }
  476. }