diff --git a/.projectile b/.projectile index c396cc06b..19219319e 100644 --- a/.projectile +++ b/.projectile @@ -1,3 +1,4 @@ -/node_modules/ -/tmp/ -/build/ +-/.localgems/ diff --git a/src/css/pages/SurahStream.scss b/src/css/pages/SurahStream.scss index a2653e117..f0a835bb3 100644 --- a/src/css/pages/SurahStream.scss +++ b/src/css/pages/SurahStream.scss @@ -12,10 +12,15 @@ scrollbar-gutter: stable; li.ayah { - span.surah-id.ayah-id { - display: block; + span.title { + display: flex; + align-items: center; font-weight: 500; } + span.title .svg.sound-on, span.title .svg.sound-off { + cursor: pointer; + width: 24px; + } p { margin: 3px 0 10px 0 } diff --git a/src/css/themes/leaf/pages/_SurahStream.scss b/src/css/themes/leaf/pages/_SurahStream.scss index 4a8029801..3415e3242 100644 --- a/src/css/themes/leaf/pages/_SurahStream.scss +++ b/src/css/themes/leaf/pages/_SurahStream.scss @@ -6,7 +6,7 @@ } ul.body.stream li.ayah { - span.surah-id.ayah-id { + span.title { color: $green; } p { } @@ -17,17 +17,6 @@ color: $white; } - .row svg g { - polygon { - stroke: $green; - stroke-width: 5; - } - path { - stroke: $green; - stroke-width: 5; - } - } - .row .shape.refresh { background: unset; fill: $green; @@ -36,4 +25,11 @@ .row .loading div { background: $green; } + + .svg.sound-on, .svg.sound-off { + polygon { + fill: $green; + stroke-width: 2; + } + } } diff --git a/src/css/themes/moon/pages/_SurahStream.scss b/src/css/themes/moon/pages/_SurahStream.scss index 0d1309760..827db95c1 100644 --- a/src/css/themes/moon/pages/_SurahStream.scss +++ b/src/css/themes/moon/pages/_SurahStream.scss @@ -7,8 +7,7 @@ ul.body.stream { li.ayah { - /* TODO "surah.id", "ayah.id" */ - span.surah-id.ayah-id { + span.title { color: $gold; } p { } @@ -29,18 +28,14 @@ fill: $gold; } - .row svg g { - polygon { - stroke: $gold; - stroke-width: 5; - } - path { - stroke: $gold; - stroke-width: 5; - } - } - .row .loading div { background: $gold; } + + .svg.sound-on, .svg.sound-off { + polygon { + fill: $blue; + stroke-width: 2; + } + } } diff --git a/src/html/stream.html.erb b/src/html/stream.html.erb index 5dc19c3dd..16cfea254 100644 --- a/src/html/stream.html.erb +++ b/src/html/stream.html.erb @@ -34,7 +34,7 @@ data-surah-id="<%= context.surah.id %>"> <%= inline_json("/i18n.json") %> - <%= inline_json("/reciters.json") %> + <%= inline_json("/recitations.json") %> diff --git a/src/js/components/AudioControl.tsx b/src/js/components/AudioControl.tsx new file mode 100644 index 000000000..9aab080bd --- /dev/null +++ b/src/js/components/AudioControl.tsx @@ -0,0 +1,56 @@ +import url from "url"; +import * as Quran from "lib/Quran"; +import React, { useEffect, useMemo, useState } from "react"; +import { SoundOnShape, SoundOffShape } from "components/Shape"; + +type Props = { + recitation: Quran.Recitation; + surah: Quran.Surah; + ayah: Quran.Ayah; + onStall?: (e?: Event) => void; + onPlay?: (e?: Event) => void; + onPlaying?: (e?: Event) => void; + onPause?: (e?: Event) => void; + onEnd?: (turnOffSound: () => void) => void; +}; + +export function AudioControl({ + recitation, + surah, + ayah, + onPlay = () => null, + onPlaying = () => null, + onPause = () => null, + onStall = () => null, + onEnd = () => null, +}: Props) { + const [soundOn, setSoundOn] = useState(false); + const audio = useMemo(() => new Audio(), []); + const turnOnSound = () => setSoundOn(true); + const turnOffSound = () => setSoundOn(false); + + useEffect(() => { + audio.addEventListener("ended", () => onEnd(turnOffSound)); + audio.addEventListener("stalled", onStall); + audio.addEventListener("waiting", onStall); + audio.addEventListener("play", onPlay); + audio.addEventListener("playing", onPlaying); + }, []); + + useEffect(() => { + if (soundOn) { + audio.src = [url.format(recitation.url), surah.id, `${ayah.id}.mp3`].join("/"); + audio.play(); + } else { + audio.pause(); + onPause(); + } + }, [soundOn, ayah.id]); + + return ( + <> + {soundOn && } + {!soundOn && } + + ); +} diff --git a/src/js/components/Shape.tsx b/src/js/components/Shape.tsx index 850d68b99..15bca76f6 100644 --- a/src/js/components/Shape.tsx +++ b/src/js/components/Shape.tsx @@ -23,7 +23,7 @@ export function PauseShape({ onClick }: Props) { export function SoundOnShape({ onClick }: Props) { return ( - + + { return (
  • - - {t(locale, "surah")} {formatNumber(surah.id, locale)} - {t(locale, "comma")} {t(locale, "ayah")} {formatNumber(ayah.id, locale)} + + {(isPaused || endOfStream) && ( + turnOffSound()} + /> + )} + + {t(locale, "surah")} {formatNumber(surah.id, locale)} + {t(locale, "comma")} {t(locale, "ayah")} {formatNumber(ayah.id, locale)} +

    {ayah.text}

  • diff --git a/src/js/lib/Quran.ts b/src/js/lib/Quran.ts index 6c0de1a25..0ac621a21 100644 --- a/src/js/lib/Quran.ts +++ b/src/js/lib/Quran.ts @@ -4,6 +4,17 @@ import { Surah } from "lib/Quran/Surah"; type Locale = "ar" | "en"; type Ayat = Ayah[]; -type Reciter = { id: string; name: string; nationality: string; url: string }; +type Recitation = { + id: string; + author: { + name: string; + nationality: string; + }; + url: { + protocol: string; + hostname: string; + pathname: string; + }; +}; -export { Surah, Ayah, Ayat, Reciter, Locale, JSON }; +export { Surah, Ayah, Ayat, Recitation, Locale, JSON }; diff --git a/src/js/loaders/SurahStreamLoader.ts b/src/js/loaders/SurahStreamLoader.ts index 0f50023ad..2ed1de304 100644 --- a/src/js/loaders/SurahStreamLoader.ts +++ b/src/js/loaders/SurahStreamLoader.ts @@ -1,4 +1,5 @@ import postman, { item } from "postman"; +import url from "url"; import * as Quran from "lib/Quran"; (function () { @@ -7,8 +8,8 @@ import * as Quran from "lib/Quran"; const progressNumber: HTMLSpanElement = parent.querySelector(".percentage")!; const inlineStyle: HTMLStyleElement = document.querySelector(".css.postman")!; const { locale, surahId } = document.querySelector(".root")!.dataset; - const reciters = JSON.parse( - document.querySelector(".json.reciters")!.innerText, + const recitations = JSON.parse( + document.querySelector(".json.recitations")!.innerText, ); postman( @@ -20,10 +21,10 @@ import * as Quran from "lib/Quran"; item.font("Vazirmatn Regular", "url(/fonts/vazirmatn-regular.ttf)"), item.font("Roboto Mono Regular", "url(/fonts/roboto-mono-regular.ttf)"), item.json(`/${locale}/${surahId}/surah.json`, { className: "surah" }), - ...reciters.map((reciter: Quran.Reciter) => { - const { url: baseUrl } = reciter; - return item.json(`${baseUrl}/time_slots/${surahId}.json`, { - className: `reciter time-slots ${reciter.id}`, + ...recitations.map((recitation: Quran.Recitation) => { + const ts = [url.format(recitation.url), "time_slots", `${surahId}.json`].join("/"); + return item.json(ts, { + className: `recitation time-slots ${recitation.id}`, }); }), item.progress((percent: number) => { diff --git a/src/js/pages/SurahStream.tsx b/src/js/pages/SurahStream.tsx index 2d1c2bed1..a711ce852 100644 --- a/src/js/pages/SurahStream.tsx +++ b/src/js/pages/SurahStream.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import ReactDOM from "react-dom/client"; import classNames from "classnames"; import { get as getCookie } from "es-cookie"; @@ -7,50 +7,38 @@ import { Stream } from "components/Stream"; import { SelectOption } from "components/Select"; import { ThemeSelect } from "components/ThemeSelect"; import { LanguageSelect } from "components/LanguageSelect"; -import { - PlayShape, - PauseShape, - SoundOnShape, - SoundOffShape, - RefreshShape, - LoadingShape, -} from "components/Shape"; +import { AudioControl } from "components/AudioControl"; +import { PlayShape, PauseShape, RefreshShape, LoadingShape } from "components/Shape"; import * as Quran from "lib/Quran"; import { i18n, TFunction } from "lib/i18n"; interface Props { node: HTMLScriptElement; - reciters: Quran.Reciter[]; + recitations: Quran.Recitation[]; locale: Quran.Locale; paused: boolean; t: TFunction; } -const getTimeSlots = (reciter: Quran.Reciter) => { - const selector = `script.reciter.time-slots.${reciter.id}`; +const getTimeSlots = (recitation: Quran.Recitation) => { + const selector = `script.recitation.time-slots.${recitation.id}`; const timeSlots: HTMLScriptElement = document.querySelector(selector)!; return timeSlots; }; -const getAudioURL = (reciter: Quran.Reciter, surah: Quran.Surah, stream: Quran.Ayat) => { - const { url: baseUrl } = reciter; - const ayah = stream[stream.length - 1]; - return `${baseUrl}/${surah.id}/${ayah?.id}.mp3`; -}; - -function SurahStream({ node, reciters, locale, paused, t }: Props) { +function SurahStream({ node, recitations, locale, paused, t }: Props) { const [stream, setStream] = useState([]); const [isPaused, setIsPaused] = useState(paused); const [soundOn, setSoundOn] = useState(false); const [isStalled, setIsStalled] = useState(false); const [endOfStream, setEndOfStream] = useState(false); const [theme, setTheme] = useState(getCookie("theme") || "moon"); - const [reciter] = useState(reciters[0]); + const [recitation] = useState(recitations[0]); const [surah] = useState( - Quran.Surah.fromDOMNode(locale, node, getTimeSlots(reciter)), + Quran.Surah.fromDOMNode(locale, node, getTimeSlots(recitation)), ); const readyToRender = stream.length > 0; - const audioRef = useRef(null); + const ayah = stream[stream.length - 1]; const onLanguageChange = (o: SelectOption) => { const locale = o.value; const params = [["paused", isPaused ? "t" : null]]; @@ -66,40 +54,6 @@ function SurahStream({ node, reciters, locale, paused, t }: Props) { setStream([surah.ayat[0]]); }, [stream.length === 0]); - useEffect(() => { - const audio = audioRef.current; - if (!audio) { - return; - } else if (isPaused) { - audio.pause(); - } else if (audio.paused) { - audio.play().catch(() => setSoundOn(false)); - } - }, [soundOn, isPaused, stream.length]); - - useEffect(() => { - const audio = audioRef.current; - if (!audio) return; - const onEnded = () => { - const src = getAudioURL(reciter, surah, stream); - if (src !== audio.src) { - audio.src = src; - } - }; - const isStalled = () => setIsStalled(true); - const unStalled = () => setIsStalled(false); - audio.addEventListener("ended", onEnded); - audio.addEventListener("stalled", isStalled); - audio.addEventListener("waiting", isStalled); - audio.addEventListener("playing", unStalled); - return () => { - audio.removeEventListener("ended", onEnded); - audio.removeEventListener("stalled", isStalled); - audio.removeEventListener("waiting", isStalled); - audio.removeEventListener("playing", unStalled); - }; - }, [soundOn, stream.length]); - return (
    @@ -122,6 +76,7 @@ function SurahStream({ node, reciters, locale, paused, t }: Props) { )} {readyToRender && ( setIsPaused(true)} /> )} - {readyToRender && !endOfStream && soundOn && ( - setSoundOn(false)} /> - )} - {readyToRender && !endOfStream && !soundOn && ( - setSoundOn(true)} /> + {readyToRender && !endOfStream && ( + setSoundOn(true)} + onPause={() => setSoundOn(false)} + onPlaying={() => setIsStalled(false)} + onStall={() => setIsStalled(true)} + /> )} {readyToRender && !endOfStream && (
    )} - {readyToRender && soundOn && ( -
    ); } @@ -177,12 +134,18 @@ function SurahStream({ node, reciters, locale, paused, t }: Props) { str !== null && ["1", "t", "true", "yes"].includes(str); const params = new URLSearchParams(location.search); const paused = toBoolean(params.get("paused")); - const reciters = JSON.parse( - document.querySelector(".json.reciters")!.innerText, + const recitations = JSON.parse( + document.querySelector(".json.recitations")!.innerText, ); const t = i18n(document.querySelector(".json.i18n")!.innerText); ReactDOM.createRoot(root).render( - , + , ); })(); diff --git a/src/recitations.json b/src/recitations.json new file mode 100644 index 000000000..4d30727bf --- /dev/null +++ b/src/recitations.json @@ -0,0 +1,62 @@ +[ + { + "id": "mishari_alafasy", + "author": { + "name": "Mishari bin Rashed Alafasy", + "nationality": "KW" + }, + "url": { + "protocol": "https", + "hostname": "al-quran.reflectslight.io", + "pathname": "/audio/alafasy" + } + }, + { + "id": "aziz_alili", + "author": { + "name": "Aziz Alili", + "nationality": "MK" + }, + "url": { + "protocol": "https", + "hostname": "al-quran.reflectslight.io", + "pathname": "/audio/aziz_alili" + } + }, + { + "id": "abdullah_awad_al_juhany", + "author": { + "name": "Abdullah Awad Al Juhany", + "nationality": "SA" + }, + "url": { + "protocol": "https", + "hostname": "al-quran.reflectslight.io", + "pathname": "/audio/abdullah_awad_al_juhany" + } + }, + { + "id": "ahmad_bin_ali_al_ajmi", + "author": { + "name": "Ahmad bin Ali Al-Ajmi", + "nationality": "SA" + }, + "url": { + "protocol": "https", + "hostname": "al-quran.reflectslight.io", + "pathname": "/audio/ahmad_bin_ali_al_ajmi" + } + }, + { + "id": "sahl_yassin", + "author": { + "name": "Sahl Yassin", + "nationality": "SA" + }, + "url": { + "protocol": "https", + "hostname": "al-quran.reflectslight.io", + "pathname": "/audio/sahl_yassin" + } + } +] diff --git a/src/reciters.json b/src/reciters.json deleted file mode 100644 index f148b7c49..000000000 --- a/src/reciters.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "id": "mishari_alafasy", - "name": "Mishari bin Rashed Alafasy", - "nationality": "KW", - "url": "https://al-quran.reflectslight.io/audio/alafasy/" - }, - { - "id": "aziz_alili", - "name": "Aziz Alili", - "nationality": "MK", - "url": "https://al-quran.reflectslight.io/audio/aziz_alili/" - }, - { - "id": "abdullah_awad_al_juhany", - "name": "Abdullah Awad Al Juhany", - "nationality": "SA", - "url": "https://al-quran.reflectslight.io/audio/abdullah_awad_al_juhany/" - }, - { - "id": "ahmad_bin_ali_al_ajmi", - "name": "Ahmad bin Ali Al-Ajmi", - "nationality": "SA", - "url": "https://al-quran.reflectslight.io/audio/ahmad_bin_ali_al_ajmi/" - }, - { - "id": "sahl_yassin", - "name": "Sahl Yassin", - "nationality": "SA", - "url": "https://al-quran.reflectslight.io/audio/sahl_yassin/" - } -]