Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobile): ✨ Play to read en/ar sentence in component #2258

Merged
merged 1 commit into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading