Skip to content

Commit

Permalink
Merge pull request #2258 from edenmind/YunusAndreasson/issue2237
Browse files Browse the repository at this point in the history
feat(mobile): ✨ Play to read en/ar sentence in component
  • Loading branch information
YunusAndreasson committed Sep 5, 2023
2 parents db5927d + af71e81 commit 2a9276e
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 62 deletions.
Binary file modified mobile/.yarn/install-state.gz
Binary file not shown.
Empty file.
7 changes: 4 additions & 3 deletions mobile/components/arabic-words.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ArabicWordButton } from './arabic-words-button.js'
import { useSharedStyles } from '../styles/common.js'
import * as Haptics from 'expo-haptics'

export default function TextArabicWords({ sentence: { words } }) {
export default function ArabicWords({ sentence: { words }, currentPlayingWordIndex }) {
const [sound, setSound] = useState()
const theme = useTheme()
const sharedStyle = useSharedStyles(theme)
Expand Down Expand Up @@ -48,7 +48,7 @@ export default function TextArabicWords({ sentence: { words } }) {
<ArabicWordButton
key={wordIndex}
word={word}
isSelected={selectedWordIndex === wordIndex}
isSelected={currentPlayingWordIndex === wordIndex || selectedWordIndex === wordIndex}
theme={theme}
sharedStyle={sharedStyle}
onSelect={() => {
Expand All @@ -70,7 +70,8 @@ const styles = StyleSheet.create({
}
})

TextArabicWords.propTypes = {
ArabicWords.propTypes = {
currentPlayingWordIndex: PropTypes.number,
sentence: PropTypes.shape({
arabic: PropTypes.string.isRequired,
english: PropTypes.string.isRequired,
Expand Down
31 changes: 27 additions & 4 deletions mobile/components/english-arabic.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, { useMemo } from 'react'
/* eslint-disable unicorn/no-null */
import React, { useMemo, useState } from 'react'
import { useSharedStyles } from '../styles/common.js'
import { Text, useTheme } from 'react-native-paper'
import PropTypes from 'prop-types'
import { transliterateArabicToEnglish } from '../services/utility-service.js'
import { useSelector } from 'react-redux'
import TextArabicWords from './arabic-words.js'
import ArabicWords from './arabic-words.js'
import PlaySound from './play-sound.js'
import { UI } from '../constants/ui.js'

const isTransliterationOnSelector = (state) => state.isTransliterationOn

Expand All @@ -15,21 +18,41 @@ export const EnglishArabic = ({ sentence }) => {
const { isTransliterationOn } = useSelector(isTransliterationOnSelector)
const showTransliteration = isTransliterationOn === 'on'

const [currentPlayingWordIndex, setCurrentPlayingWordIndex] = useState(null)

const handlePlayingWord = (index) => {
setCurrentPlayingWordIndex(index)
}

const handlePlaybackFinished = () => {
setCurrentPlayingWordIndex(null)
}

const transliteratedText = useMemo(() => {
return transliterateArabicToEnglish(sentence.arabic)
}, [sentence.arabic])

//loop over all words in sentence that contains a property filename and add all filenames to an array
const fileNames = sentence.words.map((word) => word.filename)

return (
<>
<TextArabicWords sentence={sentence} />
<ArabicWords sentence={sentence} currentPlayingWordIndex={currentPlayingWordIndex} />

{showTransliteration && (
<Text style={{ ...sharedStyle.englishBody, color: theme.colors.outline, marginTop: 10 }} variant="bodyLarge">
<Text style={{ ...sharedStyle.englishBody, color: theme.colors.outline }} variant="bodyLarge">
{transliteratedText}
</Text>
)}
<Text variant="bodyLarge" style={{ ...sharedStyle.englishBody }}>
{sentence.english}
</Text>
<PlaySound
audioFileNames={fileNames}
buttonText={UI.playSentence}
onPlayingWord={handlePlayingWord}
onFinish={handlePlaybackFinished}
/>
</>
)
}
Expand Down
88 changes: 75 additions & 13 deletions mobile/components/play-sound.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable unicorn/no-null */
import * as React from 'react'
import { Audio } from 'expo-av'
import PropTypes from 'prop-types'
Expand All @@ -7,31 +8,89 @@ import { useState } from 'react'
import { capitalizeFirstLetter } from '../services/utility-service.js'

// eslint-disable-next-line putout/destructuring-as-function-argument
export default function PlaySound({ audioFileNames, buttonText }) {
export default function PlaySound({ audioFileNames, buttonText, onPlayingWord, onFinish }) {
const [sound, setSound] = React.useState()
const theme = useTheme()
const sharedStyle = useSharedStyles(theme)
const [color, setColor] = useState(theme.colors.elevation.level5)
const [isPlaying, setIsPlaying] = useState(false)

React.useEffect(() => {
// eslint-disable-next-line no-extra-semi
;async () => {
// This will override the silent switch on iOS
await Audio.setAudioModeAsync({
playsInSilentModeIOS: true
})
}
}, [])

const playAllSounds = async () => {
// Check if sound is playing
if (sound?._loaded) {
await sound.stopAsync()
await sound.unloadAsync()
setSound()
setSound(null)
setColor(theme.colors.elevation.level5)
setIsPlaying(false) // Update isPlaying state when sound is stopped manually
return
}

if (Array.isArray(audioFileNames)) {
for (const audioFileName of audioFileNames) {
await playSound(audioFileName)
let currentIndex = 0

const playNextSound = async () => {
if (currentIndex >= audioFileNames.length) {
setIsPlaying(false) // Set isPlaying to false when all sounds have finished

if (onFinish) {
onFinish() // Signal that all sounds are finished
}

return // Exit if all sounds have been played
}

const audioFileName = audioFileNames[currentIndex]

const { sound: newSound } = await Audio.Sound.createAsync(
{ uri: audioFileName },
{
shouldPlay: true,
rate: 1,
shouldCorrectPitch: true,
volume: 1,
isMuted: false,
isLooping: false,
isPlaybackAllowed: true,
isLoopingIOS: false,
isMutedIOS: false,
playsInSilentModeIOS: true,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX
},
(status) => {
if (status.didJustFinish) {
newSound.unloadAsync() // Unload the sound instance
currentIndex++ // Increase index for next sound
playNextSound() // Recursive call to play next sound
}
}
)

// Signal which word is playing, if provided in props
if (onPlayingWord) {
onPlayingWord(currentIndex)
}

setIsPlaying(true) // Set isPlaying to true when a sound starts
setSound(newSound)
await newSound.playAsync()
}
} else {
await playSound(audioFileNames)

playNextSound() // Kick off the recursive playing
return
}

await playSound(audioFileNames)
}

const playSound = async (audioFileName) => {
Expand Down Expand Up @@ -63,11 +122,6 @@ export default function PlaySound({ audioFileNames, buttonText }) {

setSound(sound)

// This will override the silent switch on iOS
await Audio.setAudioModeAsync({
playsInSilentModeIOS: true
})

setColor(theme.colors.primary)
setIsPlaying(true) // Update isPlaying state when sound starts
await sound.playAsync()
Expand All @@ -82,7 +136,13 @@ export default function PlaySound({ audioFileNames, buttonText }) {
}, [sound])

return (
<Button onPress={playAllSounds} style={{ ...sharedStyle.buttonAnswer, borderColor: color }}>
<Button
onPress={playAllSounds}
style={{
...sharedStyle.buttonAnswer,
borderColor: isPlaying ? theme.colors.primary : theme.colors.elevation.level5
}}
>
<Text style={{ ...sharedStyle.answerText, fontSize: buttonText.length > 25 ? 20 : 23 }}>
{isPlaying ? 'Stop' : capitalizeFirstLetter(buttonText)}
</Text>
Expand All @@ -92,5 +152,7 @@ export default function PlaySound({ audioFileNames, buttonText }) {

PlaySound.propTypes = {
audioFileNames: PropTypes.any.isRequired,
buttonText: PropTypes.string.isRequired
buttonText: PropTypes.string.isRequired,
onPlayingWord: PropTypes.func,
onFinish: PropTypes.func
}
2 changes: 1 addition & 1 deletion mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "MIT",
"homepage": "https://openarabic.io",
"repository": "https://github.com/edenmind/OpenArabic",
"version": "1445.2.351",
"version": "1445.2.352",
"authors": [
"Yunus Andreasson <yunus@edenmind.com> (https://github.com/YunusAndreasson)"
],
Expand Down
4 changes: 1 addition & 3 deletions mobile/screens/text-bilingual-sentences.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import React from 'react'
import { View } from 'react-native'
import { Divider, useTheme } from 'react-native-paper'
import PropTypes from 'prop-types'
import PlaySound from '../components/play-sound.js'
import { useSharedStyles } from '../styles/common.js'
import { UI } from '../constants/ui.js'
import { EnglishArabic } from '../components/english-arabic.js'

function TextBilingualSentences({ sentences }) {
Expand All @@ -14,7 +12,7 @@ function TextBilingualSentences({ sentences }) {
const renderedSentences = sentences.map((sentence, index) => (
<View key={index} style={[sharedStyle.container, { marginTop: 10, marginBottom: 10 }]}>
<EnglishArabic sentence={sentence} />
<PlaySound audioFileNames={sentence.filename} buttonText={UI.playSentence} />

<Divider style={{ ...sharedStyle.dividerHidden }} />
</View>
))
Expand Down
4 changes: 0 additions & 4 deletions mobile/screens/text-list-card-text.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import React from 'react'
import { useSharedStyles } from '../styles/common.js'
import { prepareIngress } from '../services/utility-service.js'
import SCREENS from '../constants/screens.js'
import { Card, Divider, useTheme, IconButton } from 'react-native-paper'
import PropTypes from 'prop-types'
import { generateShare } from '../services/ui-services.js'
import { CardFooter } from '../components/card-footer.js'
import { PressableCard } from '../components/pressable-card.js'
import { EnglishArabic } from '../components/english-arabic.js'
import TextCategoryIntro from '../components/text-category-intro.js'

export default function TextListCardText({ setShouldReload, navigation, text }) {
const theme = useTheme()
const sharedStyle = useSharedStyles(theme)

const subtitle = `${text.author} in #${text.category}`
const english = text.texts?.english && prepareIngress(text.texts.english, 110)
const arabic = text.texts?.arabic && prepareIngress(text.texts.arabic, 90)

const onPress = () => {
setShouldReload(false)
Expand Down
33 changes: 13 additions & 20 deletions mobile/screens/text-practice.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import WordsContextHighLighted from '../components/context-highlighted.js'
import TextPracticeWords from './text-practice-words.js'
import Spinner from '../components/spinner.js'
import ModalScrollView from '../components/modal-scroll-view.js'
import PlaySound from '../components/play-sound.js'
import WordPairsList from '../components/word-pairs-list.js'
import { EnglishArabic } from '../components/english-arabic.js'
import { Progress } from '../components/progress.js'
import { AnswerButton } from '../components/answer-button.js'
import { ActionButton } from '../components/action-button.js'
import TakbirCelebrate from '../components/takbir-celebrate.js'
import { getThreeRandomWords } from '../services/utility-service.js'
Expand Down Expand Up @@ -146,22 +144,15 @@ const TextPractice = () => {
const sentenceControl = useMemo(
() => (
<View>
<AnswerButton
onPress={() => {
setExplanation(<WordPairsList words={text.sentences[currentSentence].words} />)
setVisible(true)
}}
text="Explain"
/>
<PlaySound audioFileNames={sentencesInText[currentSentence].filename} buttonText="Play" />
<EnglishArabic sentence={text.sentences[currentSentence]} />
{isLastSentence ? (
<ActionButton onPress={handleReset} text="PRACTICE AGAIN" />
) : (
<ActionButton onPress={handleContinue} text="CONTINUE" />
)}
</View>
),
[sentencesInText, currentSentence, isLastSentence, handleContinue, text.sentences]
[text, currentSentence, isLastSentence, handleContinue]
)

return textLoading ? (
Expand All @@ -173,14 +164,16 @@ const TextPractice = () => {
text="Session Completed Successfully!"
/>
<Progress progress={currentSentence / (sentencesInText.length - 1)} />
<Surface style={{ backgroundColor: color, minHeight: 250 }}>
<WordsContextHighLighted
arabicSentence={sentencesInText[currentSentence].arabicWords}
currentWord={currentWord}
arabicWord={currentArabicWord}
sentenceIsComplete={sentenceIsComplete}
/>
</Surface>
{!sentenceIsComplete && (
<Surface style={{ backgroundColor: color, minHeight: 250 }}>
<WordsContextHighLighted
arabicSentence={sentencesInText[currentSentence].arabicWords}
currentWord={currentWord}
arabicWord={currentArabicWord}
sentenceIsComplete={sentenceIsComplete}
/>
</Surface>
)}
{sentenceIsComplete && sentenceControl}
<ModalScrollView
visible={visible}
Expand Down
15 changes: 1 addition & 14 deletions mobile/screens/words-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import TakbirCelebrate from '../components/takbir-celebrate.js'
import { useDispatch, useSelector } from 'react-redux'
import * as Haptics from 'expo-haptics'
import PropTypes from 'prop-types'
import PlaySound from '../components/play-sound.js'
import { generateRandomPositions } from '../services/utility-service.js'
import { Progress } from '../components/progress.js'
import { AnswerButton } from '../components/answer-button.js'
Expand All @@ -35,21 +34,12 @@ const WordsContent = ({
const dispatch = useDispatch()

// Destructure currentWord for cleaner referencing
const { arabic, filename } = words[currentWord] || {}

// Create the URL outside the JSX
const audioURL = `https://openarabic.ams3.digitaloceanspaces.com/audio/${filename}`
const { arabic } = words[currentWord] || {}

// Decide on the font size outside the JSX
const fontSize = arabic?.trim().length > 15 ? 95 : 120

const styles = StyleSheet.create({
bottomRow: {
bottom: 10,
flexDirection: 'row',
position: 'absolute',
right: 0
},
centeredView: {
alignItems: 'center',
flex: 1,
Expand Down Expand Up @@ -175,9 +165,6 @@ const WordsContent = ({
<View style={styles.centeredView}>
<Text style={[styles.text, { fontSize, color: theme.colors.secondary }]}>{arabic?.trim()}</Text>
</View>
<View style={styles.bottomRow}>
<PlaySound mode="text" audioFileNames={[audioURL]} buttonText={'Play'} style={{}} />
</View>
</Surface>
<TakbirCelebrate
visible={celebrationSnackBarVisibility}
Expand Down

0 comments on commit 2a9276e

Please sign in to comment.