diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index 72fe03bc2..f012f6445 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -2,7 +2,7 @@ object ProjectConfig { const val minSdk = 23 const val targetSdk = 31 const val compileSdk = 31 - const val versionName = "0.1.17" - const val versionCode = 18 + const val versionName = "0.1.18" + const val versionCode = 19 const val applicationId = "ir.kazemcodes.infinityreader" } \ No newline at end of file diff --git a/core-ui/src/main/java/org/ireader/core_ui/theme/AppPreferences.kt b/core-ui/src/main/java/org/ireader/core_ui/theme/AppPreferences.kt index 02c4cd0b3..abed6eba7 100644 --- a/core-ui/src/main/java/org/ireader/core_ui/theme/AppPreferences.kt +++ b/core-ui/src/main/java/org/ireader/core_ui/theme/AppPreferences.kt @@ -32,6 +32,12 @@ class AppPreferences @Inject constructor( const val SCROLL_INDICATOR_PADDING = "scroll_indicator_padding" const val SCROLL_INDICATOR_WIDTH = "scroll_indicator_width" + + const val TEXT_READER_SPEECH_RATE = "text_reader_speech_rate" + const val TEXT_READER_SPEECH_PITCH = "text_reader_speech_pitch" + const val TEXT_READER_SPEECH_LANGUAGE = "text_reader_speech_language" + const val TEXT_READER_SPEECH_VOICE = "text_reader_speech_voice" + /** Services **/ const val Last_UPDATE_CHECK = "last_update_check" @@ -126,6 +132,21 @@ class AppPreferences @Inject constructor( return preferenceStore.getBoolean(SORT_DESC_LIBRARY_SCREEN, true) } + fun speechRate(): Preference { + return preferenceStore.getFloat(TEXT_READER_SPEECH_RATE, .8f) + } + + fun speechPitch(): Preference { + return preferenceStore.getFloat(TEXT_READER_SPEECH_PITCH, .8f) + } + + fun speechVoice(): Preference { + return preferenceStore.getString(TEXT_READER_SPEECH_VOICE, "") + } + + fun speechLanguage(): Preference { + return preferenceStore.getString(TEXT_READER_SPEECH_LANGUAGE, "") + } fun lastUpdateCheck(): Preference { return preferenceStore.getLong(Last_UPDATE_CHECK, 0) diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 295ac5ec3..3e2d64d9e 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -54,6 +54,7 @@ Crash logs restoring from Backup Checking Library + Text Reader Recent Updates Most Popular Latest diff --git a/domain/src/main/java/org/ireader/domain/feature_services/notification/Notification.kt b/domain/src/main/java/org/ireader/domain/feature_services/notification/Notification.kt index bf9ffdf75..e328a42f1 100644 --- a/domain/src/main/java/org/ireader/domain/feature_services/notification/Notification.kt +++ b/domain/src/main/java/org/ireader/domain/feature_services/notification/Notification.kt @@ -28,6 +28,14 @@ object Notifications { const val CHANNEL_LIBRARY_ERROR = "library_errors_channel" const val ID_LIBRARY_ERROR = -102 + + private const val GROUP_TEXT_READER = "group_text_reader" + const val CHANNEL_TEXT_READER_PROGRESS = "library_text_reader_channel" + const val ID_TEXT_READER_PROGRESS = -601 + const val CHANNEL_TEXT_READER_ERROR = "library_text_reader_error_channel" + const val ID_TEXT_READER_ERROR = -602 + + /** * Notification channel and ids used by the downloader. */ @@ -110,6 +118,9 @@ object Notifications { buildNotificationChannelGroup(GROUP_LIBRARY) { setName(context.getString(R.string.label_library)) }, + buildNotificationChannelGroup(GROUP_TEXT_READER) { + setName(context.getString(R.string.label_text_reader)) + }, buildNotificationChannelGroup(GROUP_APK_UPDATES) { setName(context.getString(R.string.label_recent_updates)) }, @@ -121,6 +132,16 @@ object Notifications { buildNotificationChannel(CHANNEL_COMMON, IMPORTANCE_LOW) { setName(context.getString(R.string.channel_common)) }, + buildNotificationChannel(CHANNEL_TEXT_READER_PROGRESS, IMPORTANCE_LOW) { + setName(context.getString(R.string.channel_progress)) + setGroup(GROUP_TEXT_READER) + setShowBadge(false) + }, + buildNotificationChannel(CHANNEL_TEXT_READER_ERROR, IMPORTANCE_LOW) { + setName(context.getString(R.string.channel_errors)) + setGroup(GROUP_TEXT_READER) + setShowBadge(false) + }, buildNotificationChannel(CHANNEL_LIBRARY_PROGRESS, IMPORTANCE_LOW) { setName(context.getString(R.string.channel_progress)) setGroup(GROUP_LIBRARY) @@ -131,6 +152,7 @@ object Notifications { setGroup(GROUP_LIBRARY) setShowBadge(false) }, + buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) { setName(context.getString(R.string.channel_new_chapters)) }, diff --git a/domain/src/main/java/org/ireader/domain/use_cases/preferences/reader_preferences/OrientationUseCase.kt b/domain/src/main/java/org/ireader/domain/use_cases/preferences/reader_preferences/OrientationUseCase.kt index 413e7c323..cf0618b4e 100644 --- a/domain/src/main/java/org/ireader/domain/use_cases/preferences/reader_preferences/OrientationUseCase.kt +++ b/domain/src/main/java/org/ireader/domain/use_cases/preferences/reader_preferences/OrientationUseCase.kt @@ -88,3 +88,39 @@ fun mapFilterType(input: Int): FilterType { } +class TextReaderPrefUseCase @Inject constructor( + private val appPreferences: AppPreferences, +) { + fun savePitch(value: Float) { + appPreferences.speechRate().set(value) + } + + fun readPitch(): Float { + return appPreferences.speechRate().get() + } + + fun saveRate(value: Float) { + appPreferences.speechPitch().set(value) + } + + fun readRate(): Float { + return appPreferences.speechPitch().get() + } + + fun saveLanguage(value: String) { + appPreferences.speechLanguage().set(value) + } + + fun readLanguage(): String { + return appPreferences.speechLanguage().get() + } + + fun saveVoice(value: String) { + appPreferences.speechVoice().set(value) + } + + fun readVoice(): String { + return appPreferences.speechVoice().get() + } + +} \ No newline at end of file diff --git a/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/components/FontChip.kt b/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/components/FontChip.kt index faadeb2d0..6cb3e3de0 100644 --- a/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/components/FontChip.kt +++ b/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/components/FontChip.kt @@ -31,7 +31,9 @@ fun FontChip( viewModel: ReaderScreenViewModel, ) { - Row(modifier = Modifier.fillMaxWidth(), + Row(modifier = Modifier + .fillMaxWidth() + .height(32.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Text( diff --git a/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/viewmodel/ReaderScreenState.kt b/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/viewmodel/ReaderScreenState.kt index c19a4140c..51d54db6c 100644 --- a/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/viewmodel/ReaderScreenState.kt +++ b/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/viewmodel/ReaderScreenState.kt @@ -1,6 +1,7 @@ package org.ireader.presentation.feature_reader.presentation.reader.viewmodel +import android.speech.tts.Voice import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -11,6 +12,7 @@ import org.ireader.core_ui.theme.FontType import org.ireader.domain.models.entities.Book import org.ireader.domain.models.entities.Chapter import tachiyomi.source.Source +import java.util.* import javax.inject.Inject //data class ReaderScreenState( @@ -51,14 +53,40 @@ open class ReaderScreenStateImpl @Inject constructor() : ReaderScreenState { override var stateChapters: List by mutableStateOf>(emptyList()) override var stateChapter: Chapter? by mutableStateOf(null) override var book: Book? by mutableStateOf(null) + + +} + + +interface TextReaderScreenState { + var currentReadingParagraph: Int + var isPlaying: Boolean + var voiceMode: Boolean + var autoNextChapter: Boolean + var languages: List + var voices: List + var currentVoice: String + var currentLanguage: String + var pitch: Float + var speechSpeed: Float +} + +class TextReaderScreenStateImpl @Inject constructor() : TextReaderScreenState { override var currentReadingParagraph: Int by mutableStateOf(0) + override var languages by mutableStateOf>(emptyList()) + override var voices by mutableStateOf>(emptyList()) + + override var currentVoice by mutableStateOf("") + override var currentLanguage by mutableStateOf("") override var isPlaying by mutableStateOf(false) override var voiceMode by mutableStateOf(false) - - + override var autoNextChapter by mutableStateOf(false) + override var pitch by mutableStateOf(.8f) + override var speechSpeed by mutableStateOf(.8f) } + interface ReaderScreenState { var isLoading: Boolean var isRemoteLoading: Boolean @@ -76,9 +104,6 @@ interface ReaderScreenState { var stateChapters: List var stateChapter: Chapter? var book: Book? - var currentReadingParagraph: Int - var isPlaying: Boolean - var voiceMode: Boolean } diff --git a/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/viewmodel/ReaderScreenViewModel.kt b/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/viewmodel/ReaderScreenViewModel.kt index c74484e6c..fce9c13de 100644 --- a/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/viewmodel/ReaderScreenViewModel.kt +++ b/presentation/src/main/java/org/ireader/presentation/feature_reader/presentation/reader/viewmodel/ReaderScreenViewModel.kt @@ -10,6 +10,8 @@ import android.view.WindowManager import androidx.compose.foundation.lazy.LazyListState import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat @@ -28,6 +30,7 @@ import org.ireader.core_ui.theme.readerScreenBackgroundColors import org.ireader.core_ui.viewmodel.BaseViewModel import org.ireader.domain.R import org.ireader.domain.catalog.service.CatalogStore +import org.ireader.domain.feature_services.notification.Notifications import org.ireader.domain.models.entities.Book import org.ireader.domain.models.entities.Chapter import org.ireader.domain.models.entities.History @@ -36,11 +39,11 @@ import org.ireader.domain.use_cases.history.HistoryUseCase import org.ireader.domain.use_cases.local.LocalGetChapterUseCase import org.ireader.domain.use_cases.local.LocalInsertUseCases import org.ireader.domain.use_cases.preferences.reader_preferences.ReaderPrefUseCases +import org.ireader.domain.use_cases.preferences.reader_preferences.TextReaderPrefUseCase import org.ireader.domain.use_cases.remote.RemoteUseCases import org.ireader.domain.utils.withIOContext import tachiyomi.source.Source import timber.log.Timber -import java.util.* import javax.inject.Inject @@ -55,8 +58,11 @@ class ReaderScreenViewModel @Inject constructor( private val readerUseCases: ReaderPrefUseCases, private val prefState: ReaderScreenPreferencesStateImpl, private val state: ReaderScreenStateImpl, + private val speakerState: TextReaderScreenStateImpl, + val speechPrefUseCases: TextReaderPrefUseCase, savedStateHandle: SavedStateHandle, -) : BaseViewModel(), ReaderScreenPreferencesState by prefState, ReaderScreenState by state { +) : BaseViewModel(), ReaderScreenPreferencesState by prefState, ReaderScreenState by state, + TextReaderScreenState by speakerState { @@ -105,6 +111,10 @@ class ReaderScreenViewModel @Inject constructor( autoScrollInterval = readerUseCases.autoScrollMode.readInterval() autoScrollOffset = readerUseCases.autoScrollMode.readOffset() autoBrightnessMode = readerUseCases.brightnessStateUseCase.readAutoBrightness() + speechSpeed = speechPrefUseCases.readRate() + pitch = speechPrefUseCases.readPitch() + currentVoice = speechPrefUseCases.readVoice() + currentLanguage = speechPrefUseCases.readLanguage() } fun onEvent(event: ReaderEvent) { @@ -133,7 +143,7 @@ class ReaderScreenViewModel @Inject constructor( toggleLoading(true) toggleLocalLoaded(false) viewModelScope.launch { - speaker?.stop() + onNextVoice() val resultChapter = getChapterUseCase.findChapterById( chapterId = chapterId, state.book?.id, @@ -636,12 +646,59 @@ class ReaderScreenViewModel @Inject constructor( var speaker: TextToSpeech? = null + fun onNextVoice() { + speaker?.stop() + isPlaying = false + currentReadingParagraph = 0 + } + + fun showTextReaderNotification(context: Context) { + stateChapter?.let { chapter -> + val builder = + NotificationCompat.Builder(context, + Notifications.CHANNEL_TEXT_READER_PROGRESS).apply { + setContentTitle(chapter.title) + setSmallIcon(org.ireader.core.R.drawable.ic_infinity) + setOnlyAlertOnce(true) + priority = NotificationCompat.PRIORITY_LOW + setAutoCancel(true) + setOngoing(true) + setShowWhen(isPlaying) + } + NotificationManagerCompat.from(context).apply { + builder.setProgress(chapter.content.size, 0, false) + notify(Notifications.ID_TEXT_READER_PROGRESS, builder.build()) + cancel(Notifications.ID_TEXT_READER_PROGRESS) + + } + + } + + } fun readText(context: Context) { speaker = TextToSpeech(context) { status -> + if (status != TextToSpeech.ERROR && speaker != null) { speaker?.let { ttl -> - ttl.language = Locale.UK + ttl.availableLanguages?.let { + languages = it.toList() + } + ttl.voices?.toList()?.let { + voices = it + } + ttl.voices?.firstOrNull { it.name == currentVoice }?.let { + ttl.voice = it + } + ttl.availableLanguages?.firstOrNull { it.displayName == currentLanguage } + ?.let { + ttl.language = it + } + + ttl.setPitch(pitch) + ttl.setSpeechRate(speechSpeed) + + stateChapter?.let { chapter -> try { ttl.speak(chapter.content[currentReadingParagraph], @@ -653,19 +710,36 @@ class ReaderScreenViewModel @Inject constructor( UtteranceProgressListener() { override fun onStop(utteranceId: String?, interrupted: Boolean) { super.onStop(utteranceId, interrupted) - Timber.e("onStop....") isPlaying = false } override fun onStart(p0: String?) { - Timber.e("OnStart....") + // showTextReaderNotification(context) isPlaying = true } override fun onDone(p0: String?) { - currentReadingParagraph += 1 - Timber.e(currentReadingParagraph.toString()) - readText(context) + if (currentReadingParagraph < chapter.content.size) { + currentReadingParagraph += 1 + readText(context) + } + if (currentReadingParagraph == chapter.content.size - 1) { + isPlaying = false + if (autoNextChapter) { + source?.let { + updateChapterSliderIndex(currentChapterIndex + 1) + viewModelScope.launch { + getChapter(getCurrentChapterByIndex().id, + source = it) + isPlaying = true + readText(context = context) + } + + } + + + } + } } override fun onError(p0: String?) { diff --git a/presentation/src/main/java/org/ireader/presentation/feature_ttl/TTSScreen.kt b/presentation/src/main/java/org/ireader/presentation/feature_ttl/TTSScreen.kt index 9ee901cc8..f7ac1d121 100644 --- a/presentation/src/main/java/org/ireader/presentation/feature_ttl/TTSScreen.kt +++ b/presentation/src/main/java/org/ireader/presentation/feature_ttl/TTSScreen.kt @@ -1,5 +1,6 @@ package org.ireader.presentation.feature_ttl +import androidx.activity.OnBackPressedDispatcher import androidx.compose.animation.core.TweenSpec import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -16,6 +17,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -26,6 +28,8 @@ import org.ireader.core.R import org.ireader.domain.feature_services.io.BookCover import org.ireader.domain.models.entities.Chapter import org.ireader.presentation.feature_reader.presentation.reader.ReaderScreenDrawer +import org.ireader.presentation.feature_reader.presentation.reader.components.SettingItemComposable +import org.ireader.presentation.feature_reader.presentation.reader.components.SettingItemToggleComposable import org.ireader.presentation.feature_reader.presentation.reader.viewmodel.ReaderScreenViewModel import org.ireader.presentation.presentation.Toolbar import org.ireader.presentation.presentation.components.BookImageComposable @@ -33,8 +37,9 @@ import org.ireader.presentation.presentation.components.showLoading import org.ireader.presentation.presentation.reusable_composable.AppIconButton import org.ireader.presentation.presentation.reusable_composable.BigSizeTextComposable import org.ireader.presentation.presentation.reusable_composable.MidSizeTextComposable -import org.ireader.presentation.presentation.reusable_composable.TopAppBarBackButton +import org.ireader.presentation.presentation.reusable_composable.SuperSmallTextComposable import tachiyomi.source.Source +import java.math.RoundingMode @OptIn(ExperimentalMaterialApi::class) @@ -51,123 +56,199 @@ fun TTSScreen( source: Source, onChapter: (Chapter) -> Unit, ) { + navController.setOnBackPressedDispatcher(OnBackPressedDispatcher { + + }) + val context = LocalContext.current + val drawerScrollState = rememberLazyListState() val scrollState = rememberLazyListState() val chapter = vm.stateChapter val chapters = vm.stateChapters val scope = rememberCoroutineScope() val scaffoldState = rememberScaffoldState() + val bottomSheetState = + rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) LaunchedEffect(key1 = scaffoldState.drawerState.targetValue) { if (chapter != null && scaffoldState.drawerState.targetValue == DrawerValue.Open && vm.stateChapters.isNotEmpty()) { drawerScrollState.scrollToItem(vm.getCurrentIndexOfChapter(chapter)) } } - Scaffold( - modifier = Modifier - .fillMaxSize(), + ModalBottomSheetLayout( + modifier = Modifier.systemBarsPadding(), + sheetContent = { + Box(modifier.defaultMinSize(minHeight = 1.dp)) { + Column(modifier = Modifier + .fillMaxSize() + .padding(16.dp)) { + VoiceChip(viewModel = vm, modifier = Modifier.height(32.dp)) + LanguageChip(viewModel = vm, modifier = Modifier.height(32.dp)) + SettingItemToggleComposable(text = "Auto Next Chapter", + value = vm.autoNextChapter, + onToggle = { vm.autoNextChapter = !vm.autoNextChapter }) + SettingItemComposable(text = "Speech Rate", + value = vm.speechSpeed.toString(), + onAdd = { + vm.speechSpeed += .1f + vm.speechSpeed = + vm.speechSpeed.toBigDecimal().setScale(1, RoundingMode.FLOOR) + .toFloat() + vm.speechPrefUseCases.saveRate(vm.speechSpeed) + }, + onMinus = { + vm.speechSpeed -= .1f + vm.speechSpeed = + vm.speechSpeed.toBigDecimal().setScale(1, RoundingMode.FLOOR) + .toFloat() + vm.speechPrefUseCases.saveRate(vm.speechSpeed) + }) + + SettingItemComposable(text = "Pitch", + value = vm.pitch.toString(), + onAdd = { + vm.pitch += .1f + vm.pitch = + vm.pitch.toBigDecimal().setScale(1, RoundingMode.FLOOR).toFloat() + vm.speechPrefUseCases.savePitch(vm.speechSpeed) + }, + onMinus = { + vm.pitch -= .1f + vm.pitch = + vm.pitch.toBigDecimal().setScale(1, RoundingMode.FLOOR).toFloat() + vm.speechPrefUseCases.savePitch(vm.speechSpeed) + + }) - scaffoldState = scaffoldState, - topBar = { - Toolbar( - title = {}, - navigationIcon = { TopAppBarBackButton(navController = navController) } - ) + } + } }, - drawerGesturesEnabled = true, - drawerBackgroundColor = MaterialTheme.colors.background, - drawerContent = { - ReaderScreenDrawer( - modifier = Modifier.statusBarsPadding(), - onReverseIcon = { - if (chapter != null) { - vm.reverseChapters() - scope.launch { - vm.getLocalChaptersByPaging(chapter.bookId) + sheetState = bottomSheetState, + sheetBackgroundColor = MaterialTheme.colors.background, + sheetContentColor = MaterialTheme.colors.onBackground, + ) { + Scaffold( + modifier = Modifier + .fillMaxSize(), + + scaffoldState = scaffoldState, + topBar = { + Toolbar( + title = {}, + navigationIcon = { + AppIconButton(imageVector = Icons.Default.ArrowBack, + title = "Return to Reader Screen", + onClick = { + vm.voiceMode = false + }) + }, + ) + }, + drawerGesturesEnabled = true, + drawerBackgroundColor = MaterialTheme.colors.background, + drawerContent = { + ReaderScreenDrawer( + modifier = Modifier.statusBarsPadding(), + onReverseIcon = { + if (chapter != null) { + vm.reverseChapters() + scope.launch { + vm.getLocalChaptersByPaging(chapter.bookId) + } } - } - }, - onChapter = onChapter, - chapter = chapter, - source = source, - chapters = chapters, - drawerScrollState = drawerScrollState - ) + }, + onChapter = onChapter, + chapter = chapter, + source = source, + chapters = chapters, + drawerScrollState = drawerScrollState + ) - } - ) { - Column(modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally + } ) { - vm.book?.let { book -> - vm.stateChapter?.let { chapter -> - Column(modifier = Modifier - .fillMaxWidth(), - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally) { - BookImageComposable( - image = BookCover.from(book), - modifier = Modifier - .padding(8.dp) - .height(150.dp) - .width(120.dp) - .clip(MaterialTheme.shapes.medium) - .border(2.dp, MaterialTheme.colors.onBackground.copy(alpha = .2f)), - contentScale = ContentScale.Crop, - ) + Column(modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally + ) { + vm.book?.let { book -> + vm.stateChapter?.let { chapter -> + Column(modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally) { + BookImageComposable( + image = BookCover.from(book), + modifier = Modifier + .padding(8.dp) + .height(150.dp) + .width(120.dp) + .clip(MaterialTheme.shapes.medium) + .border(2.dp, + MaterialTheme.colors.onBackground.copy(alpha = .2f)), + contentScale = ContentScale.Crop, + ) - BigSizeTextComposable(text = chapter.title, align = TextAlign.Center) + BigSizeTextComposable(text = chapter.title, align = TextAlign.Center) + MidSizeTextComposable(text = book.title, align = TextAlign.Center) + vm.stateChapter?.let { chapter -> + SuperSmallTextComposable(text = "${vm.currentReadingParagraph}/${chapter.content.size - 1}") - MidSizeTextComposable(text = book.title, align = TextAlign.Center) - } - Text( - modifier = modifier - - .fillMaxWidth() - .padding(horizontal = vm.paragraphsIndent.dp, - vertical = 4.dp), - text = chapter.content[vm.currentReadingParagraph], - fontSize = vm.fontSize.sp, - fontFamily = vm.font.fontFamily, - textAlign = TextAlign.Start, - color = MaterialTheme.colors.onBackground, - lineHeight = vm.lineHeight.sp, - ) - Column(modifier = Modifier - .fillMaxWidth(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally) { - TTLScreenSetting( - onSetting = {}, - onContent = { - scope.launch { - scaffoldState.drawerState.animateTo(DrawerValue.Open, - TweenSpec()) - } } + } + Text( + modifier = modifier + + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .padding(horizontal = vm.paragraphsIndent.dp, + vertical = 4.dp), + text = if (chapter.content.isNotEmpty() && vm.currentReadingParagraph != chapter.content.size) chapter.content[vm.currentReadingParagraph] else "", + fontSize = vm.fontSize.sp, + fontFamily = vm.font.fontFamily, + textAlign = TextAlign.Start, + color = MaterialTheme.colors.onBackground, + lineHeight = vm.lineHeight.sp, + maxLines = 12, ) - TTLScreenPlay( - modifier = Modifier.padding(bottom = 46.dp, top = 32.dp), - onPlay = onPlay, - onNext = onNext, - onPrev = onPrev, - vm = vm, - onNextPar = onNextPar, - onPrevPar = onPrevPar - ) - } + Column(modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally) { + TTLScreenSetting( + onSetting = { + scope.launch { + bottomSheetState.show() + } + }, + onContent = { + scope.launch { + scaffoldState.drawerState.animateTo(DrawerValue.Open, + TweenSpec()) + } + } + ) + TTLScreenPlay( + modifier = Modifier.padding(bottom = 46.dp, top = 32.dp), + onPlay = onPlay, + onNext = onNext, + onPrev = onPrev, + vm = vm, + onNextPar = onNextPar, + onPrevPar = onPrevPar + ) + } + } } + } - } + } } - } @@ -249,7 +330,7 @@ private fun TTLScreenPlay( } vm.isPlaying -> { AppIconButton(modifier = Modifier.size(80.dp), - imageVector = Icons.Filled.Stop, + imageVector = Icons.Filled.Pause, title = "Play", onClick = onPlay, tint = MaterialTheme.colors.onBackground) @@ -276,6 +357,7 @@ private fun TTLScreenPlay( onClick = onNext, tint = MaterialTheme.colors.onBackground) + } } } diff --git a/presentation/src/main/java/org/ireader/presentation/feature_ttl/VoiceChip.kt b/presentation/src/main/java/org/ireader/presentation/feature_ttl/VoiceChip.kt new file mode 100644 index 000000000..4cdac541f --- /dev/null +++ b/presentation/src/main/java/org/ireader/presentation/feature_ttl/VoiceChip.kt @@ -0,0 +1,128 @@ +package org.ireader.presentation.feature_ttl + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.ireader.presentation.feature_reader.presentation.reader.viewmodel.ReaderScreenViewModel +import org.ireader.presentation.presentation.reusable_composable.CaptionTextComposable + + +@Composable +fun VoiceChip( + modifier: Modifier = Modifier, + viewModel: ReaderScreenViewModel, +) { + + Row(modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier + .width(100.dp) + .height(50.dp), + text = "Voices", + fontSize = 12.sp, + style = TextStyle(fontWeight = FontWeight.W400) + ) + + LazyRow { + items(count = viewModel.voices.size) { index -> + viewModel.voices.filter { !it.isNetworkConnectionRequired }.let { voices -> + Spacer(modifier = modifier.width(10.dp)) + Box(modifier = modifier + .height(20.dp) + .clip(RectangleShape) + .background(MaterialTheme.colors.background) + .border(2.dp, + if (voices[index].name == viewModel.currentVoice) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground.copy( + .4f), + CircleShape) + .clickable { + viewModel.currentVoice = voices[index].name + viewModel.speechPrefUseCases.saveVoice(voices[index].name) + }, + contentAlignment = Alignment.Center + ) { + CaptionTextComposable(text = voices[index].name, + maxLine = 1, + align = TextAlign.Center, + modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)) + } + } + } + + } + + } + + +} + +@Composable +fun LanguageChip( + modifier: Modifier = Modifier, + viewModel: ReaderScreenViewModel, +) { + val context = LocalContext.current + Row(modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier + .width(100.dp) + .height(50.dp), + text = "Languages", + fontSize = 12.sp, + style = TextStyle(fontWeight = FontWeight.W400) + ) + + LazyRow { + items(count = viewModel.languages.size) { index -> + viewModel.languages.sortedBy { it.displayName }.let { language -> + Spacer(modifier = modifier.width(10.dp)) + Box(modifier = modifier + .height(50.dp) + .clip(RectangleShape) + .background(MaterialTheme.colors.background) + .border(2.dp, + if (language[index].displayName == viewModel.currentLanguage) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground.copy( + .4f), + CircleShape) + .clickable { + viewModel.currentLanguage = language[index].displayName + viewModel.speechPrefUseCases.saveLanguage(language[index].displayName) + }, + contentAlignment = Alignment.Center + ) { + CaptionTextComposable(text = language[index].displayName, + maxLine = 1, + align = TextAlign.Center, + modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)) + } + } + } + + } + + } + + +} diff --git a/presentation/src/main/java/org/ireader/presentation/presentation/components/ChapterItemListComposable.kt b/presentation/src/main/java/org/ireader/presentation/presentation/components/ChapterItemListComposable.kt index 12c012b93..1a2357798 100644 --- a/presentation/src/main/java/org/ireader/presentation/presentation/components/ChapterItemListComposable.kt +++ b/presentation/src/main/java/org/ireader/presentation/presentation/components/ChapterItemListComposable.kt @@ -23,6 +23,7 @@ fun ChapterListItemComposable( onLongClick: () -> Unit = {}, isLastRead: Boolean = false, isSelected: Boolean = false, + isLoading: Boolean = false, ) { ListItem( modifier = modifier @@ -55,6 +56,9 @@ fun ChapterListItemComposable( ) }, trailing = { + if (isLoading) { + showLoading() + } if (chapter.content.joinToString(" , ").length > 10) { Icon( imageVector = Icons.Default.PublishedWithChanges, diff --git a/presentation/src/main/java/org/ireader/presentation/ui/ReaderScreenSpec.kt b/presentation/src/main/java/org/ireader/presentation/ui/ReaderScreenSpec.kt index 2d966e89c..883397517 100644 --- a/presentation/src/main/java/org/ireader/presentation/ui/ReaderScreenSpec.kt +++ b/presentation/src/main/java/org/ireader/presentation/ui/ReaderScreenSpec.kt @@ -81,7 +81,7 @@ object ReaderScreenSpec : ScreenSpec { }, onPlay = { when { - vm.speaker?.isSpeaking == true -> { + vm.isPlaying -> { vm.speaker?.stop() } else -> {