diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a4282358f..269520939a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,10 @@ All notable changes to this project will be documented in this file. Take a look
* Scroll mode: jumping between two EPUB resources with a horizontal swipe triggers the `Navigator.Listener.onJumpToLocator()` callback.
* This can be used to allow the user to go back to their previous location if they swiped across chapters by mistake.
* Support for keyboard events in the EPUB, PDF and image navigators. See `VisualNavigator.addInputListener()`.
+* Support for non-linear EPUB resources with an opt-in in reading apps (contributed by @chrfalch in [#375](https://github.com/readium/kotlin-toolkit/pull/375) and [#376](https://github.com/readium/kotlin-toolkit/pull/376)).
+ 1. Override loading non-linear resources with `VisualNavigator.Listener.shouldJumpToLink()`.
+ 2. Present a new `EpubNavigatorFragment` by providing a custom `readingOrder` with only this resource to the constructor.
+
#### Streamer
diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt
index 897e081750..d532871dd4 100644
--- a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt
+++ b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt
@@ -173,6 +173,18 @@ public interface VisualNavigator : Navigator {
public fun removeInputListener(listener: InputListener)
public interface Listener : Navigator.Listener {
+
+ /**
+ * Called when a link to an internal resource was clicked in the navigator.
+ *
+ * You can use this callback to perform custom navigation like opening a new window
+ * or other operations.
+ *
+ * By returning false the navigator wont try to open the link itself and it is up
+ * to the calling app to decide how to display the link.
+ */
+ public fun shouldJumpToLink(link: Link): Boolean { return true }
+
@Deprecated("Use `addInputListener` instead", level = DeprecationLevel.ERROR)
public fun onTap(point: PointF): Boolean = false
diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFactory.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFactory.kt
index 180d23d3b0..3d909a2310 100644
--- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFactory.kt
+++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFactory.kt
@@ -9,6 +9,7 @@ package org.readium.r2.navigator.epub
import androidx.fragment.app.FragmentFactory
import org.readium.r2.navigator.ExperimentalDecorator
import org.readium.r2.shared.ExperimentalReadiumApi
+import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.publication.Locator
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.publication.epub.EpubLayout
@@ -38,9 +39,23 @@ public class EpubNavigatorFactory(
private val layout: EpubLayout =
publication.metadata.presentation.layout ?: EpubLayout.REFLOWABLE
+ /**
+ * Creates a factory for [EpubNavigatorFragment].
+ *
+ * @param initialLocator The first location which should be visible when rendering the
+ * publication. Can be used to restore the last reading location.
+ * @param readingOrder Custom reading order to override the publication's one.
+ * @param initialPreferences The set of preferences that should be initially applied to the
+ * navigator.
+ * @param listener Optional listener to implement to observe navigator events.
+ * @param paginationListener Optional listener to implement to observe events related to
+ * pagination.
+ * @param configuration Additional configuration.
+ */
@OptIn(ExperimentalDecorator::class)
public fun createFragmentFactory(
initialLocator: Locator?,
+ readingOrder: List? = null,
initialPreferences: EpubPreferences = EpubPreferences(),
listener: EpubNavigatorFragment.Listener? = null,
paginationListener: EpubNavigatorFragment.PaginationListener? = null,
@@ -49,6 +64,7 @@ public class EpubNavigatorFactory(
EpubNavigatorFragment(
publication = publication,
initialLocator = initialLocator,
+ readingOrder = readingOrder,
initialPreferences = initialPreferences,
listener = listener,
paginationListener = paginationListener,
diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt
index 3668cfa021..4500016a72 100644
--- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt
+++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt
@@ -109,6 +109,7 @@ public typealias JavascriptInterfaceFactory = (resource: Link) -> Any?
public class EpubNavigatorFragment internal constructor(
override val publication: Publication,
private val initialLocator: Locator?,
+ readingOrder: List?,
private val initialPreferences: EpubPreferences,
internal val listener: Listener?,
internal val paginationListener: PaginationListener?,
@@ -286,12 +287,21 @@ public class EpubNavigatorFragment internal constructor(
publication,
config = this.config,
initialPreferences = initialPreferences,
+ listener = listener,
layout = epubLayout,
defaults = defaults
)
}
- internal lateinit var positionsByReadingOrder: List>
+ private val readingOrder: List = readingOrder ?: publication.readingOrder
+
+ private val positionsByReadingOrder: List> =
+ if (readingOrder != null) {
+ emptyList()
+ } else {
+ runBlocking { publication.positionsByReadingOrder() }
+ }
+
internal lateinit var positions: List
internal lateinit var resourcePager: R2ViewPager
@@ -321,12 +331,11 @@ public class EpubNavigatorFragment internal constructor(
_binding = ReadiumNavigatorViewpagerBinding.inflate(inflater, container, false)
var view: View = binding.root
- positionsByReadingOrder = runBlocking { publication.positionsByReadingOrder() }
positions = positionsByReadingOrder.flatten()
when (viewModel.layout) {
EpubLayout.REFLOWABLE -> {
- resourcesSingle = publication.readingOrder.mapIndexed { index, link ->
+ resourcesSingle = readingOrder.mapIndexed { index, link ->
PageResource.EpubReflowable(
link = link,
url = viewModel.urlTo(link),
@@ -341,9 +350,9 @@ public class EpubNavigatorFragment internal constructor(
// TODO needs work, currently showing two resources for fxl, needs to understand which two resources, left & right, or only right etc.
var doublePageLeft: Link? = null
- var doublePageRight: Link?
+ var doublePageRight: Link? = null
- for ((index, link) in publication.readingOrder.withIndex()) {
+ for ((index, link) in readingOrder.withIndex()) {
val url = viewModel.urlTo(link)
resourcesSingle.add(PageResource.EpubFxl(leftLink = link, leftUrl = url))
@@ -517,7 +526,7 @@ public class EpubNavigatorFragment internal constructor(
is EpubNavigatorViewModel.Event.RunScript -> {
run(event.command)
}
- is EpubNavigatorViewModel.Event.GoTo -> {
+ is EpubNavigatorViewModel.Event.OpenInternalLink -> {
go(event.target)
}
EpubNavigatorViewModel.Event.InvalidateViewPager -> {
@@ -915,7 +924,7 @@ public class EpubNavigatorFragment internal constructor(
locatorToResourceAtIndex(resourcePager.currentItem + 1)
private fun locatorToResourceAtIndex(index: Int): Locator? =
- publication.readingOrder.getOrNull(index)
+ readingOrder.getOrNull(index)
?.let { publication.locatorFromLink(it) }
private val r2PagerAdapter: R2PagerAdapter?
@@ -952,8 +961,8 @@ public class EpubNavigatorFragment internal constructor(
override val currentLocator: StateFlow get() = _currentLocator
private val _currentLocator = MutableStateFlow(
- initialLocator?.let { publication.normalizeLocator(it) }
- ?: requireNotNull(publication.locatorFromLink(publication.readingOrder.first()))
+ initialLocator
+ ?: requireNotNull(publication.locatorFromLink(this.readingOrder.first()))
)
/**
@@ -964,7 +973,7 @@ public class EpubNavigatorFragment internal constructor(
override suspend fun firstVisibleElementLocator(): Locator? {
if (!::resourcePager.isInitialized) return null
- val resource = publication.readingOrder[resourcePager.currentItem]
+ val resource = readingOrder[resourcePager.currentItem]
return currentReflowablePageFragment?.webView?.findFirstVisibleLocator()
?.copy(
href = resource.url(),
@@ -1071,6 +1080,8 @@ public class EpubNavigatorFragment internal constructor(
* @param initialLocator The first location which should be visible when rendering the
* publication. Can be used to restore the last reading location.
* @param listener Optional listener to implement to observe events, such as user taps.
+ * @param readingOrder Custom order of resources to display. Used for example to display a
+ * non-linear resource on its own.
* @param config Additional configuration.
*/
@Deprecated(
diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt
index f8bda8ddda..6304f52448 100644
--- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt
+++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt
@@ -48,6 +48,7 @@ internal class EpubNavigatorViewModel(
val config: EpubNavigatorFragment.Configuration,
initialPreferences: EpubPreferences,
val layout: EpubLayout,
+ val listener: VisualNavigator.Listener?,
private val defaults: EpubDefaults,
private val server: WebViewServer
) : AndroidViewModel(application) {
@@ -66,7 +67,7 @@ internal class EpubNavigatorViewModel(
}
sealed class Event {
- data class GoTo(val target: Link) : Event()
+ data class OpenInternalLink(val target: Link) : Event()
data class OpenExternalLink(val url: Url) : Event()
/** Refreshes all the resources in the view pager. */
@@ -177,7 +178,9 @@ internal class EpubNavigatorViewModel(
fun navigateToUrl(url: AbsoluteUrl) = viewModelScope.launch {
val link = internalLinkFromUrl(url)
if (link != null) {
- _events.send(Event.GoTo(link))
+ if (listener?.shouldJumpToLink(link) == true) {
+ _events.send(Event.OpenInternalLink(link))
+ }
} else {
_events.send(Event.OpenExternalLink(url))
}
@@ -338,6 +341,7 @@ internal class EpubNavigatorViewModel(
application: Application,
publication: Publication,
layout: EpubLayout,
+ listener: VisualNavigator.Listener?,
defaults: EpubDefaults,
config: EpubNavigatorFragment.Configuration,
initialPreferences: EpubPreferences
@@ -348,6 +352,7 @@ internal class EpubNavigatorViewModel(
config,
initialPreferences,
layout,
+ listener,
defaults = defaults,
server = WebViewServer(
application,