Skip to content

Commit

Permalink
Add composable pdf navigator and refactor demo
Browse files Browse the repository at this point in the history
  • Loading branch information
qnga committed Oct 14, 2024
1 parent 1c68458 commit b39036d
Show file tree
Hide file tree
Showing 26 changed files with 792 additions and 148 deletions.
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ androidx-core = "1.13.1"
androidx-datastore = "1.1.1"
androidx-expresso-core = "3.5.1"
androidx-ext-junit = "1.1.5"
androidx-fragment-ktx = "1.7.1"
androidx-fragment = "1.8.4"
androidx-legacy = "1.0.0"
androidx-lifecycle = "2.8.0"
androidx-lifecycle-extensions = "2.2.0"
Expand Down Expand Up @@ -93,7 +93,8 @@ androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "and
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidx-datastore" }
androidx-expresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-expresso-core" }
androidx-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-ext-junit" }
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "androidx-fragment-ktx" }
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "androidx-fragment" }
androidx-fragment-compose = { group = "androidx.fragment", name = "fragment-compose", version.ref = "androidx-fragment" }
androidx-legacy-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "androidx-legacy" }
androidx-legacy-ui = { group = "androidx.legacy", name = "legacy-support-core-ui", version.ref = "androidx-legacy" }
androidx-lifecycle-common = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "androidx-lifecycle" }
Expand Down
31 changes: 31 additions & 0 deletions readium/navigators/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2022 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.
*/

plugins {
id("readium.library-conventions")
alias(libs.plugins.kotlin.serialization)
}

android {
namespace = "org.readium.navigators.common"

composeOptions {
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
}
buildFeatures {
compose = true
}
}

dependencies {
api(project(":readium:readium-shared"))
api(project(":readium:readium-navigator"))

implementation(libs.kotlinx.serialization.json)
implementation(libs.bundles.compose)
implementation(libs.timber)
implementation(libs.kotlinx.coroutines.android)
}
1 change: 1 addition & 0 deletions readium/navigators/common/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pom.artifactId=readium-navigator-common
2 changes: 2 additions & 0 deletions readium/navigators/common/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.readium.navigator.common

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import org.readium.r2.shared.ExperimentalReadiumApi

@ExperimentalReadiumApi
public interface Configurable<S : Any, P : Any> {

public val preferences: MutableState<P>

public val settings: State<S>
}

@ExperimentalReadiumApi
public typealias Settings = org.readium.r2.navigator.preferences.Configurable.Settings

@ExperimentalReadiumApi
public typealias Preferences<P> = org.readium.r2.navigator.preferences.Configurable.Preferences<P>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.readium.navigator.common

public interface NavigatorState
15 changes: 8 additions & 7 deletions readium/navigators/demo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,12 @@ android {
}

dependencies {
api(project(":readium:navigators:readium-navigator-web"))
api(project(":readium:readium-shared"))
api(project(":readium:readium-streamer"))
implementation(project(":readium:readium-shared"))
implementation(project(":readium:readium-streamer"))
implementation(project(":readium:readium-navigator"))
implementation(project(":readium:navigators:readium-navigator-web"))
implementation(project(":readium:navigators:readium-navigator-pdf"))
implementation(project(":readium:adapters:pdfium"))

coreLibraryDesugaring(libs.desugar.jdk.libs)

Expand All @@ -71,9 +74,7 @@ dependencies {
implementation(libs.androidx.legacy.v4)
implementation(libs.bundles.compose)
implementation(libs.androidx.core)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.fragment.compose)
implementation(libs.androidx.appcompat)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.ext.junit)
androidTestImplementation(libs.androidx.expresso.core)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,47 @@ package org.readium.navigator.demo

import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentActivity
import org.readium.navigator.demo.reader.Reader
import org.readium.navigator.demo.util.Fullscreenable
import org.readium.navigator.demo.util.Theme
import org.readium.r2.shared.util.toAbsoluteUrl

class DemoActivity : ComponentActivity() {
class DemoActivity : FragmentActivity() {

private val viewModel: DemoViewModel by viewModels()

private val sharedStoragePickerLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
uri?.let {
val url = requireNotNull(it.toAbsoluteUrl())
viewModel.open(url)
viewModel.onBookSelected(url)
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

setContent {
Theme {
val fullscreenState = remember { mutableStateOf(false) }
Expand All @@ -63,33 +69,51 @@ class DemoActivity : ComponentActivity() {
}
}

when (val stateNow = state.value) {
DemoViewModel.State.BookSelection -> {
Placeholder()
LaunchedEffect(stateNow) {
sharedStoragePickerLauncher.launch(arrayOf("*/*"))
Box(
modifier = Modifier.fillMaxSize()
) {
when (val stateNow = state.value) {
DemoViewModel.State.BookSelection -> {
Placeholder()
LaunchedEffect(stateNow) {
sharedStoragePickerLauncher.launch(arrayOf("*/*"))
}
}
}

is DemoViewModel.State.Error -> {
Placeholder()
LaunchedEffect(stateNow.error) {
snackbarHostState.showSnackbar(stateNow.error.message)
viewModel.acknowledgeError()
is DemoViewModel.State.Error -> {
Placeholder()
LaunchedEffect(stateNow.error) {
snackbarHostState.showSnackbar(
message = stateNow.error.message,
duration = SnackbarDuration.Short
)
viewModel.onErrorDisplayed()
}
}
}

DemoViewModel.State.Loading -> {
Placeholder()
// Display and do nothing
}
DemoViewModel.State.Loading -> {
Placeholder()
// Display and do nothing
}

is DemoViewModel.State.Reader -> {
BackHandler {
viewModel.onBookClosed()
}

is DemoViewModel.State.Reader -> {
Reader(
state = stateNow,
fullScreenState = fullscreenState
)
Reader(
state = stateNow.readerState,
fullScreenState = fullscreenState
)
}
}

SnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)
.safeDrawingPadding(),
hostState = snackbarHostState
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
* available in the top-level LICENSE file of the project.
*/

@file:OptIn(ExperimentalReadiumApi::class)

package org.readium.navigator.demo

import android.app.Application
Expand All @@ -14,22 +12,10 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.readium.navigator.demo.preferences.PreferencesManager
import org.readium.navigator.demo.preferences.UserPreferencesViewModel
import org.readium.navigator.web.FixedWebNavigatorFactory
import org.readium.navigator.web.FixedWebNavigatorState
import org.readium.navigator.web.preferences.FixedWebPreferences
import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.navigator.demo.reader.ReaderOpener
import org.readium.navigator.demo.reader.ReaderState
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.streamer.PublicationOpener
import org.readium.r2.streamer.parser.DefaultPublicationParser
import timber.log.Timber

class DemoViewModel(
Expand All @@ -49,83 +35,40 @@ class DemoViewModel(
) : State

data class Reader(
val navigatorState: FixedWebNavigatorState,
val preferencesViewModel: UserPreferencesViewModel<FixedWebPreferences>
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 stateMutable: MutableStateFlow<State> =
MutableStateFlow(State.BookSelection)

val state: StateFlow<State> = stateMutable.asStateFlow()

fun open(url: AbsoluteUrl) {
fun onBookSelected(url: AbsoluteUrl) {
stateMutable.value = State.Loading

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

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

val navigatorFactory = FixedWebNavigatorFactory(getApplication(), publication)
?: run {
publication.close()
val error = DebugError("Publication not supported")
stateMutable.value = State.Error(error)
return@launch
}

val initialPreferences = FixedWebPreferences()

val preferencesViewModel =
UserPreferencesViewModel(
viewModelScope = viewModelScope,
preferencesManager = PreferencesManager(initialPreferences),
createPreferencesEditor = navigatorFactory::createPreferencesEditor
)

val navigatorState = navigatorFactory.createNavigator(
initialPreferences = initialPreferences
).getOrElse {
throw IllegalStateException()
}

preferencesViewModel.preferences
.onEach {
navigatorState.preferences.value = it
}
.launchIn(viewModelScope)

stateMutable.value = State.Reader(navigatorState, preferencesViewModel)
readerOpener.open(url)
.onFailure { stateMutable.value = State.Error(it) }
.onSuccess { stateMutable.value = State.Reader(it) }
}
}

fun acknowledgeError() {
fun onBookClosed() {
val stateNow = state.value
check(stateNow is State.Reader)
stateMutable.value = State.BookSelection
stateNow.readerState.close()
}

fun onErrorDisplayed() {
stateMutable.value = State.BookSelection
}
}
Loading

0 comments on commit b39036d

Please sign in to comment.