You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SpeakerStats.tsx 10KB

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