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
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ import org.readium.r2.shared.publication.Manifest
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.publication.flatten
import org.readium.r2.shared.publication.html.cssSelector
import org.readium.r2.shared.publication.services.content.Content
import org.readium.r2.shared.publication.services.content.ContentService
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.resource.Resource
import org.readium.r2.shared.util.resource.TransformingResource
import org.readium.r2.shared.util.resource.filename
import java.util.Locale
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import org.readium.r2.navigator.preferences.Color as ReadiumColor

private const val TAG = "ReadiumExtensions"
Expand Down Expand Up @@ -150,10 +156,10 @@ suspend fun Publication.getMediaOverlays(): List<FlutterMediaOverlay?>? {
if (!hasMediaOverlays()) return null

// Flatten TOC for title lookup
val toc = tableOfContents.flatten().map { Pair(it.href.toString(), it.title) }
val toc = tableOfContents.flatten().map { Pair(it.href.toString(), it) }

// Remember last matched TOC item for titles
var lastTocMatch: Pair<String, String?>? = null
var lastTocMatch: Pair<String, Link>? = null

return this.readingOrder.mapNotNull { r ->
r.alternates.find { a ->
Expand All @@ -163,7 +169,7 @@ suspend fun Publication.getMediaOverlays(): List<FlutterMediaOverlay?>? {
val jsonString =
this.get(link)?.read()?.getOrNull()?.let { String(it) } ?: return@mapIndexedNotNull null
val jsonObject = JSONObject(jsonString)
FlutterMediaOverlay.fromJson(jsonObject, index + 1, link.title ?: "")
FlutterMediaOverlay.fromJson(jsonObject, index + 1, null, link.title ?: "")
}
.map { mo ->
val items = mo.items.map { item ->
Expand All @@ -174,9 +180,15 @@ suspend fun Publication.getMediaOverlays(): List<FlutterMediaOverlay?>? {

if (match?.second != null) {
lastTocMatch = match
item.copy(title = match.second ?: "")
item.copy(
title = match.second.title ?: "",
tocHref = match.second.href.resolve()
)
} else if (lastTocMatch?.second != null && lastTocMatch.first.substringBefore("#") == item.textFile) {
item.copy(title = lastTocMatch.second ?: "")
item.copy(
title = lastTocMatch.second.title ?: "",
tocHref = lastTocMatch.second.href.resolve()
)
} else {
item
}
Expand Down Expand Up @@ -251,3 +263,107 @@ fun Locator.copyWithTimeFragment(time: Double): Locator {
)
)
}

/**
* Helper for getting all cssSelectors for a HTML document.
*/
suspend fun Publication.findAllCssSelectors(href: Url): List<String>? {
if (!conformsTo(Publication.Profile.EPUB)) {
Log.d(TAG, ":findAllCssSelectors - this only works for an EPUB Profile")
return null
}

val contentService = findService(ContentService::class) ?: run {
Log.d(TAG, ":findAllCssSelectors - no content service found")
return null
}

val cleanHref = href.cleanHref()

val ids = arrayListOf<String>()
for (element in contentService.content(
Locator(
href = cleanHref,
mediaType = MediaType.XHTML
)
)) {
if (element !is Content.TextElement) {
continue
}

if (element.locator.href.cleanHref() != cleanHref) {
// We iterated to the next document, stopping
break
}

// We are only interested in #id type of cssSelectors.
val cssSelector =
element.locator.locations.cssSelector?.takeIf { it.startsWith("#") } ?: continue

ids.add(cssSelector)
}

return ids
}

/**
* Find the cssSelector for a locator. If it already have one return it, otherwise we need to look it up.
*/
suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? {
locator.locations.cssSelector?.takeIf { it.startsWith("#") }?.let { return it }

val contentService = findService(ContentService::class) ?: run {
Log.d(TAG, ":findCssSelectorForLocator - no content service found")
return null
}

val cleanHref = locator.href.cleanHref()
for (element in contentService.content(locator)) {
if (element !is Content.TextElement) {
continue
}

if (element.locator.href.cleanHref() != cleanHref) {
// We iterated to the next document, stopping
break
}

val cssSelector =
element.locator.locations.cssSelector?.takeIf { it.startsWith("#") } ?: continue

return cssSelector
}

return null
}

/**
* Remove query and fragment from the Url
*/
fun Url.cleanHref() = removeFragment().removeQuery()

/**
* Remove query and fragment from the Href
*/
fun Href.cleanHref() = Href(resolve().cleanHref())

val Href.fragmentParameters: Map<String, String>
get() = resolve()
.fragment
// Splits parameters
?.split("&")
?.filter { !it.startsWith("=") }
?.map { it.split("=") }
// Only keep named parameters
?.filter { it.size == 2 }
?.associate { it[0].trim().lowercase(Locale.ROOT) to it[1].trim() }
?: mapOf()


/**
* Media fragment, used for example in audiobooks.
*
* https://www.w3.org/TR/media-frags/
*/
val Href.time: Duration? get() =
fragmentParameters["t"]?.toIntOrNull()?.seconds
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ import org.readium.navigator.media.tts.android.AndroidTtsPreferences
import org.readium.navigator.media.tts.android.AndroidTtsSettings
import org.readium.r2.navigator.Decoration
import org.readium.r2.navigator.epub.EpubPreferences
import org.readium.r2.navigator.extensions.time
import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.InternalReadiumApi
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.allAreHtml
import org.readium.r2.shared.publication.flatten
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.DebugError
import org.readium.r2.shared.util.Language
Expand Down Expand Up @@ -176,9 +178,6 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua

private var epubNavigator: EpubNavigator? = null

val epubCurrentLocator: Locator?
get() = epubNavigator?.currentLocator?.value

private var _audioPreferences: FlutterAudioPreferences = FlutterAudioPreferences()

/** Current audio preferences (defaults if audio hasn't been enabled yet). */
Expand Down Expand Up @@ -398,6 +397,13 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua
state[currentPublicationUrlKey] = value
}

/***
* For EPUB profile, maps document [Url] to a list of all the cssSelectors in the document.
*
* This is used to find the current toc item.
*/
private var currentPublicationCssSelectorMap: MutableMap<Url, List<String>>? = null

/**
* Sets the headers used in the HTTP requests for fetching publication resources, including
* resources in already created `Publication` objects.
Expand Down Expand Up @@ -579,6 +585,7 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua
mainScope.async {
_currentPublication?.close()
_currentPublication = null
currentPublicationCssSelectorMap = null

ttsNavigator?.dispose()
ttsNavigator = null
Expand Down Expand Up @@ -608,12 +615,13 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua
// TODO: Notify client
}

@OptIn(InternalReadiumApi::class)
override fun onTimebasedCurrentLocatorChanges(
locator: Locator, currentReadingOrderLink: Link?
) {
val duration = currentReadingOrderLink?.duration
val timeOffset =
locator.locations.fragments.find { it.startsWith("t=") }?.substring(2)?.toDoubleOrNull()
locator.locations.time?.inWholeSeconds?.toDouble()
?: (duration?.let { duration ->
locator.locations.progression?.let { prog -> duration * prog }
})
Expand All @@ -631,6 +639,47 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua
currentReaderWidget?.go(locator, true)
}

/**
* Find the current table of content item from a locator.
*/
suspend fun epubFindCurrentToc(locator: Locator): Locator {
val publication = currentPublication ?: run {
Log.e(TAG, ":epubFindCurrentToc, no currentPublication")
return locator
}

val cssSelector = publication.findCssSelectorForLocator(locator) ?: run {
Log.e(TAG, ":epubFindCurrentToc, missing cssSelector in locator")
return locator
}

val resultLocator = locator.copyWithLocations(otherLocations = locator.locations.otherLocations + ("cssLocator" to cssSelector))

val contentIds = epubGetAllDocumentCssSelectors(locator.href)
val idx = contentIds.indexOf(cssSelector).takeIf { it > -1 } ?: run {
Log.d(TAG, ":epubFindCurrentToc cssSelector:${cssSelector} not found in contentIds")
return resultLocator
}

val cleanHref = resultLocator.href.cleanHref()
val toc = publication.tableOfContents.flatten().filter {
it.href.resolve().cleanHref() == cleanHref
}.associateBy { contentIds.indexOf("${it.href.resolve().fragment}") }

val tocItem = toc.entries.lastOrNull { it.key <= idx }?.value
?: toc.entries.firstOrNull()?.value ?: run {
Log.d(TAG, ":epubFindCurrentToc - no tocItem found")
return resultLocator
}

return resultLocator.copy(
title = tocItem.title
).copyWithLocations(
otherLocations = resultLocator.locations.otherLocations + ("toc" to tocItem.href.resolve()
.toString())
)
}

@OptIn(InternalReadiumApi::class)
suspend fun epubEnable(
initialLocator: Locator?,
Expand Down Expand Up @@ -716,7 +765,8 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua
)
},
{ language, availableVoices -> null },
AndroidTtsPreferences())?.voices ?: setOf()
AndroidTtsPreferences()
)?.voices ?: setOf()
}

fun ttsGetPreferences(): FlutterTtsPreferences? {
Expand Down Expand Up @@ -929,10 +979,25 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua
}

/**
* Get locator fragments from EPUB navigator.
* Get first visible locator from the EPUB navigator.
*/
suspend fun epubGetLocatorFragments(locator: Locator): Locator? {
return epubNavigator?.getLocatorFragments(locator)
suspend fun firstVisibleElementLocator(): Locator? {
return epubNavigator?.firstVisibleElementLocator()
}

/**
* Get all cssSelectors for an EPUB file.
*/
suspend fun epubGetAllDocumentCssSelectors(href: Url): List<String> {
val cssSelectorMap = currentPublicationCssSelectorMap ?: mutableMapOf()
currentPublicationCssSelectorMap = cssSelectorMap

val cleanHref = href.cleanHref()
return cssSelectorMap.getOrPut(cleanHref) {
currentPublication?.findAllCssSelectors(
cleanHref
) ?: listOf()
}
}

/**
Expand Down
Loading