Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

SpeakerStats.tsx 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import React, { useCallback, useEffect } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { useDispatch, useSelector } from 'react-redux';
  4. import { makeStyles } from 'tss-react/mui';
  5. import { IReduxState } from '../../../app/types';
  6. import Icon from '../../../base/icons/components/Icon';
  7. import {
  8. IconEmotionsAngry,
  9. IconEmotionsDisgusted,
  10. IconEmotionsFearful,
  11. IconEmotionsHappy,
  12. IconEmotionsNeutral,
  13. IconEmotionsSad,
  14. IconEmotionsSurprised
  15. } from '../../../base/icons/svg';
  16. import Tooltip from '../../../base/tooltip/components/Tooltip';
  17. import Dialog from '../../../base/ui/components/web/Dialog';
  18. import { escapeRegexp } from '../../../base/util/helpers';
  19. import { initSearch, resetSearchCriteria, toggleFaceExpressions } from '../../actions.any';
  20. import {
  21. DISPLAY_SWITCH_BREAKPOINT,
  22. MOBILE_BREAKPOINT
  23. } from '../../constants';
  24. import FaceExpressionsSwitch from './FaceExpressionsSwitch';
  25. import SpeakerStatsLabels from './SpeakerStatsLabels';
  26. import SpeakerStatsList from './SpeakerStatsList';
  27. import SpeakerStatsSearch from './SpeakerStatsSearch';
  28. const useStyles = makeStyles()(theme => {
  29. return {
  30. speakerStats: {
  31. '& .header': {
  32. position: 'fixed',
  33. backgroundColor: theme.palette.ui01,
  34. paddingLeft: theme.spacing(4),
  35. paddingRight: theme.spacing(4),
  36. marginLeft: `-${theme.spacing(4)}`,
  37. '&.large': {
  38. width: '616px'
  39. },
  40. '&.medium': {
  41. width: '352px'
  42. },
  43. '@media (max-width: 448px)': {
  44. width: 'calc(100% - 48px) !important'
  45. },
  46. '& .upper-header': {
  47. display: 'flex',
  48. justifyContent: 'space-between',
  49. alignItems: 'center',
  50. width: '100%',
  51. '& .search-switch-container': {
  52. display: 'flex',
  53. width: '100%',
  54. '& .search-container': {
  55. width: 175,
  56. marginRight: theme.spacing(3)
  57. },
  58. '& .search-container-full-width': {
  59. width: '100%'
  60. }
  61. },
  62. '& .emotions-icons': {
  63. display: 'flex',
  64. '& svg': {
  65. fill: '#000'
  66. },
  67. '&>div': {
  68. marginRight: theme.spacing(3)
  69. },
  70. '&>div:last-child': {
  71. marginRight: 0
  72. }
  73. }
  74. }
  75. },
  76. '& .row': {
  77. display: 'flex',
  78. alignItems: 'center',
  79. '& .name-time': {
  80. width: 'calc(100% - 48px)',
  81. display: 'flex',
  82. justifyContent: 'space-between',
  83. alignItems: 'center',
  84. '&.expressions-on': {
  85. width: 'calc(47% - 48px)',
  86. marginRight: theme.spacing(4)
  87. }
  88. },
  89. '& .timeline-container': {
  90. height: '100%',
  91. width: `calc(53% - ${theme.spacing(4)})`,
  92. display: 'flex',
  93. alignItems: 'center',
  94. borderLeftWidth: 1,
  95. borderLeftColor: theme.palette.ui02,
  96. borderLeftStyle: 'solid',
  97. '& .timeline': {
  98. height: theme.spacing(2),
  99. display: 'flex',
  100. width: '100%',
  101. '&>div': {
  102. marginRight: theme.spacing(1),
  103. borderRadius: 5
  104. },
  105. '&>div:first-child': {
  106. borderRadius: '0 5px 5px 0'
  107. },
  108. '&>div:last-child': {
  109. marginRight: 0,
  110. borderRadius: '5px 0 0 5px'
  111. }
  112. }
  113. },
  114. '& .axis-container': {
  115. height: '100%',
  116. width: `calc(53% - ${theme.spacing(6)})`,
  117. display: 'flex',
  118. alignItems: 'center',
  119. marginLeft: theme.spacing(3),
  120. '& div': {
  121. borderRadius: 5
  122. },
  123. '& .axis': {
  124. height: theme.spacing(1),
  125. display: 'flex',
  126. width: '100%',
  127. backgroundColor: theme.palette.ui03,
  128. position: 'relative',
  129. '& .left-bound': {
  130. position: 'absolute',
  131. bottom: 10,
  132. left: 0
  133. },
  134. '& .right-bound': {
  135. position: 'absolute',
  136. bottom: 10,
  137. right: 0
  138. },
  139. '& .handler': {
  140. position: 'absolute',
  141. backgroundColor: theme.palette.ui09,
  142. height: 12,
  143. marginTop: -4,
  144. display: 'flex',
  145. justifyContent: 'space-between',
  146. '& .resize': {
  147. height: '100%',
  148. width: 5,
  149. cursor: 'col-resize'
  150. }
  151. }
  152. }
  153. }
  154. },
  155. '& .separator': {
  156. width: 'calc(100% + 48px)',
  157. height: 1,
  158. marginLeft: -24,
  159. backgroundColor: theme.palette.ui02
  160. }
  161. }
  162. };
  163. });
  164. const EMOTIONS_LEGEND = [
  165. {
  166. translationKey: 'speakerStats.neutral',
  167. icon: IconEmotionsNeutral
  168. },
  169. {
  170. translationKey: 'speakerStats.happy',
  171. icon: IconEmotionsHappy
  172. },
  173. {
  174. translationKey: 'speakerStats.surprised',
  175. icon: IconEmotionsSurprised
  176. },
  177. {
  178. translationKey: 'speakerStats.sad',
  179. icon: IconEmotionsSad
  180. },
  181. {
  182. translationKey: 'speakerStats.fearful',
  183. icon: IconEmotionsFearful
  184. },
  185. {
  186. translationKey: 'speakerStats.angry',
  187. icon: IconEmotionsAngry
  188. },
  189. {
  190. translationKey: 'speakerStats.disgusted',
  191. icon: IconEmotionsDisgusted
  192. }
  193. ];
  194. const SpeakerStats = () => {
  195. const { faceLandmarks } = useSelector((state: IReduxState) => state['features/base/config']);
  196. const { showFaceExpressions } = useSelector((state: IReduxState) => state['features/speaker-stats']);
  197. const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
  198. const displaySwitch = faceLandmarks?.enableDisplayFaceExpressions && clientWidth > DISPLAY_SWITCH_BREAKPOINT;
  199. const displayLabels = clientWidth > MOBILE_BREAKPOINT;
  200. const dispatch = useDispatch();
  201. const { classes } = useStyles();
  202. const { t } = useTranslation();
  203. const onToggleFaceExpressions = useCallback(() =>
  204. dispatch(toggleFaceExpressions())
  205. , [ dispatch ]);
  206. const onSearch = useCallback((criteria = '') => {
  207. dispatch(initSearch(escapeRegexp(criteria)));
  208. }
  209. , [ dispatch ]);
  210. useEffect(() => {
  211. showFaceExpressions && !displaySwitch && dispatch(toggleFaceExpressions());
  212. }, [ clientWidth ]);
  213. useEffect(() => () => {
  214. dispatch(resetSearchCriteria());
  215. }, []);
  216. return (
  217. <Dialog
  218. cancel = {{ hidden: true }}
  219. ok = {{ hidden: true }}
  220. size = { showFaceExpressions ? 'large' : 'medium' }
  221. titleKey = 'speakerStats.speakerStats'>
  222. <div className = { classes.speakerStats }>
  223. <div className = { `header ${showFaceExpressions ? 'large' : 'medium'}` }>
  224. <div className = 'upper-header'>
  225. <div
  226. className = {
  227. `search-switch-container
  228. ${showFaceExpressions ? 'expressions-on' : ''}`
  229. }>
  230. <div
  231. className = {
  232. displaySwitch
  233. ? 'search-container'
  234. : 'search-container-full-width' }>
  235. <SpeakerStatsSearch
  236. onSearch = { onSearch } />
  237. </div>
  238. { displaySwitch
  239. && <FaceExpressionsSwitch
  240. onChange = { onToggleFaceExpressions }
  241. showFaceExpressions = { showFaceExpressions } />
  242. }
  243. </div>
  244. { showFaceExpressions && <div className = 'emotions-icons'>
  245. {
  246. EMOTIONS_LEGEND.map(emotion => (
  247. <Tooltip
  248. content = { t(emotion.translationKey) }
  249. key = { emotion.translationKey }
  250. position = { 'top' }>
  251. <Icon
  252. size = { 20 }
  253. src = { emotion.icon } />
  254. </Tooltip>
  255. ))
  256. }
  257. </div>}
  258. </div>
  259. { displayLabels && (
  260. <SpeakerStatsLabels
  261. showFaceExpressions = { showFaceExpressions ?? false } />
  262. )}
  263. </div>
  264. <SpeakerStatsList />
  265. </div>
  266. </Dialog>
  267. );
  268. };
  269. export default SpeakerStats;