Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion demos/navigator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ dependencies {
implementation(project(":readium:readium-navigator"))
implementation(project(":readium:navigators:web:readium-navigator-web-reflowable"))
implementation(project(":readium:navigators:web:readium-navigator-web-fixedlayout"))
implementation(project(":readium:adapters:pdfium"))
implementation(project(":readium:navigators:media:readium-navigator-media-readaloud"))
implementation(project(":readium:adapters:exoplayer:readium-adapter-exoplayer-readaloud"))

coreLibraryDesugaring(libs.desugar.jdk.libs)

Expand Down
35 changes: 27 additions & 8 deletions demos/navigator/src/main/java/org/readium/demo/navigator/DemoUi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.core.view.WindowInsetsControllerCompat
import org.readium.demo.navigator.reader.Rendition
import org.readium.demo.navigator.reader.ReadAloudReaderState
import org.readium.demo.navigator.reader.ReadAloudRendition
import org.readium.demo.navigator.reader.SelectNavigatorMenu
import org.readium.demo.navigator.reader.VisualReaderState
import org.readium.demo.navigator.reader.VisualRendition
import org.readium.demo.navigator.util.Fullscreenable

@Composable
Expand All @@ -42,7 +45,6 @@ fun Scaffold(
) {
content.invoke()

LocalTextToolbar
SnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)
Expand All @@ -67,7 +69,11 @@ fun MainContent(
DemoViewModel.State.BookSelection -> true
is DemoViewModel.State.Error -> false
DemoViewModel.State.Loading -> true
is DemoViewModel.State.Reader -> true
is DemoViewModel.State.Reader -> when (viewmodelState.readerState) {
is VisualReaderState<*, *, *, *> -> true
is ReadAloudReaderState -> false
}
is DemoViewModel.State.NavigatorSelection -> true
}
}

Expand All @@ -79,6 +85,10 @@ fun MainContent(
}
}

is DemoViewModel.State.NavigatorSelection -> {
SelectNavigatorMenu(viewmodelState.viewModel)
}

is DemoViewModel.State.Error -> {
Placeholder()
LaunchedEffect(viewmodelState.error) {
Expand All @@ -100,10 +110,19 @@ fun MainContent(
viewmodel.onBookClosed()
}

Rendition(
readerState = viewmodelState.readerState,
fullScreenState = fullscreenState
)
when (viewmodelState.readerState) {
is ReadAloudReaderState -> {
ReadAloudRendition(
readerState = viewmodelState.readerState
)
}
is VisualReaderState<*, *, *, *> -> {
VisualRendition(
readerState = viewmodelState.readerState,
fullScreenState = fullscreenState
)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,39 @@
* available in the top-level LICENSE file of the project.
*/

@file:OptIn(ExperimentalReadiumApi::class)

package org.readium.demo.navigator

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.application
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.readium.adapter.exoplayer.readaloud.ExoPlayerEngineProvider
import org.readium.demo.navigator.reader.ReaderOpener
import org.readium.demo.navigator.reader.ReaderState
import org.readium.demo.navigator.reader.SelectNavigatorItem
import org.readium.demo.navigator.reader.SelectNavigatorViewModel
import org.readium.demo.navigator.reader.fixedConfig
import org.readium.demo.navigator.reader.reflowableConfig
import org.readium.navigator.media.readaloud.ReadAloudNavigatorFactory
import org.readium.navigator.media.readaloud.SystemTtsEngineProvider
import org.readium.navigator.web.fixedlayout.FixedWebRenditionFactory
import org.readium.navigator.web.reflowable.ReflowableWebRenditionFactory
import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.DebugError
import org.readium.r2.shared.util.asset.AssetRetriever
import org.readium.r2.shared.util.getOrElse
import org.readium.r2.shared.util.http.DefaultHttpClient
import org.readium.r2.shared.util.toDebugDescription
import org.readium.r2.streamer.PublicationOpener
import org.readium.r2.streamer.parser.DefaultPublicationParser
import timber.log.Timber

class DemoViewModel(
Expand All @@ -28,6 +48,10 @@ class DemoViewModel(
data object BookSelection :
State

data class NavigatorSelection(
val viewModel: SelectNavigatorViewModel,
) : State

data object Loading :
State

Expand All @@ -36,17 +60,32 @@ class DemoViewModel(
) : State

data class Reader(
val readerState: ReaderState<*, *, *, *>,
val readerState: ReaderState,
) : State
}

init {
Timber.plant(Timber.DebugTree())
}

private val httpClient =
DefaultHttpClient()

private val assetRetriever =
AssetRetriever(application.contentResolver, httpClient)

private val publicationParser =
DefaultPublicationParser(application, httpClient, assetRetriever, null)

private val publicationOpener =
PublicationOpener(publicationParser)

private val readerOpener =
ReaderOpener(application)

private val audioEngineProvider =
ExoPlayerEngineProvider(application)

private val stateMutable: MutableStateFlow<State> =
MutableStateFlow(State.BookSelection)

Expand All @@ -56,7 +95,83 @@ class DemoViewModel(
stateMutable.value = State.Loading

viewModelScope.launch {
readerOpener.open(url)
val asset = assetRetriever.retrieve(url)
.getOrElse {
Timber.d(it.toDebugDescription())
stateMutable.value = State.Error(it)
return@launch
}

val publication = publicationOpener.open(asset, allowUserInteraction = false)
.getOrElse {
asset.close()
Timber.d(it.toDebugDescription())
stateMutable.value = State.Error(it)
return@launch
}

val reflowableFactory =
ReflowableWebRenditionFactory(
application = application,
publication = publication,
configuration = reflowableConfig
)?.let { SelectNavigatorItem.ReflowableWeb(it) }

val fixedFactory =
FixedWebRenditionFactory(
application = application,
publication = publication,
configuration = fixedConfig
)?.let { SelectNavigatorItem.FixedWeb(it) }

val ttsEngineProvider =
SystemTtsEngineProvider(application)

val readAloudFactory = ReadAloudNavigatorFactory.invoke(
application = application,
publication = publication,
audioEngineProvider = audioEngineProvider,
ttsEngineProvider = ttsEngineProvider
)?.let { SelectNavigatorItem.ReadAloud(it) }

val factories = listOfNotNull(
reflowableFactory,
fixedFactory,
readAloudFactory
)

when (factories.size) {
0 -> {
val error = DebugError("Publication not supported")
Timber.d(error.toDebugDescription())
stateMutable.value = State.Error(error)
}
1 -> {
onNavigatorSelected(url, publication, factories.first())
}
else -> {
val selectionViewModel = SelectNavigatorViewModel(
items = factories,
onItemSelected = { onNavigatorSelected(url, publication, it) },
onMenuDismissed = { stateMutable.value = State.BookSelection }
)

stateMutable.value =
State.NavigatorSelection(selectionViewModel)
}
}
}
}

fun onNavigatorSelected(
url: AbsoluteUrl,
publication: Publication,
navigatorItem: SelectNavigatorItem,
) {
stateMutable.value = State.Loading

viewModelScope.launch {
readerOpener.open(url, publication, navigatorItem)
.onFailure {
Timber.d(it.toDebugDescription())
stateMutable.value = State.Error(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
package org.readium.demo.navigator.preferences

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.readium.navigator.common.Preferences
import org.readium.r2.shared.ExperimentalReadiumApi

Expand All @@ -21,6 +23,9 @@ class PreferencesManager<P : Preferences<P>>(
private val preferencesMutable: MutableStateFlow<P> =
MutableStateFlow(initialPreferences)

val preferences: StateFlow<P> =
preferencesMutable.asStateFlow()

fun setPreferences(preferences: P) {
preferencesMutable.value = preferences
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2023 Readium Foundation. All rights reserved.
* Use of this source code is governed by the BSD-style license
* available in the top-level LICENSE file of the project.
*/

package org.readium.demo.navigator.preferences

import kotlinx.coroutines.flow.StateFlow
import org.readium.navigator.common.PreferencesEditor
import org.readium.navigator.media.readaloud.SystemTtsEngine
import org.readium.navigator.media.readaloud.preferences.ReadAloudPreferences
import org.readium.navigator.media.readaloud.preferences.ReadAloudSettings
import org.readium.r2.navigator.preferences.EnumPreference
import org.readium.r2.navigator.preferences.Preference
import org.readium.r2.navigator.preferences.RangePreference
import org.readium.r2.navigator.preferences.map
import org.readium.r2.navigator.preferences.withSupportedValues
import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.util.Language

@OptIn(ExperimentalReadiumApi::class)
class ReadAloudPreferencesEditor(
private val editor: org.readium.navigator.media.readaloud.preferences.ReadAloudPreferencesEditor,
private val availableVoices: Set<SystemTtsEngine.Voice>,
) : PreferencesEditor<ReadAloudPreferences, ReadAloudSettings> {

override val preferences: ReadAloudPreferences
get() = editor.preferences

val preferencesState: StateFlow<ReadAloudPreferences> =
editor.preferencesState

override val settings: ReadAloudSettings
get() = editor.settings

override fun clear() {
editor.clear()
}

val language: Preference<Language?> =
editor.language

/**
* [ReadAloudPreferencesEditor] supports choosing voices for any language or region.
* For this test app, we've chosen to present to the user only the voice for the
* TTS default language and to ignore regions.
*/
val voice: EnumPreference<SystemTtsEngine.Voice?> = run {
val currentLanguage = language.effectiveValue?.removeRegion()

editor.voices.map(
from = { voiceIds ->
currentLanguage
?.let { voiceIds[it] }
?.let { voiceId -> availableVoices.firstOrNull { it.id == voiceId } }
},
to = { voice ->
currentLanguage
?.let { editor.voices.value.orEmpty().update(it, voice?.id) }
?: editor.voices.value.orEmpty()
}
).withSupportedValues(
availableVoices
.filter { voice -> currentLanguage in voice.languages.map { it.removeRegion() } }
)
}
val pitch: RangePreference<Double> =
editor.pitch

val speed: RangePreference<Double> =
editor.speed

val readContinuously: Preference<Boolean> =
editor.readContinuously

private fun <K, V> Map<K, V>.update(key: K, value: V?): Map<K, V> =
buildMap {
putAll(this@update)
if (value == null) {
remove(key)
} else {
put(key, value)
}
}
}
Loading