progress_bar.dart 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. part of 'control_board.dart';
  2. class _ProgressBar extends StatefulWidget {
  3. @override
  4. State<StatefulWidget> createState() => _ProgressBarState();
  5. }
  6. class _ProgressBarState extends State<_ProgressBar> {
  7. final playerController = Get.find<IPlayerController>() as VidPlayerController;
  8. /// 测量AI数据
  9. final measureData = Get.find<MeasureDataController>();
  10. /// ai结果
  11. late final List<AIDiagnosisPerImageDTO> aiResult = [];
  12. late List<double> aiKeyFrame = [];
  13. late DiagnosisOrganEnum diagnosisOrgan = DiagnosisOrganEnum.Null;
  14. double curCursorIndex = -10;
  15. /// 获取关键帧
  16. void getKeyFrame() {
  17. bool canShowAI = [
  18. DiagnosisConclusionEnum.Benign,
  19. DiagnosisConclusionEnum.Malignant,
  20. DiagnosisConclusionEnum.BenignAndMalignant
  21. ].contains(measureData.diagnosisConclusion);
  22. if (canShowAI) {
  23. final measureDataAIResults = jsonDecode(
  24. measureData.aiResults,
  25. );
  26. aiResult.clear();
  27. for (int i = 0; i < (measureDataAIResults as List).length; i++) {
  28. aiResult.add(
  29. AIDiagnosisPerImageDTO.fromJson(
  30. measureDataAIResults[i],
  31. ),
  32. );
  33. }
  34. for (int j = 0; j < aiResult.length; j++) {
  35. List<AIDiagnosisResultPerOrgan> diagResultsForEachOrgan =
  36. aiResult[j].diagResultsForEachOrgan ?? [];
  37. if (diagResultsForEachOrgan.isNotEmpty) {
  38. List<AIDetectedObject> detectedObjects =
  39. diagResultsForEachOrgan[0].detectedObjects ?? [];
  40. diagnosisOrgan = diagResultsForEachOrgan[0].organ;
  41. for (int m = 0; m < detectedObjects.length; m++) {
  42. if (detectedObjects[m].label != 0) {
  43. aiKeyFrame.add(j.toDouble());
  44. }
  45. }
  46. }
  47. }
  48. }
  49. }
  50. void onMeasuredAIResultsInfoChanged(Object sender, String e) {
  51. aiKeyFrame = [];
  52. if (e.isNotEmpty && e != "[]") {
  53. getKeyFrame();
  54. } else {
  55. setState(() {
  56. aiResult.clear();
  57. });
  58. }
  59. }
  60. @override
  61. void initState() {
  62. WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
  63. if (mounted) {
  64. playerController.eventHandler.addListener(onControllerEvent);
  65. getKeyFrame();
  66. }
  67. });
  68. measureData.aiResultsInfoChanged
  69. .addListener(onMeasuredAIResultsInfoChanged);
  70. super.initState();
  71. }
  72. @override
  73. void dispose() {
  74. measureData.aiResultsInfoChanged
  75. .removeListener(onMeasuredAIResultsInfoChanged);
  76. super.dispose();
  77. }
  78. @override
  79. Widget build(BuildContext context) {
  80. final controller = playerController;
  81. var index = controller.currentFrameIndex.toDouble();
  82. var max = 100.0;
  83. if (index < 0) {
  84. index = 0;
  85. } else {
  86. max = controller.totalFramesCount.toDouble();
  87. }
  88. return SliderTheme(
  89. data: const SliderThemeData(
  90. trackHeight: 12,
  91. thumbColor: Colors.white,
  92. trackShape: _FullWidthTrackShape(),
  93. ),
  94. child: LayoutBuilder(builder:
  95. (BuildContext layoutBuilderContext, BoxConstraints constraints) {
  96. return Stack(
  97. alignment: Alignment.center,
  98. children: [
  99. Slider(
  100. max: max,
  101. value: index,
  102. onChanged: (v) {
  103. controller.pause();
  104. controller.gotoFrame(v.toInt());
  105. },
  106. ),
  107. ...aiKeyFrame.map((i) {
  108. return _PanelPoint(
  109. max: max,
  110. index: index,
  111. constraints: constraints,
  112. currentIndex: i,
  113. onChanged: () {
  114. controller.pause();
  115. controller.gotoFrame(i.toInt());
  116. },
  117. aiDiagnosticOrganName: _buildAIDiagnosticOrgans(),
  118. curCursorIndex: curCursorIndex,
  119. );
  120. }).toList(),
  121. _CursorTrack(
  122. constraints: constraints,
  123. cursorUpdateCallback: (localPositionX) {
  124. setState(() {
  125. curCursorIndex = (localPositionX / constraints.maxWidth * max)
  126. .roundToDouble();
  127. });
  128. },
  129. cursorExit: () {
  130. setState(() {
  131. curCursorIndex = -10;
  132. });
  133. },
  134. )
  135. ],
  136. );
  137. }),
  138. );
  139. }
  140. void onControllerEvent(Object sender, VidPlayerEvent e) {
  141. if (e is VidPlayerFrameIndexChangeEvent) {
  142. onPlayFrameIndexChanged(e);
  143. }
  144. }
  145. void onPlayFrameIndexChanged(VidPlayerFrameIndexChangeEvent e) {
  146. if (mounted) {
  147. setState(() {});
  148. }
  149. }
  150. String _buildAIDiagnosticOrgans() {
  151. switch (diagnosisOrgan) {
  152. case DiagnosisOrganEnum.Breast:
  153. return i18nBook.remedical.breast.t;
  154. case DiagnosisOrganEnum.Abdomen:
  155. return i18nBook.bodyParts.abdomen.t;
  156. case DiagnosisOrganEnum.Liver:
  157. return i18nBook.remedical.liver.t;
  158. case DiagnosisOrganEnum.Cholecyst:
  159. return i18nBook.remedical.cholecyst.t;
  160. case DiagnosisOrganEnum.Kidney:
  161. return i18nBook.remedical.kidney.t;
  162. case DiagnosisOrganEnum.Spleen:
  163. return i18nBook.remedical.spleen.t;
  164. default:
  165. return '';
  166. }
  167. }
  168. }
  169. // https://juejin.cn/post/6959703051586240549
  170. class _FullWidthTrackShape extends RoundedRectSliderTrackShape {
  171. const _FullWidthTrackShape();
  172. @override
  173. Rect getPreferredRect({
  174. required RenderBox parentBox,
  175. Offset offset = Offset.zero,
  176. required SliderThemeData sliderTheme,
  177. bool isEnabled = false,
  178. bool isDiscrete = false,
  179. }) {
  180. final double trackHeight = sliderTheme.trackHeight ?? 2;
  181. final double trackLeft = offset.dx;
  182. final double trackTop =
  183. offset.dy + (parentBox.size.height - trackHeight) / 2;
  184. // 让轨道宽度等于 Slider 宽度
  185. final double trackWidth = parentBox.size.width;
  186. return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
  187. }
  188. }
  189. class MeasureTooltip extends StatelessWidget {
  190. final String tooltips;
  191. final FWidget child;
  192. const MeasureTooltip({
  193. Key? key,
  194. required this.tooltips,
  195. required this.child,
  196. }) : super(key: key);
  197. @override
  198. Widget build(BuildContext context) {
  199. return FMaterialTooltip(
  200. message: tooltips,
  201. preferBelow: false,
  202. padding: const EdgeInsets.all(8),
  203. textStyle: const TextStyle(
  204. fontSize: 16,
  205. color: Colors.white,
  206. ),
  207. triggerMode: TooltipTriggerMode.tap, // 单击 显示Tooltip
  208. verticalOffset: 30,
  209. child: child,
  210. );
  211. }
  212. }
  213. /// AI节点
  214. class _PanelPoint extends StatefulWidget {
  215. const _PanelPoint({
  216. required this.max,
  217. required this.index,
  218. required this.constraints,
  219. required this.currentIndex,
  220. required this.onChanged,
  221. required this.curCursorIndex,
  222. required this.aiDiagnosticOrganName,
  223. });
  224. final double max;
  225. final double index;
  226. final BoxConstraints constraints;
  227. final double currentIndex;
  228. final VoidCallback onChanged;
  229. final double curCursorIndex;
  230. final String aiDiagnosticOrganName;
  231. @override
  232. State<_PanelPoint> createState() => _PanelPointState();
  233. }
  234. class _PanelPointState extends State<_PanelPoint> {
  235. FWidget marker({
  236. required double size,
  237. required Color color,
  238. }) {
  239. return FInkWell(
  240. onTap: widget.onChanged,
  241. child: FContainer(
  242. height: size,
  243. width: size,
  244. decoration: BoxDecoration(
  245. color: color,
  246. borderRadius: BorderRadius.circular(size / 2),
  247. ),
  248. ),
  249. );
  250. }
  251. @override
  252. Widget build(BuildContext context) {
  253. double size =
  254. max(8 - (widget.curCursorIndex - widget.currentIndex).abs(), 3);
  255. return Positioned(
  256. left: widget.currentIndex / widget.max * widget.constraints.maxWidth -
  257. (size / 2),
  258. top: widget.constraints.maxHeight / 2 + 15 + size,
  259. child: MeasureTooltip(
  260. tooltips: widget.aiDiagnosticOrganName,
  261. child: marker(
  262. size: size,
  263. color: widget.currentIndex == widget.index
  264. ? const Color.fromARGB(255, 255, 111, 45)
  265. : const Color.fromARGB(255, 252, 255, 45),
  266. ),
  267. ),
  268. );
  269. }
  270. }
  271. class _CursorTrack extends StatelessWidget {
  272. const _CursorTrack({
  273. required this.constraints,
  274. required this.cursorUpdateCallback,
  275. required this.cursorExit,
  276. });
  277. final BoxConstraints constraints;
  278. final ValueCallback<double> cursorUpdateCallback;
  279. final VoidCallback cursorExit;
  280. @override
  281. Widget build(BuildContext context) {
  282. return Positioned(
  283. top: constraints.maxHeight / 2 + 10,
  284. child: MouseRegion(
  285. onHover: (e) {
  286. cursorUpdateCallback(e.localPosition.dx);
  287. },
  288. onExit: (e) => {cursorExit()},
  289. opaque: false,
  290. child: SizedBox(
  291. width: constraints.maxWidth,
  292. height: 40,
  293. child: Container(),
  294. )),
  295. );
  296. }
  297. }