Design system.
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 

261 líneas
6.8 KiB

  1. import * as React from 'react';
  2. import {getFormValues} from '@theoryofnekomata/formxtra';
  3. export interface UseMediaControlsOptions<T extends HTMLMediaElement> {
  4. controllerRef: React.Ref<T>;
  5. actionFormKey?: string;
  6. visualizationMode?: string;
  7. }
  8. export const useMediaControls = <T extends HTMLMediaElement>({
  9. controllerRef: forwardedRef,
  10. actionFormKey = 'action' as const,
  11. }: UseMediaControlsOptions<T>) => {
  12. const defaultRef = React.useRef<T>(null);
  13. const ref = forwardedRef ?? defaultRef;
  14. const seekRef = React.useRef<HTMLInputElement>(null);
  15. const volumeRef = React.useRef<HTMLInputElement>(null);
  16. const filenameRef = React.useRef<HTMLElement>(null);
  17. const visualizationId = React.useId();
  18. const formId = React.useId();
  19. const [isPlaying, setIsPlaying] = React.useState(false);
  20. const [isSeeking, setIsSeeking] = React.useState(false);
  21. const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState<number>();
  22. const [seekTimeDisplay, setSeekTimeDisplay] = React.useState<number>();
  23. const [durationDisplay, setDurationDisplay] = React.useState<number>();
  24. const [isSeekTimeCountingDown, setIsSeekTimeCountingDown] = React.useState(false);
  25. const refreshControls: React.ReactEventHandler<T> = React.useCallback((e) => {
  26. const { currentTarget: mediaController } = e;
  27. setCurrentTimeDisplay(mediaController.currentTime);
  28. setDurationDisplay(mediaController.duration);
  29. }, []);
  30. const togglePlayback = React.useCallback(() => {
  31. setIsPlaying((p) => !p);
  32. }, []);
  33. const startSeek: React.MouseEventHandler<HTMLInputElement> = React.useCallback(() => {
  34. setIsSeeking(true);
  35. }, []);
  36. const doSetSeek = React.useCallback((thisElement: HTMLInputElement, mediaController: HTMLMediaElement) => {
  37. mediaController.currentTime = thisElement.valueAsNumber;
  38. }, []);
  39. const setSeek: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => {
  40. if (!(typeof ref === 'object' && ref)) {
  41. return;
  42. }
  43. const { current: mediaController } = ref;
  44. if (!mediaController) {
  45. return;
  46. }
  47. const { currentTarget: thisElement } = e;
  48. setSeekTimeDisplay(thisElement.valueAsNumber);
  49. if (isSeeking) {
  50. return;
  51. }
  52. doSetSeek(thisElement, mediaController);
  53. }, [ref, isSeeking, doSetSeek]);
  54. const endSeek: React.MouseEventHandler<HTMLInputElement> = React.useCallback((e) => {
  55. if (!(typeof ref === 'object' && ref)) {
  56. return;
  57. }
  58. const { current: mediaController } = ref;
  59. if (!mediaController) {
  60. return;
  61. }
  62. const { currentTarget: thisElement } = e;
  63. setIsSeeking(false);
  64. setCurrentTimeDisplay(thisElement.valueAsNumber);
  65. doSetSeek(thisElement, mediaController);
  66. }, [ref, doSetSeek]);
  67. const reset: React.ReactEventHandler<T> = React.useCallback((e) => {
  68. const videoElement = e.currentTarget;
  69. setIsPlaying(false);
  70. videoElement.currentTime = 0;
  71. }, []);
  72. const updateSeekFromPlayback: React.ReactEventHandler<T> = React.useCallback((e) => {
  73. if (isSeeking) {
  74. return;
  75. }
  76. const videoElement = e.currentTarget;
  77. const currentTime = videoElement.currentTime;
  78. setCurrentTimeDisplay(currentTime);
  79. if (!(typeof seekRef === 'object' && seekRef)) {
  80. return;
  81. }
  82. const { current: seek } = seekRef;
  83. if (!seek) {
  84. return;
  85. }
  86. seek.value = String(currentTime);
  87. }, [isSeeking, seekRef]);
  88. const toggleSeekTimeCountMode = React.useCallback(() => {
  89. setIsSeekTimeCountingDown((b) => !b);
  90. }, []);
  91. const download = React.useCallback(() => {
  92. if (!(typeof ref === 'object' && ref)) {
  93. return;
  94. }
  95. const { current: mediaController } = ref;
  96. if (!mediaController) {
  97. return;
  98. }
  99. if (!(typeof filenameRef === 'object' && filenameRef)) {
  100. return;
  101. }
  102. const { current: filename } = filenameRef;
  103. if (!filename) {
  104. return;
  105. }
  106. const downloadLink = window.document.createElement('a');
  107. downloadLink.download = filename.textContent ?? 'media';
  108. downloadLink.href = mediaController.currentSrc;
  109. downloadLink.addEventListener('click', () => {
  110. downloadLink.remove();
  111. });
  112. downloadLink.click();
  113. }, [ref, filenameRef]);
  114. const actions = React.useMemo(() => ({
  115. togglePlayback,
  116. toggleSeekTimeCountMode,
  117. download,
  118. }), [togglePlayback, toggleSeekTimeCountMode, download]);
  119. const handleAction: React.FormEventHandler<HTMLFormElement> = React.useCallback((e) => {
  120. e.preventDefault();
  121. const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement };
  122. const formData = getFormValues(
  123. e.currentTarget,
  124. {
  125. submitter: nativeEvent.submitter,
  126. }
  127. );
  128. const actionName = formData[actionFormKey] as keyof typeof actions;
  129. const { [actionName]: actionFunction } = actions;
  130. actionFunction?.();
  131. }, [actions, actionFormKey]);
  132. const adjustVolume: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => {
  133. if (!(typeof ref === 'object' && ref)) {
  134. return;
  135. }
  136. const { current: mediaController } = ref;
  137. if (!mediaController) {
  138. return;
  139. }
  140. const { value } = e.currentTarget;
  141. mediaController.volume = Number(value);
  142. }, [ref]);
  143. React.useEffect(() => {
  144. if (!(typeof ref === 'object' && ref)) {
  145. return;
  146. }
  147. const { current: mediaController } = ref;
  148. if (!mediaController) {
  149. return;
  150. }
  151. if (isPlaying) {
  152. void mediaController.play();
  153. return
  154. }
  155. mediaController.pause();
  156. }, [isPlaying, ref]);
  157. React.useEffect(() => {
  158. if (!(typeof seekRef === 'object' && seekRef)) {
  159. return;
  160. }
  161. const { current: seek } = seekRef;
  162. if (!seek) {
  163. return;
  164. }
  165. seek.value = String(currentTimeDisplay);
  166. }, [currentTimeDisplay, seekRef]);
  167. React.useEffect(() => {
  168. if (!(typeof ref === 'object' && ref)) {
  169. return;
  170. }
  171. const { current: mediaController } = ref;
  172. if (!mediaController) {
  173. return;
  174. }
  175. if (!(typeof volumeRef === 'object' && volumeRef)) {
  176. return;
  177. }
  178. const { current: volume } = volumeRef;
  179. if (!volume) {
  180. return;
  181. }
  182. volume.value = String(mediaController.volume);
  183. }, [ref, volumeRef]);
  184. return React.useMemo(() => ({
  185. seekRef,
  186. volumeRef,
  187. isPlaying,
  188. refreshControls,
  189. adjustVolume,
  190. reset,
  191. startSeek,
  192. endSeek,
  193. setSeek,
  194. updateSeekFromPlayback,
  195. durationDisplay,
  196. currentTimeDisplay,
  197. seekTimeDisplay,
  198. isSeeking,
  199. isSeekTimeCountingDown,
  200. mediaControllerRef: ref,
  201. handleAction,
  202. filenameRef,
  203. visualizationId,
  204. formId,
  205. }), [
  206. refreshControls,
  207. isPlaying,
  208. isSeeking,
  209. adjustVolume,
  210. reset,
  211. startSeek,
  212. endSeek,
  213. setSeek,
  214. updateSeekFromPlayback,
  215. durationDisplay,
  216. currentTimeDisplay,
  217. seekTimeDisplay,
  218. isSeekTimeCountingDown,
  219. ref,
  220. handleAction,
  221. filenameRef,
  222. visualizationId,
  223. formId,
  224. ]);
  225. };