Skip to content

Implement model and SMIL parsing for Guided Navigation #700

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

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
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
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,38 @@
* 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.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 +47,10 @@ class DemoViewModel(
data object BookSelection :
State

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

data object Loading :
State

Expand All @@ -36,17 +59,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 +94,80 @@ 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 readAloudFactory =
ReadAloudNavigatorFactory(
application = application,
publication = publication,
audioEngineProvider = audioEngineProvider
)?.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
@@ -0,0 +1,86 @@
/*
* Copyright 2024 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.
*/

@file:OptIn(ExperimentalReadiumApi::class)

package org.readium.demo.navigator.reader

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowOutward
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import org.readium.r2.shared.ExperimentalReadiumApi

@Composable
fun ReadAloudRendition(
readerState: ReadAloudReaderState,
) {
Scaffold(
modifier = Modifier.fillMaxSize()
) { contentPadding ->
Column(
modifier = Modifier
.padding(contentPadding)
.fillMaxSize(),
verticalArrangement = Arrangement.Bottom
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
onClick = { readerState.navigator.skip(force = true) }
) {
Icon(
imageVector = Icons.Default.SkipPrevious,
contentDescription = "Skip to previous"
)
}

IconButton(
onClick = { readerState.navigator.pause() }
) {
Icon(
imageVector = Icons.Default.Pause,
contentDescription = "Pause"
)
}

IconButton(
onClick = { readerState.navigator.skip(force = true) }
) {
Icon(
imageVector = Icons.Default.SkipNext,
contentDescription = "Skip to next"
)
}

IconButton(
onClick = { readerState.navigator.escape(force = true) }
) {
Icon(
imageVector = Icons.Default.ArrowOutward,
contentDescription = "Escape"
)
}
}
}
}
}
Loading