Add ability to pause, and resume the stream

This change allows the stream to be paused, and
resumed on-demand.
This commit is contained in:
0x1eef 2022-12-28 05:53:47 -03:00 committed by Robert
parent 44fdb443f8
commit 13789768a1
10 changed files with 125 additions and 30 deletions

View file

@ -0,0 +1,38 @@
.shape-box {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
width: 24px;
height: 24px;
border-radius: 5px;
background: #606850;
cursor: pointer;
}
.shape-box .play-shape {
position: relative;
left: 1px;
width: 12px;
height: 12px;
background: #FFF;
clip-path: polygon(0 0, 100% 50%, 0 100%);
}
.shape-box .pause-shape {
width: 3px;
height: 12px;
background: #FFF;
clip-path: stroke-box;
&:first-child {
position: relative;
right: 2px;
}
&:last-child {
position: relative;
left: 1.5px;
}
}

View file

@ -1,5 +1,6 @@
@import "fonts"; @import "fonts";
@import "components/Select"; @import "components/Select";
@import "components/TheQuran/Shape";
$black: #454545; $black: #454545;
@ -104,17 +105,24 @@ body {
} }
.surah .timer { .surah .timer {
margin: 0 auto;
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
font-size: 65%; font-size: 65%;
font-family: "Roboto Mono Regular"; font-family: "Kanit Regular";
text-align: center; text-align: center;
width: 30px; width: 32px;
height: 18px;
font-weight: bold; font-weight: bold;
border-radius: 10px; border-radius: 5px;
padding: 3px; padding: 2px;
position: relative;
top: 4px;
}
.surah.ar .timer {
position: relative;
top: 2px;
} }
} }

View file

@ -24,9 +24,13 @@
} }
} }
.timer { .surah-row .timer {
color: $white; color: $white;
background-color: lighten($gold, 5%); background: $gold;
}
.surah-row .container.shape {
background: $gold;
} }
} }

View file

@ -35,8 +35,7 @@ const createOption = (e: ChangeEvent, children: JSX.Element[]): SelectOption =>
}; };
}; };
export function Select(props: Props) { export function Select({ value, children, onChange, className }: Props) {
const { children, className, value, onChange } = props;
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const [activeOption, setActiveOption] = useState<string | null>(findOption(value, children)); const [activeOption, setActiveOption] = useState<string | null>(findOption(value, children));
const openSelect = (e: React.MouseEvent<HTMLSpanElement>) => { const openSelect = (e: React.MouseEvent<HTMLSpanElement>) => {

View file

@ -6,13 +6,15 @@ interface Props {
locale: string locale: string
surah: Surah surah: Surah
stream: Ayah[] stream: Ayah[]
isPaused: boolean
} }
export function LanguageSelect(props: Props) { export function LanguageSelect({ locale, surah, stream, isPaused }: Props) {
const { locale, surah, stream } = props;
const changeLanguage = (o: SelectOption) => { const changeLanguage = (o: SelectOption) => {
const locale = o.value; const locale = o.value;
location.replace(`/${locale}/${surah.slug}/?ayah=${stream.length}`); location.replace(
`/${locale}/${surah.slug}/?ayah=${stream.length}&paused=${isPaused ? 't' : 'f'}`
);
}; };
return ( return (

View file

@ -0,0 +1,22 @@
import React from 'react';
interface Props {
onClick: () => void
}
export function PlayShape({ onClick }: Props) {
return (
<div className="shape-box" onClick={onClick}>
<div className="play-shape" />
</div>
);
}
export function PauseShape({ onClick }: Props) {
return (
<div className="shape-box" onClick={onClick}>
<div className="pause-shape" />
<div className="pause-shape" />
</div>
);
}

View file

@ -10,12 +10,13 @@ interface Props {
locale: Locale locale: Locale
slice: Slice slice: Slice
endOfStream: boolean endOfStream: boolean
isPaused: boolean
} }
export function Stream({ surah, stream, locale, slice, endOfStream }: Props) { export function Stream({ surah, stream, locale, slice, endOfStream, isPaused }: Props) {
const n = numbers(locale); const n = numbers(locale);
const s = strings(locale); const s = strings(locale);
const className = classNames('stream', { 'scroll-y': endOfStream }); const className = classNames('stream', { 'scroll-y': endOfStream || isPaused });
const ayat = stream.map((ayah: Ayah) => { const ayat = stream.map((ayah: Ayah) => {
return ( return (
<li key={ayah.id.number} className="ayah fade"> <li key={ayah.id.number} className="ayah fade">

View file

@ -7,19 +7,22 @@ interface Props {
locale: Locale locale: Locale
stream: Ayat stream: Ayat
setStream: (stream: Ayat) => void setStream: (stream: Ayat) => void
isPaused: boolean
} }
export function Timer ({ surah, stream, setStream, locale }: Props) { export function Timer ({ surah, stream, setStream, locale, isPaused }: Props) {
const ayah = stream[stream.length - 1]; const ayah = stream[stream.length - 1];
const [ms, setMs] = useState(ayah.readTimeMs); const [ms, setMs] = useState(ayah.readTimeMs);
useEffect(() => setMs(ayah.readTimeMs), [ayah.id]); useEffect(() => setMs(ayah.readTimeMs), [ayah.id]);
useEffect(() => { useEffect(() => {
if (ms <= 0) { if (isPaused) {
return;
} else if (ms <= 0) {
setStream([...stream, surah.ayat[ayah.id.number]]); setStream([...stream, surah.ayat[ayah.id.number]]);
} else { } else {
setTimeout(() => setMs(ms - 100), 100); setTimeout(() => setMs(ms - 100), 100);
} }
}, [ms]); }, [ms, isPaused]);
return ( return (
<div className='timer'> <div className='timer'>
{numberToDecimal(ms / 1000, locale)} {numberToDecimal(ms / 1000, locale)}

View file

@ -66,6 +66,6 @@ export function numberToDecimal(number: number, locale: Locale): string {
const s = strings(locale); const s = strings(locale);
const n = numbers(locale); const n = numbers(locale);
return decimal.split('.') return decimal.split('.')
.map((num: Digit) => n(num)) .map((num: Digit) => n(num))
.join(s('decimal')); .join(` ${s('decimal')} `);
} }

View file

@ -6,6 +6,7 @@ import { Timer } from 'components/TheQuran/Timer';
import { Stream } from 'components/TheQuran/Stream'; import { Stream } from 'components/TheQuran/Stream';
import { ThemeSelect } from 'components/TheQuran/ThemeSelect'; import { ThemeSelect } from 'components/TheQuran/ThemeSelect';
import { LanguageSelect } from 'components/TheQuran/LanguageSelect'; import { LanguageSelect } from 'components/TheQuran/LanguageSelect';
import { PlayShape, PauseShape } from 'components/TheQuran/Shape';
import { Locale, Surah } from 'lib/Quran'; import { Locale, Surah } from 'lib/Quran';
import { Slice } from 'lib/Quran/slice'; import { Slice } from 'lib/Quran/slice';
@ -13,12 +14,14 @@ interface Props {
locale: Locale locale: Locale
surahId: number surahId: number
slice: Slice slice: Slice
paused: boolean
} }
function TheSurahPage({ locale, surahId, slice }: Props) { function TheSurahPage({ locale, surahId, slice, paused }: Props) {
const path = `/${locale}/${surahId}/surah.json`; const path = `/${locale}/${surahId}/surah.json`;
const node: HTMLScriptElement = document.querySelector(`script[src="${path}"]`); const node: HTMLScriptElement = document.querySelector(`script[src="${path}"]`);
const [stream, setStream] = useState([]); const [stream, setStream] = useState([]);
const [isPaused, setIsPaused] = useState<boolean>(paused);
const [theme, setTheme] = useState(getCookie('theme') || 'moon'); const [theme, setTheme] = useState(getCookie('theme') || 'moon');
const [surah] = useState<Surah>(Surah.fromDOMNode(locale, node)); const [surah] = useState<Surah>(Surah.fromDOMNode(locale, node));
const readyToRender = stream.length > 0; const readyToRender = stream.length > 0;
@ -52,11 +55,16 @@ function TheSurahPage({ locale, surahId, slice }: Props) {
{readyToRender && ( {readyToRender && (
<div className="surah-row theme-language"> <div className="surah-row theme-language">
<ThemeSelect theme={theme} setTheme={setTheme} /> <ThemeSelect theme={theme} setTheme={setTheme} />
<LanguageSelect locale={locale} surah={surah} stream={stream} /> <LanguageSelect
locale={locale}
surah={surah}
stream={stream}
isPaused={isPaused}
/>
</div> </div>
)} )}
{readyToRender && ( {readyToRender && (
<div className='surah-row surah-details'> <div className="surah-row surah-details">
<span lang={locale}>{surahName}</span> <span lang={locale}>{surahName}</span>
<span>{surah.transliteratedName}</span> <span>{surah.transliteratedName}</span>
</div> </div>
@ -68,27 +76,36 @@ function TheSurahPage({ locale, surahId, slice }: Props) {
stream={stream} stream={stream}
locale={locale} locale={locale}
endOfStream={endOfStream} endOfStream={endOfStream}
isPaused={isPaused}
/> />
} }
{readyToRender && !endOfStream && ( <div className="surah-row">
<Timer { readyToRender && isPaused && !endOfStream &&
surah={surah} <PlayShape onClick={() => setIsPaused(false)}/> }
setStream={setStream} { readyToRender && !isPaused && !endOfStream &&
stream={stream} <PauseShape onClick={() => setIsPaused(true)}/> }
locale={locale} { readyToRender && !endOfStream &&
/> <Timer
)} surah={surah}
setStream={setStream}
stream={stream}
locale={locale}
isPaused={isPaused}
/> }
</div>
</div> </div>
); );
} }
(function() { (function() {
const toBoolean = (str: string | null): boolean => ['1', 't', 'true', 'yes'].includes(str);
const rootBox: HTMLElement = document.querySelector('.root-box'); const rootBox: HTMLElement = document.querySelector('.root-box');
const locale = rootBox.getAttribute('data-locale') as Locale; const locale = rootBox.getAttribute('data-locale') as Locale;
const surahId = parseInt(rootBox.getAttribute('data-surah-id')); const surahId = parseInt(rootBox.getAttribute('data-surah-id'));
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const slice = Slice.fromParam(params.get('ayah')); const slice = Slice.fromParam(params.get('ayah'));
const paused = toBoolean(params.get('paused'));
ReactDOM ReactDOM
.createRoot(rootBox) .createRoot(rootBox)
@ -97,6 +114,7 @@ function TheSurahPage({ locale, surahId, slice }: Props) {
locale={locale} locale={locale}
surahId={surahId} surahId={surahId}
slice={slice} slice={slice}
paused={paused}
/> />
); );
})(); })();