diff --git a/src/App.tsx b/src/App.tsx index 16b39b6..f824941 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,140 +1,16 @@ import { useEffect } from 'react' -import { useAtom, useAtomValue, useSetAtom } from 'jotai' -import { useResetAtom } from 'jotai/utils' -import usePlayer from './hooks/usePlayer' +import PlayerControl from './components/PlayerControl' -import { - playerSourceAtom, - playerVolumeAtom, - playerMutedAtom, -} from './atoms/player' -import { - audioMomentsAtom, - generateRandomAudioMomentsAtom, - removeActualAudioMomentAtom, - audioMomentShouldUnpauseAtom, - audioMomentShouldPlayAtom, -} from './atoms/audioMoments' -import { - timerIsRunningAtom, - startTimerAtom, - pauseTimerAtom, - resetTimerAtom, - timerCanResetAtom, - timeTickingEffect, -} from './atoms/timer' +import { useTranslation } from 'react-i18next' import cn from './lib/cn' -import { MediaPlayer, MediaProvider } from '@vidstack/react' -import '@vidstack/react/player/styles/base.css' - -import { - AudioOrVideoSourceInput, - Button, - StartOrPauseTimerButton, - Timer, - VolumeControl, -} from './components' - -import { useTranslation } from 'react-i18next' - import { FaGithub } from 'react-icons/fa' export default function App() { - const { - player, - playerPaused, - playerCurrentTime, - playerDuration, - playerCanPlay, - resumePlayer, - pausePlayer, - resetPlayerCurrentTime, - resetPlayer, - } = usePlayer() - - const [playerSource, setPlayerSource] = useAtom(playerSourceAtom) - const playerVolume = useAtomValue(playerVolumeAtom) - const playerMuted = useAtomValue(playerMutedAtom) - - const audioMoments = useAtomValue(audioMomentsAtom) - const generateRandomAudioMoments = useSetAtom(generateRandomAudioMomentsAtom) - const removeActualAudioMoment = useSetAtom(removeActualAudioMomentAtom) - const resetAudioMoments = useResetAtom(audioMomentsAtom) - const [audioMomentShouldUnpause, setAudioMomentShouldUnpause] = useAtom( - audioMomentShouldUnpauseAtom, - ) - const audioMomentShouldPlay = useAtomValue(audioMomentShouldPlayAtom) - - const timerIsRunning = useAtomValue(timerIsRunningAtom) - const startTimer = useSetAtom(startTimerAtom) - const pauseTimer = useSetAtom(pauseTimerAtom) - const resetTimer = useSetAtom(resetTimerAtom) - const timerCanReset = useAtomValue(timerCanResetAtom) - useAtom(timeTickingEffect) - const { t, i18n } = useTranslation('', { keyPrefix: 'app' }) - function handleAudioOrVideoSourceInputChange(input: string | File): void { - resetTimer() - if (playerSource !== '') { - resetAudioMoments() - pausePlayer() - resetPlayerCurrentTime() - } - - setPlayerSource(input) - } - - function handleStartTimer(): void { - startTimer() - if (playerPaused && audioMomentShouldUnpause) { - resumePlayer() - setAudioMomentShouldUnpause(false) - } - } - - function handlePauseTimer(): void { - pauseTimer() - if (!playerPaused) { - pausePlayer() - setAudioMomentShouldUnpause(true) - } - } - - function handleStartOrPauseTimerButtonClick(): void { - if (!audioMoments) generateRandomAudioMoments(playerDuration) - - if (timerIsRunning) { - handlePauseTimer() - return - } - - handleStartTimer() - } - - function handleResetTimerButtonClick(): void { - resetTimer() - - if (playerCurrentTime > 0) { - resetPlayer() - resetAudioMoments() - } - } - - useEffect(() => { - function handleAudioMoments() { - if (audioMomentShouldPlay) { - resumePlayer() - removeActualAudioMoment() - } - } - - handleAudioMoments() - }, [audioMomentShouldPlay, resumePlayer, removeActualAudioMoment]) - useEffect(() => { document.title = t('pageTitle') document.documentElement.lang = i18n.language @@ -155,39 +31,7 @@ export default function App() { > {t('title')} -
- - - -
- - -
-
-
- - - -
+ | undefined>(undefined) const playerSourceAtom = atom('') const playerMutedAtom = atom(false) const playerVolumeAtom = atom( @@ -19,4 +24,4 @@ const playerVolumeAtom = atom( }, ) -export { playerSourceAtom, playerMutedAtom, playerVolumeAtom } +export { playerAtom, playerSourceAtom, playerMutedAtom, playerVolumeAtom } diff --git a/src/atoms/player/remote.ts b/src/atoms/player/remote.ts new file mode 100644 index 0000000..a382ef2 --- /dev/null +++ b/src/atoms/player/remote.ts @@ -0,0 +1,34 @@ +import { atom } from 'jotai' + +import { playerAtom } from '.' + +import { type MediaRemoteControl, useMediaRemote } from '@vidstack/react' + +const remoteAtom = atom((get) => + useMediaRemote(get(playerAtom)), +) + +const resumePlayerAtom = atom(null, (get) => { + const remote = get(remoteAtom) + + remote.play() +}) + +const pausePlayerAtom = atom(null, (get) => { + const remote = get(remoteAtom) + + remote.pause() +}) + +const resetPlayerCurrentTimeAtom = atom(null, (get) => { + const remote = get(remoteAtom) + + remote.seek(0) +}) + +export { + remoteAtom, + resumePlayerAtom, + pausePlayerAtom, + resetPlayerCurrentTimeAtom, +} diff --git a/src/atoms/player/store.ts b/src/atoms/player/store.ts new file mode 100644 index 0000000..779cd16 --- /dev/null +++ b/src/atoms/player/store.ts @@ -0,0 +1,20 @@ +import { atom } from 'jotai' + +import { playerAtom } from '.' + +import { useMediaStore, type MediaState } from '@vidstack/react' + +const storeAtom = atom>((get) => + useMediaStore(get(playerAtom)), +) +const playerPausedAtom = atom((get) => get(storeAtom).paused) +const playerDurationAtom = atom((get) => get(storeAtom).duration) +const playerCanPlayAtom = atom((get) => get(storeAtom).canPlay) +const playerCurrentTimeAtom = atom((get) => get(storeAtom).currentTime) + +export { + playerPausedAtom, + playerDurationAtom, + playerCanPlayAtom, + playerCurrentTimeAtom, +} diff --git a/src/atoms/timer/formattedTimeAtom.ts b/src/atoms/timer/formattedTimeAtom.ts new file mode 100644 index 0000000..38bd848 --- /dev/null +++ b/src/atoms/timer/formattedTimeAtom.ts @@ -0,0 +1,40 @@ +import { atom } from 'jotai' + +import { timerTotalSecondsAtom } from '.' + +import { ONE_HOUR_IN_SECONDS } from '../../constants' + +const ONE_MINUTE_IN_SECONDS = 60 + +function getHoursDigit(totalSeconds: number): number { + return Math.floor(totalSeconds / ONE_HOUR_IN_SECONDS) +} + +function getMinutesDigit(totalSeconds: number): number { + return Math.floor( + (totalSeconds % ONE_HOUR_IN_SECONDS) / ONE_MINUTE_IN_SECONDS, + ) +} + +function getSecondsDigit(totalSeconds: number): number { + return totalSeconds % ONE_MINUTE_IN_SECONDS +} + +const TIMER_FORMAT_LENGTH = 2 +const TIMER_FORMAT_PADDING = '0' + +function formatDigit(digit: number): string { + return digit.toString().padStart(TIMER_FORMAT_LENGTH, TIMER_FORMAT_PADDING) +} + +const formattedTimeAtom = atom((get) => { + const timerTotalSeconds = get(timerTotalSecondsAtom) + + const hoursDigit = getHoursDigit(timerTotalSeconds) + const minutesDigit = getMinutesDigit(timerTotalSeconds) + const secondsDigit = getSecondsDigit(timerTotalSeconds) + + return [hoursDigit, minutesDigit, secondsDigit].map(formatDigit).join(':') +}) + +export default formattedTimeAtom diff --git a/src/atoms/timer.ts b/src/atoms/timer/index.ts similarity index 71% rename from src/atoms/timer.ts rename to src/atoms/timer/index.ts index 7306d33..0b8d408 100644 --- a/src/atoms/timer.ts +++ b/src/atoms/timer/index.ts @@ -2,9 +2,9 @@ import { atom } from 'jotai' import { atomWithReset, RESET } from 'jotai/utils' import { atomEffect } from 'jotai-effect' -import { ONE_HOUR_IN_SECONDS } from '../constants' +import formattedTimeAtom from './formattedTimeAtom' -const ONE_MINUTE_IN_SECONDS = 60 +import { ONE_HOUR_IN_SECONDS } from '../../constants' const timerTotalSecondsAtom = atomWithReset(ONE_HOUR_IN_SECONDS) const decreaseTimerTotalSecondsAtom = atom(null, (_get, set) => { @@ -13,17 +13,6 @@ const decreaseTimerTotalSecondsAtom = atom(null, (_get, set) => { (previousTimerTotalSeconds) => previousTimerTotalSeconds - 1, ) }) -const timerHoursAtom = atom((get) => - Math.floor(get(timerTotalSecondsAtom) / ONE_HOUR_IN_SECONDS), -) -const timerMinutesAtom = atom((get) => - Math.floor( - (get(timerTotalSecondsAtom) % ONE_HOUR_IN_SECONDS) / ONE_MINUTE_IN_SECONDS, - ), -) -const timerSecondsAtom = atom( - (get) => get(timerTotalSecondsAtom) % ONE_MINUTE_IN_SECONDS, -) const timerIsRunningAtom = atom(false) @@ -55,9 +44,7 @@ const timeTickingEffect = atomEffect((get, set) => { export { timerTotalSecondsAtom, - timerHoursAtom, - timerMinutesAtom, - timerSecondsAtom, + formattedTimeAtom, timerIsRunningAtom, startTimerAtom, pauseTimerAtom, diff --git a/src/components/AudioOrVideoSourceInput/FileInputButton.tsx b/src/components/AudioOrVideoSourceInput/FileInputButton.tsx deleted file mode 100644 index 7ce380e..0000000 --- a/src/components/AudioOrVideoSourceInput/FileInputButton.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useRef } from 'react' -import { atom, useAtom } from 'jotai' - -import Button from '../Button' - -import { useTranslation } from 'react-i18next' - -const fileAtom = atom(undefined) - -interface FileInputButtonProps { - onChange?: (file: File) => void -} - -export default function FileInputButton({ onChange }: FileInputButtonProps) { - const inputRef = useRef(null) - const [file, setFile] = useAtom(fileAtom) - const { t } = useTranslation('', { keyPrefix: 'fileInputButton' }) - - function isNewFileEqualToPrevious(newFile: File | undefined): boolean { - if (!newFile || !file) return false - - return ( - newFile.name === file.name && - newFile.size === file.size && - newFile.lastModified === file.lastModified - ) - } - - function handleInputChange(event: React.ChangeEvent) { - const newFile = event.target.files?.[0] - if (!newFile || isNewFileEqualToPrevious(newFile)) return - - setFile(newFile) - onChange?.(newFile) - } - - function handleButtonClick() { - inputRef.current?.click() - } - - return ( -
- - -
- ) -} diff --git a/src/components/AudioOrVideoSourceInput/index.tsx b/src/components/AudioOrVideoSourceInput/index.tsx deleted file mode 100644 index 81257d1..0000000 --- a/src/components/AudioOrVideoSourceInput/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import FileInputButton from './FileInputButton' -import YoutubeVideoUrlInput from './YoutubeVideoUrlInput' - -import { useTranslation } from 'react-i18next' - -interface AudioOrVideoInputProps { - onChange?: (input: string | File) => void -} - -export default function AudioOrVideoSourceInput({ - onChange, -}: AudioOrVideoInputProps) { - const { t } = useTranslation('', { keyPrefix: 'audioOrVideoSourceInput' }) - - return ( -
-
- - - {t('audioOrVideoSourceInputSpan')} - -
- -
- ) -} diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 5dde83a..6c807d6 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -6,7 +6,7 @@ interface ButtonProps { children?: ReactNode className?: string disabled?: boolean - onClick?: () => void + onClick?: VoidFunction } export default function Button({ diff --git a/src/components/PlayerControl/HiddenMediaPlayer.tsx b/src/components/PlayerControl/HiddenMediaPlayer.tsx new file mode 100644 index 0000000..1dea6f7 --- /dev/null +++ b/src/components/PlayerControl/HiddenMediaPlayer.tsx @@ -0,0 +1,64 @@ +import { useAtom, useAtomValue, useSetAtom } from 'jotai' + +import { + playerAtom, + playerSourceAtom, + playerVolumeAtom, + playerMutedAtom, +} from '../../atoms/player' +import { + pausePlayerAtom, + resetPlayerCurrentTimeAtom, +} from '../../atoms/player/remote' +import { + playerCurrentTimeAtom, + playerPausedAtom, +} from '../../atoms/player/store' + +import { MediaPlayer, MediaProvider } from '@vidstack/react' +import '@vidstack/react/player/styles/base.css' + +export default function HiddenMediaPlayer() { + const player = useAtomValue(playerAtom) + const [playerSource, setPlayerSource] = useAtom(playerSourceAtom) + const playerVolume = useAtomValue(playerVolumeAtom) + const playerMuted = useAtomValue(playerMutedAtom) + + const pausePlayer = useSetAtom(pausePlayerAtom) + const resetPlayerCurrentTime = useSetAtom(resetPlayerCurrentTimeAtom) + + const playerPaused = useAtomValue(playerPausedAtom) + const playerCurrentTime = useAtomValue(playerCurrentTimeAtom) + + async function resetPlayer(): Promise { + const playerReset = playerPaused && playerCurrentTime === 0 + + if (!playerReset) { + pausePlayer() + resetPlayerCurrentTime() + + if (playerSource instanceof File) { + await new Promise((resolve) => { + setPlayerSource('') + resolve(true) + }) + + setPlayerSource(playerSource) + } + } + } + + return ( +
+ + + +
+ ) +} diff --git a/src/components/PlayerControl/PlayerSourceSelector/FileInputButton.tsx b/src/components/PlayerControl/PlayerSourceSelector/FileInputButton.tsx new file mode 100644 index 0000000..76dd1b0 --- /dev/null +++ b/src/components/PlayerControl/PlayerSourceSelector/FileInputButton.tsx @@ -0,0 +1,52 @@ +import { useRef, useState } from 'react' + +import { useSetAtom } from 'jotai' + +import handlePlayerSourceChangeAtom from './handlePlayerSourceChange' + +import Button from '../../Button' + +import { useTranslation } from 'react-i18next' + +export default function FileInputButton() { + const inputRef = useRef(null) + + const [file, setFile] = useState(undefined) + const handlePlayerSourceChange = useSetAtom(handlePlayerSourceChangeAtom) + + const { t } = useTranslation('', { keyPrefix: 'fileInputButton' }) + + function isNewFileEqualToPrevious(newFile: File): boolean { + return ( + newFile.name === file!.name && + newFile.size === file!.size && + newFile.lastModified === file!.lastModified + ) + } + + function handleInputChange(event: React.ChangeEvent) { + const newFile = event.target.files?.item(0) + + if (!newFile || (file && isNewFileEqualToPrevious(newFile))) return + + setFile(newFile) + handlePlayerSourceChange(newFile) + } + + function triggerFileInput() { + inputRef.current?.click() + } + + return ( +
+ + +
+ ) +} diff --git a/src/components/AudioOrVideoSourceInput/YoutubeVideoUrlInput.tsx b/src/components/PlayerControl/PlayerSourceSelector/YoutubeVideoUrlInput.tsx similarity index 52% rename from src/components/AudioOrVideoSourceInput/YoutubeVideoUrlInput.tsx rename to src/components/PlayerControl/PlayerSourceSelector/YoutubeVideoUrlInput.tsx index 34e479a..65d7cac 100644 --- a/src/components/AudioOrVideoSourceInput/YoutubeVideoUrlInput.tsx +++ b/src/components/PlayerControl/PlayerSourceSelector/YoutubeVideoUrlInput.tsx @@ -1,34 +1,38 @@ -import { atom, useAtom } from 'jotai' +import { useState } from 'react' -import { useTranslation } from 'react-i18next' +import { useSetAtom } from 'jotai' + +import handlePlayerSourceChangeAtom from './handlePlayerSourceChange' -import cn from '../../lib/cn' +import { useTranslation } from 'react-i18next' -import extractYoutubeVideoId from '../../utils/extractYoutubeVideoId' +import cn from '../../../lib/cn' -const sourceAtom = atom('') +function extractYoutubeVideoIdByUrl(url: string): string | null { + const regex: RegExp = + /(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/ ]{11})/ + const match: RegExpMatchArray | null = url.match(regex) -interface YoutubeVideoUrlInputProps { - onChange?: (youtubeVideoUrl: string) => void + return match ? match[1] : null } -export default function YoutubeVideoUrlInput({ - onChange, -}: YoutubeVideoUrlInputProps) { - const [source, setSource] = useAtom(sourceAtom) +export default function YoutubeVideoUrlInput() { + const [source, setSource] = useState('') + const handlePlayerSourceChange = useSetAtom(handlePlayerSourceChangeAtom) const { t } = useTranslation('', { keyPrefix: 'youtubeVideoUrlInput' }) function handleInputChange(event: React.ChangeEvent) { const url = event.target.value - const id = extractYoutubeVideoId(url) + const id = extractYoutubeVideoIdByUrl(url) + if (!id) return const newSource = `youtube/${id}` if (newSource === source) return setSource(newSource) - onChange?.(newSource) + handlePlayerSourceChange(newSource) } return ( diff --git a/src/components/PlayerControl/PlayerSourceSelector/handlePlayerSourceChange.ts b/src/components/PlayerControl/PlayerSourceSelector/handlePlayerSourceChange.ts new file mode 100644 index 0000000..24a0016 --- /dev/null +++ b/src/components/PlayerControl/PlayerSourceSelector/handlePlayerSourceChange.ts @@ -0,0 +1,26 @@ +import { atom } from 'jotai' +import { RESET } from 'jotai/utils' + +import { resetTimerAtom } from '../../../atoms/timer' +import { playerSourceAtom } from '../../../atoms/player' +import { audioMomentsAtom } from '../../../atoms/audioMoments' +import { + pausePlayerAtom, + resetPlayerCurrentTimeAtom, +} from '../../../atoms/player/remote' + +const handlePlayerSourceChangeAtom = atom( + null, + (get, set, input: string | File) => { + set(resetTimerAtom) + if (get(playerSourceAtom) !== '') { + set(audioMomentsAtom, RESET) + set(pausePlayerAtom) + set(resetPlayerCurrentTimeAtom) + } + + set(playerSourceAtom, input) + }, +) + +export default handlePlayerSourceChangeAtom diff --git a/src/components/PlayerControl/PlayerSourceSelector/index.tsx b/src/components/PlayerControl/PlayerSourceSelector/index.tsx new file mode 100644 index 0000000..239e10b --- /dev/null +++ b/src/components/PlayerControl/PlayerSourceSelector/index.tsx @@ -0,0 +1,20 @@ +import FileInputButton from './FileInputButton' +import YoutubeVideoUrlInput from './YoutubeVideoUrlInput' + +import { useTranslation } from 'react-i18next' + +export default function PlayerSourceSelector() { + const { t } = useTranslation('', { keyPrefix: 'playerSourceSelector' }) + + return ( +
+
+ + + {t('playerSourceSelectorSpan')} + +
+ +
+ ) +} diff --git a/src/components/PlayerControl/Timer.tsx b/src/components/PlayerControl/Timer.tsx new file mode 100644 index 0000000..ee1f7a7 --- /dev/null +++ b/src/components/PlayerControl/Timer.tsx @@ -0,0 +1,21 @@ +import { useAtomValue } from 'jotai' + +import { formattedTimeAtom } from '../../atoms/timer' + +import cn from '../../lib/cn' + +export default function Timer({ className }: { className?: string }) { + const formattedTime = useAtomValue(formattedTimeAtom) + + return ( + + ) +} diff --git a/src/components/PlayerControl/TimerControlButtons/ResetTimerButton.tsx b/src/components/PlayerControl/TimerControlButtons/ResetTimerButton.tsx new file mode 100644 index 0000000..307b295 --- /dev/null +++ b/src/components/PlayerControl/TimerControlButtons/ResetTimerButton.tsx @@ -0,0 +1,78 @@ +import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import { useResetAtom } from 'jotai/utils' + +import { playerSourceAtom } from '../../../atoms/player' +import { + pausePlayerAtom, + resetPlayerCurrentTimeAtom, +} from '../../../atoms/player/remote' +import { resetTimerAtom, timerCanResetAtom } from '../../../atoms/timer' +import { + playerCurrentTimeAtom, + playerPausedAtom, +} from '../../../atoms/player/store' +import { audioMomentsAtom } from '../../../atoms/audioMoments' + +import Button from '../../Button' + +import { useTranslation } from 'react-i18next' + +export default function ResetTimerButton() { + const [playerSource, setPlayerSource] = useAtom(playerSourceAtom) + + const playerPaused = useAtomValue(playerPausedAtom) + const playerCurrentTime = useAtomValue(playerCurrentTimeAtom) + + const pausePlayer = useSetAtom(pausePlayerAtom) + const resetPlayerCurrentTime = useSetAtom(resetPlayerCurrentTimeAtom) + + const timerCanReset = useAtomValue(timerCanResetAtom) + const resetTimer = useSetAtom(resetTimerAtom) + + const resetAudioMoments = useResetAtom(audioMomentsAtom) + + const { t } = useTranslation('', { keyPrefix: 'app' }) + + function playerIsNotReset(): boolean { + return !(playerPaused && playerCurrentTime === 0) + } + + function playerSourceIsAFile(): boolean { + return playerSource instanceof File + } + + async function resetPlayer(): Promise { + if (playerIsNotReset()) { + pausePlayer() + resetPlayerCurrentTime() + + if (playerSourceIsAFile()) { + await new Promise((resolve) => { + setPlayerSource('') + resolve(true) + }) + + setPlayerSource(playerSource) + } + } + } + + function handleClick(): void { + resetTimer() + + if (playerCurrentTime > 0) { + resetPlayer() + resetAudioMoments() + } + } + + return ( + + ) +} diff --git a/src/components/PlayerControl/TimerControlButtons/StartOrPauseTimerButton.tsx b/src/components/PlayerControl/TimerControlButtons/StartOrPauseTimerButton.tsx new file mode 100644 index 0000000..eb31602 --- /dev/null +++ b/src/components/PlayerControl/TimerControlButtons/StartOrPauseTimerButton.tsx @@ -0,0 +1,112 @@ +import { useAtom, useAtomValue, useSetAtom } from 'jotai' + +import { + playerCanPlayAtom, + playerDurationAtom, + playerPausedAtom, +} from '../../../atoms/player/store' +import { pausePlayerAtom, resumePlayerAtom } from '../../../atoms/player/remote' +import { + timerIsRunningAtom, + timerCanResetAtom, + startTimerAtom, + pauseTimerAtom, +} from '../../../atoms/timer' +import { + audioMomentsAtom, + audioMomentShouldUnpauseAtom, + generateRandomAudioMomentsAtom, +} from '../../../atoms/audioMoments' + +import { useTranslation } from 'react-i18next' + +import Button from '../../Button' + +import cn from '../../../lib/cn' + +export default function StartOrPauseTimerButton() { + const playerPaused = useAtomValue(playerPausedAtom) + const playerDuration = useAtomValue(playerDurationAtom) + const playerCanPlay = useAtomValue(playerCanPlayAtom) + + const resumePlayer = useSetAtom(resumePlayerAtom) + const pausePlayer = useSetAtom(pausePlayerAtom) + + const startTimer = useSetAtom(startTimerAtom) + const pauseTimer = useSetAtom(pauseTimerAtom) + const timerIsRunning = useAtomValue(timerIsRunningAtom) + const timerCanReset = useAtomValue(timerCanResetAtom) + + const [audioMomentShouldUnpause, setAudioMomentShouldUnpause] = useAtom( + audioMomentShouldUnpauseAtom, + ) + const audioMoments = useAtomValue(audioMomentsAtom) + const generateRandomAudioMoments = useSetAtom(generateRandomAudioMomentsAtom) + + const { t, i18n } = useTranslation('', { + keyPrefix: 'startOrPauseTimerButton', + }) + + type LanguagesAbbreviations = 'en' | 'pt-BR' | 'pt' + + const width24 = 'w-24' + const width28 = 'w-28' + const width32 = 'w-32' + const width40 = 'w-40' + + const widthMapping: { [key in LanguagesAbbreviations]: string } = { + en: timerIsRunning ? width28 : timerCanReset ? width32 : width24, + 'pt-BR': timerIsRunning ? width32 : width40, + pt: timerIsRunning ? width32 : width40, + } + + function getWidthClass(): string | undefined { + return ( + widthMapping[i18n.language as LanguagesAbbreviations] || + (timerIsRunning ? width28 : timerCanReset ? width32 : width24) + ) + } + + function handleStartTimer(): void { + startTimer() + if (playerPaused && audioMomentShouldUnpause) { + resumePlayer() + setAudioMomentShouldUnpause(false) + } + } + + function handlePauseTimer(): void { + pauseTimer() + if (!playerPaused) { + pausePlayer() + setAudioMomentShouldUnpause(true) + } + } + + function handleClick(): void { + if (!audioMoments) generateRandomAudioMoments(playerDuration) + + if (timerIsRunning) { + handlePauseTimer() + } else { + handleStartTimer() + } + } + + return ( + + ) +} diff --git a/src/components/PlayerControl/TimerControlButtons/index.tsx b/src/components/PlayerControl/TimerControlButtons/index.tsx new file mode 100644 index 0000000..8017412 --- /dev/null +++ b/src/components/PlayerControl/TimerControlButtons/index.tsx @@ -0,0 +1,11 @@ +import StartOrPauseTimerButton from './StartOrPauseTimerButton' +import ResetTimerButton from './ResetTimerButton' + +export default function TimerControlButtons() { + return ( +
+ + +
+ ) +} diff --git a/src/components/VolumeControl.tsx b/src/components/PlayerControl/VolumeControl.tsx similarity index 73% rename from src/components/VolumeControl.tsx rename to src/components/PlayerControl/VolumeControl.tsx index 912413c..3985e15 100644 --- a/src/components/VolumeControl.tsx +++ b/src/components/PlayerControl/VolumeControl.tsx @@ -1,11 +1,20 @@ +import type { ChangeEvent } from 'react' + import { useAtom } from 'jotai' -import { playerMutedAtom, playerVolumeAtom } from '../atoms/player' + +import { playerMutedAtom, playerVolumeAtom } from '../../atoms/player' import { FaVolumeMute, FaVolumeUp } from 'react-icons/fa' export default function VolumeControl() { - const [playerVolume, changePlayerVolume] = useAtom(playerVolumeAtom) const [playerMuted, setPlayerMuted] = useAtom(playerMutedAtom) + const [playerVolume, changePlayerVolume] = useAtom(playerVolumeAtom) + + function handleInputChange(event: ChangeEvent) { + const valueToNumber = parseFloat(event.target.value) + + changePlayerVolume(valueToNumber) + } return (
@@ -25,7 +34,7 @@ export default function VolumeControl() { max='1' step='0.01' value={playerMuted ? 0 : playerVolume} - onChange={(e) => changePlayerVolume(parseFloat(e.target.value))} + onChange={handleInputChange} className='w-16 accent-blue-500' aria-labelledby='volume-control' /> diff --git a/src/components/PlayerControl/index.tsx b/src/components/PlayerControl/index.tsx new file mode 100644 index 0000000..bc62093 --- /dev/null +++ b/src/components/PlayerControl/index.tsx @@ -0,0 +1,54 @@ +import { useEffect, useRef } from 'react' + +import { useAtom, useAtomValue, useSetAtom } from 'jotai' + +import { playerAtom } from '../../atoms/player' +import { resumePlayerAtom } from '../../atoms/player/remote' +import { + removeActualAudioMomentAtom, + audioMomentShouldPlayAtom, +} from '../../atoms/audioMoments' +import { timeTickingEffect } from '../../atoms/timer' + +import type { MediaPlayerInstance } from '@vidstack/react' + +import Timer from './Timer' +import VolumeControl from './VolumeControl' +import PlayerSourceSelector from './PlayerSourceSelector' +import TimerControlButtons from './TimerControlButtons' +import HiddenMediaPlayer from './HiddenMediaPlayer' + +export default function PlayerControl() { + const setPlayer = useSetAtom(playerAtom) + setPlayer(useRef(null)) + + const audioMomentShouldPlay = useAtomValue(audioMomentShouldPlayAtom) + const removeActualAudioMoment = useSetAtom(removeActualAudioMomentAtom) + + const resumePlayer = useSetAtom(resumePlayerAtom) + + useAtom(timeTickingEffect) + + useEffect(() => { + function handleAudioMoments() { + if (audioMomentShouldPlay) { + resumePlayer() + removeActualAudioMoment() + } + } + + handleAudioMoments() + }, [audioMomentShouldPlay, resumePlayer, removeActualAudioMoment]) + + return ( + <> +
+ + + + +
+ + + ) +} diff --git a/src/components/StartOrPauseTimerButton.tsx b/src/components/StartOrPauseTimerButton.tsx deleted file mode 100644 index f2382b9..0000000 --- a/src/components/StartOrPauseTimerButton.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useAtomValue } from 'jotai' -import { timerIsRunningAtom, timerCanResetAtom } from '../atoms/timer' - -import { useTranslation } from 'react-i18next' - -import cn from '../lib/cn' - -import Button from './Button' - -interface StartOrPauseTimerButtonProps { - disabled: boolean - onClick: () => void -} - -export default function StartOrPauseTimerButton({ - disabled, - onClick, -}: StartOrPauseTimerButtonProps) { - const timerIsRunning = useAtomValue(timerIsRunningAtom) - const timerCanReset = useAtomValue(timerCanResetAtom) - - const { t, i18n } = useTranslation('', { - keyPrefix: 'startOrPauseTimerButton', - }) - - function startOrPauseTimerButtonText() { - if (timerIsRunning) return t('pauseTimerButtonText') - if (timerCanReset) return t('resumeTimerButtonText') - - return t('startTimerButtonText') - } - - type LanguagesAbbreviations = 'en' | 'pt-BR' | 'pt' - - const width24 = 'w-24' - const width28 = 'w-28' - const width32 = 'w-32' - const width40 = 'w-40' - - const widthMapping: { [key in LanguagesAbbreviations]: string } = { - en: timerIsRunning ? width28 : timerCanReset ? width32 : width24, - 'pt-BR': timerIsRunning ? width32 : width40, - pt: timerIsRunning ? width32 : width40, - } - - function getWidthClass(): string | undefined { - return ( - widthMapping[i18n.language as LanguagesAbbreviations] || - (timerIsRunning ? width28 : timerCanReset ? width32 : width24) - ) - } - - return ( - - ) -} diff --git a/src/components/Timer.tsx b/src/components/Timer.tsx deleted file mode 100644 index 8b32b9b..0000000 --- a/src/components/Timer.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useAtomValue } from 'jotai' -import { - timerHoursAtom, - timerMinutesAtom, - timerSecondsAtom, -} from '../atoms/timer' - -import cn from '../lib/cn' - -const TIMER_FORMAT_LENGTH = 2 -const TIMER_FORMAT_PADDING = '0' - -function formatDigit(digit: number): string { - return digit.toString().padStart(TIMER_FORMAT_LENGTH, TIMER_FORMAT_PADDING) -} - -interface TimerProps { - className?: string -} - -export default function Timer({ className }: TimerProps) { - const timerHours = useAtomValue(timerHoursAtom) - const timerMinutes = useAtomValue(timerMinutesAtom) - const timerSeconds = useAtomValue(timerSecondsAtom) - - function formatTime(): string { - const digits: number[] = [timerHours, timerMinutes, timerSeconds] - - return digits.map(formatDigit).join(':') - } - - return ( - - ) -} diff --git a/src/components/index.tsx b/src/components/index.tsx deleted file mode 100644 index 952b12b..0000000 --- a/src/components/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import AudioOrVideoSourceInput from './AudioOrVideoSourceInput' -import Button from './Button' -import StartOrPauseTimerButton from './StartOrPauseTimerButton' -import Timer from './Timer' -import VolumeControl from './VolumeControl' - -export { - AudioOrVideoSourceInput, - Button, - StartOrPauseTimerButton, - Timer, - VolumeControl, -} diff --git a/src/hooks/usePlayer.ts b/src/hooks/usePlayer.ts deleted file mode 100644 index 415bef3..0000000 --- a/src/hooks/usePlayer.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useRef } from 'react' -import { useAtom } from 'jotai' - -import { playerSourceAtom } from '../atoms/player' - -import { - useMediaRemote, - useMediaStore, - type MediaPlayerInstance, -} from '@vidstack/react' - -export default function usePlayer() { - const player = useRef(null) - const remote = useMediaRemote(player) - const { paused, currentTime, duration, canPlay } = useMediaStore(player) - - const [playerSource, setPlayerSource] = useAtom(playerSourceAtom) - - const pausePlayer = () => remote.pause() - const resetPlayerCurrentTime = () => remote.seek(0) - - async function resetPlayer(): Promise { - const playerReset = paused && currentTime === 0 - if (!playerReset) { - pausePlayer() - resetPlayerCurrentTime() - if (playerSource instanceof File) { - await new Promise((resolve) => { - setPlayerSource('') - resolve(true) - }) - setPlayerSource(playerSource) - } - } - } - - return { - player, - playerPaused: paused, - playerCurrentTime: currentTime, - playerDuration: duration, - playerCanPlay: canPlay, - resumePlayer: () => remote.play(), - pausePlayer, - resetPlayerCurrentTime, - resetPlayer, - } -} diff --git a/src/locales/en.json b/src/locales/en.json index 12f9565..722625d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -4,8 +4,8 @@ "title": "One hour of interrupted silence", "resetButton": "Reset" }, - "audioOrVideoSourceInput": { - "audioOrVideoSourceInputSpan": "or" + "playerSourceSelector": { + "playerSourceSelectorSpan": "or" }, "fileInputButton": { "fileInputButtonText": "Use audio or video file" diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index a6a9dea..4522a88 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -4,8 +4,8 @@ "title": "Uma hora de silêncio interrompido", "resetButton": "Zerar" }, - "audioOrVideoSourceInput": { - "audioOrVideoSourceInputSpan": "ou" + "playerSourceSelector": { + "playerSourceSelectorSpan": "ou" }, "fileInputButton": { "fileInputButtonText": "Usar arquivo de áudio ou vídeo" diff --git a/src/utils/extractYoutubeVideoId.ts b/src/utils/extractYoutubeVideoId.ts deleted file mode 100644 index 09d1758..0000000 --- a/src/utils/extractYoutubeVideoId.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default function extractYoutubeVideoId(url: string): string | null { - const regex: RegExp = - /(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/ ]{11})/ - const match: RegExpMatchArray | null = url.match(regex) - - return match ? match[1] : null -}