Skip to content

Commit

Permalink
183 - fix: Phrase player (#169)
Browse files Browse the repository at this point in the history
* fix: player reader

* fix: story text

* fix: css and render in map

* add: useAudioSource hook

* add: adjusted  hook

* fix: memoize

* fix: broken scrolling

* fix: sync scroll on mobile version web

* fix: sync text

* fix: text phrases

* add: seek to phrase

* fix: isPlaying

* fix: autoplay on Apple devices

* fix: autoplay on Apple devices

* fix: description

* fix: cr

* fix: cr
  • Loading branch information
mikekubn authored Sep 11, 2022
1 parent 162ea95 commit c647d32
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 123 deletions.
132 changes: 40 additions & 92 deletions components/basecomponents/StoryReader.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useEffect, useState, useRef } from 'react';
import React from 'react';
import PlayIcon from '../../public/icons/stories-play.svg';
import PauseIcon from '../../public/icons/stories-pause.svg';
import StopIcon from '../../public/icons/stories-stop.svg';
import { Language } from '../../utils/locales';
import { getCountryVariant, Language } from '../../utils/locales';
import { useLanguage } from 'utils/useLanguageHook';
import { Flag } from './Flag';
import StoryText from './StoryText';
import { useStoryReader } from 'components/hooks/useStoryReader';

interface StoryReaderProps {
titleCurrent: string;
Expand All @@ -14,96 +15,42 @@ interface StoryReaderProps {
country: string;
}

const StoryReader = ({ titleCurrent, titleOther, id, country }: StoryReaderProps): JSX.Element => {
const StoryReader = ({ titleCurrent, titleOther, id }: StoryReaderProps): JSX.Element => {
const { currentLanguage } = useLanguage();
const [currentTime, setCurrentTime] = useState(0);
const [seekValue, setSeekValue] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const [languagePlay, setLanguagePlay] = useState(currentLanguage);

// using useRef to prevent keeping playing audio when changing route, see: https://stackoverflow.com/questions/37949895/stop-audio-on-route-change-in-react
const audio = useRef<HTMLAudioElement | null>(null);

useEffect(() => {
const source = `https://data.movapp.eu/bilingual-reading/${id}-${languagePlay}.mp3`;
audio.current = new Audio(source);
return () => {
if (audio.current !== null) {
audio.current.pause();
}
};
}, [languagePlay, id]);

const playStory = () => {
if (audio.current !== null) {
audio.current.play();
setIsPlaying(true);
}
};

const pauseStory = () => {
if (audio.current !== null) {
audio.current.pause();
setIsPlaying(false);
}
};

const stopStory = () => {
if (audio.current !== null) {
audio.current.pause();
audio.current.currentTime = 0;
setIsPlaying(false);
}
};

useEffect(() => {
if (audio.current !== null) {
audio.current.ontimeupdate = () => {
if (audio.current !== null) {
const duration = audio.current.duration;
const actualTime = audio.current.currentTime;
setCurrentTime(actualTime);
setSeekValue(duration ? (actualTime / duration) * 100 : 0);
if (actualTime === duration) {
setIsPlaying(false);
}
}
};
}
});

const time = `${Math.floor(currentTime / 60)}`.padStart(2, '0') + ':' + `${Math.floor(currentTime % 60)}`.padStart(2, '0');
const { audio, languagePlay, setLanguagePlay, setSeekValue, seekValue, stopStory, isPlaying, pauseStory, playStory, time, playPhrase } =
useStoryReader(id);

const handleLanguageChange = (language: Language) => {
setSeekValue(0);
setLanguagePlay(language);
stopStory();
};

const locales = ['uk' as Language, getCountryVariant()];

return (
<div className="w-full">
<div className="controls">
<div className="flex items-center justify-between">
<h2 className="p-0 m-0 text-sm sm:text-base md:text-xl">
{titleCurrent} / {titleOther}
</h2>
<div className="flex items-center">
<button onClick={() => handleLanguageChange('cs')}>
<Flag
language="cs"
width={27}
height={27}
className={`mr-3 ml-3 ease-in-out duration-300 ${languagePlay === 'cs' ? 'scale-125' : ''}`}
/>
</button>
<button onClick={() => handleLanguageChange('uk')}>
<Flag
language="uk"
width={27}
height={27}
className={`ease-in-out duration-300 ${languagePlay === 'uk' ? 'scale-125' : ''}`}
/>
</button>
<div className={`flex items-center ${currentLanguage !== 'uk' ? 'flex-row-reverse' : 'flex-row'}`}>
{locales.map((local) => (
<button
key={local}
onClick={() => {
handleLanguageChange(local);
}}
>
<Flag
language={local}
width={27}
height={27}
className={`ml-3 ease-in-out duration-300 ${local === languagePlay && 'scale-125'}`}
/>
</button>
))}
</div>
</div>
<div className="flex items-center justify-between pt-2">
Expand All @@ -116,7 +63,6 @@ const StoryReader = ({ titleCurrent, titleOther, id, country }: StoryReaderProps
)}
</button>
<button onClick={() => stopStory()}>
{' '}
<StopIcon className="cursor-pointer active:scale-75 transition-all duration-300" width="50" height="50" />
</button>
</div>
Expand All @@ -128,26 +74,28 @@ const StoryReader = ({ titleCurrent, titleOther, id, country }: StoryReaderProps
step="1"
value={seekValue}
onChange={(e) => {
const seekto = audio.current !== null ? audio.current.duration * (Number(e.target.value) / 100) : 0;
audio.current !== null ? (audio.current.currentTime = seekto) : null;
const seekTo = audio.current !== null ? audio.current.duration * (Number(e.target.value) / 100) : 0;
audio.current !== null ? (audio.current.currentTime = seekTo) : null;
setSeekValue(Number(e.target.value));
}}
/>
<p className="text-xl text-right">{time}</p>
</div>
</div>
<div className="md:flex">
{country === 'CZ' ? (
<>
<StoryText audio={audio.current} languageText="cs" languagePlay={languagePlay} onPlaying={setIsPlaying} id={id} />
<StoryText audio={audio.current} languageText="uk" languagePlay={languagePlay} onPlaying={setIsPlaying} id={id} />
</>
) : (
<>
<StoryText audio={audio.current} languageText="uk" languagePlay={languagePlay} onPlaying={setIsPlaying} id={id} />
<StoryText audio={audio.current} languageText="cs" languagePlay={languagePlay} onPlaying={setIsPlaying} id={id} />
</>
)}
<div className={`flex ${currentLanguage !== 'uk' ? 'flex-col-reverse md:flex-row-reverse' : 'flex-col md:flex-row'}`}>
{locales.map((local) => (
<StoryText
key={local}
audio={audio.current}
textLanguage={local}
onClick={(value) => {
playPhrase(value);
setLanguagePlay(local);
}}
audioLanguage={languagePlay}
id={id}
/>
))}
</div>
</div>
);
Expand Down
63 changes: 32 additions & 31 deletions components/basecomponents/StoryText.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import React, { useEffect, useRef, MutableRefObject } from 'react';
import React, { MutableRefObject, useEffect, useRef } from 'react';
import oPernikoveChaloupce from '../../data/translations/cs/pohadka_pernikovachaloupka.json';
import oDvanactiMesickach from '../../data/translations/cs/pohadka_mesicky.json';
import oCerveneKarkulce from '../../data/translations/cs/pohadka_karkulka.json';
import oKoblizkovi from '../../data/translations/cs/pohadka_koblizek.json';
import oIvasikovi from '../../data/translations/cs/pohadka_ivasik.json';
import oHusach from '../../data/translations/cs/pohadka_husy.json';
import { Language } from 'utils/locales';

export type PhraseInfo = { language: Language; time: number };

interface StoryTextProps {
audio: HTMLAudioElement | null;
languageText: string;
languagePlay: string;
textLanguage: Language;
audioLanguage: Language;
id: string;
onPlaying: (playing: boolean) => void;
onClick: ({ language, time }: PhraseInfo) => void;
}

interface StoryPhrase {
Expand All @@ -29,7 +32,7 @@ const scrollToRef = (ref: MutableRefObject<HTMLParagraphElement | null>, div: Mu
}
};

const StoryText = ({ languageText, languagePlay, audio, id, onPlaying }: StoryTextProps): JSX.Element => {
const StoryText = ({ textLanguage, audioLanguage, id, audio, onClick }: StoryTextProps): JSX.Element => {
const phraseRef = useRef<HTMLParagraphElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);

Expand All @@ -49,18 +52,10 @@ const StoryText = ({ languageText, languagePlay, audio, id, onPlaying }: StoryTe
return scrollToRef(phraseRef, containerRef);
}, [phraseRef?.current?.offsetTop]);

const playPhrase = (start: number) => {
if (audio !== null) {
audio.currentTime = start;
audio.play();
onPlaying(true);
}
};

const playing = (phrase: StoryPhrase) => {
type ObjectKey = keyof typeof phrase;
const start = `start_${languagePlay}` as ObjectKey;
const end = `end_${languagePlay}` as ObjectKey;
const start = `start_${audioLanguage}` as ObjectKey;
const end = `end_${audioLanguage}` as ObjectKey;
if (audio !== null) {
return audio?.currentTime > phrase[start] && audio?.currentTime < phrase[end];
} else {
Expand All @@ -70,32 +65,38 @@ const StoryText = ({ languageText, languagePlay, audio, id, onPlaying }: StoryTe

const played = (phrase: StoryPhrase) => {
type ObjectKey = keyof typeof phrase;
const end = `end_${languagePlay}` as ObjectKey;
const end = `end_${audioLanguage}` as ObjectKey;
if (audio !== null) {
return audio?.currentTime >= phrase[end];
} else {
return false;
}
};

const handleClick = (e: React.MouseEvent<HTMLParagraphElement, MouseEvent>) => {
const startTime = e.currentTarget.id;

const phraseInfo: PhraseInfo = { language: textLanguage, time: Number(startTime) };

onClick(phraseInfo);
};

return (
<div className="mt-4 md:flex bg-slate-100 divide-y-8 divide-white md:divide-y-0 md:w-1/2">
<div className="max-h-[30vh] md:max-h-full overflow-y-scroll md:overflow-auto" ref={containerRef}>
{selectedStory().map((phrase: StoryPhrase, index: number) => {
return (
<div key={index}>
<button onClick={() => playPhrase(languagePlay === 'cs' ? phrase.start_cs : phrase.start_uk)} className="text-left">
<p
key={index}
ref={playing(phrase) ? phraseRef : null}
className={`mx-6 my-2 ${playing(phrase) ? 'text-[#013ABD]' : ''} ${played(phrase) ? 'text-[#64a5da]' : ''}`}
>
{languageText === 'cs' ? phrase.main : phrase.uk}
</p>
</button>
</div>
);
})}
{selectedStory().map((phrase: StoryPhrase, index: number) => (
<p
key={index}
onClick={handleClick}
ref={playing(phrase) ? phraseRef : null}
id={textLanguage === 'uk' ? phrase.start_uk.toString() : phrase.start_cs.toString()}
className={`hover:cursor-pointer mx-6 my-4 text-left ${playing(phrase) && 'text-[#013ABD]'} ${
played(phrase) && 'text-[#64a5da]'
}`}
>
{textLanguage === 'cs' ? phrase.main : phrase.uk}
</p>
))}
</div>
</div>
);
Expand Down
97 changes: 97 additions & 0 deletions components/hooks/useStoryReader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { PhraseInfo } from 'components/basecomponents/StoryText';
import React from 'react';
import { Language } from 'utils/locales';
import { useLanguage } from 'utils/useLanguageHook';

export const useStoryReader = (id: string) => {
const { currentLanguage } = useLanguage();
const [currentTime, setCurrentTime] = React.useState(0);
const [seekValue, setSeekValue] = React.useState(0);
const [isPlaying, setIsPlaying] = React.useState(false);
const [languagePlay, setLanguagePlay] = React.useState(currentLanguage);

// using useRef to prevent keeping playing audio when changing route, see: https://stackoverflow.com/questions/37949895/stop-audio-on-route-change-in-react
const audio = React.useRef<HTMLAudioElement | null>(null);
const source = `https://data.movapp.eu/bilingual-reading/${id}-${languagePlay}.mp3`;

const playStory = () => {
if (audio.current !== null) {
setIsPlaying(true);
audio.current.play();
}
};

const pauseStory = () => {
if (audio.current !== null) {
setIsPlaying(false);
audio.current.pause();
}
};

const stopStory = React.useCallback(() => {
if (audio.current !== null) {
pauseStory();
audio.current.currentTime = 0;
}
}, []);

const playPhrase = React.useCallback((value: PhraseInfo) => {
const { time, language } = value;

setLanguagePlay(language as Language);
setSeekValue(time);

// Keep setTimeout value below 951. It is the lowest value, that the browser on Apple devices know and that it can enable autoplay.
setTimeout(() => {
if (audio.current !== null) {
audio.current.currentTime = time;
playStory();
}
}, 500);
}, []);

React.useEffect(() => {
audio.current = new Audio(source);
return () => {
if (audio.current !== null) {
stopStory();
}
};
}, [languagePlay, id, source, stopStory]);

React.useEffect(() => {
audio.current = new Audio(source);
return () => {
audio.current = null;
};
}, [source]);

React.useEffect(() => {
if (audio.current !== null) {
audio.current.ontimeupdate = () => {
if (audio.current !== null) {
const duration = audio.current.duration;
const actualTime = audio.current.currentTime;
setCurrentTime(actualTime);
setSeekValue(duration ? (actualTime / duration) * 100 : 0);
}
};
}
});

const time = `${Math.floor(currentTime / 60)}`.padStart(2, '0') + ':' + `${Math.floor(currentTime % 60)}`.padStart(2, '0');

return {
languagePlay,
setLanguagePlay,
seekValue,
setSeekValue,
isPlaying,
playStory,
pauseStory,
stopStory,
playPhrase,
time,
audio,
};
};

0 comments on commit c647d32

Please sign in to comment.