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,