Skip to content
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

Extract footnote popups from the Epub navigator #448

Merged
merged 10 commits into from
Jan 30, 2024
Merged
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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ All notable changes to this project will be documented in this file. Take a look

**Warning:** Features marked as *experimental* may change or be removed in a future release without notice. Use with caution.

<!-- ## [Unreleased] -->
## [Unreleased]

### Added

* The new `HyperlinkNavigator.shouldFollowInternalLink(Link, LinkContext?)` allows you to handle footnotes according to your preference.
* By default, the navigator now moves to the footnote content instead of displaying a pop-up as it did in version 2.x.


## [3.0.0-alpha.1]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ import org.readium.r2.shared.util.AbsoluteUrl
@ExperimentalReadiumApi
public interface HyperlinkNavigator : Navigator {

@ExperimentalReadiumApi
public sealed interface LinkContext

/**
* @param noteContent Content of the footnote. Look at the [Link.mediaType] for the format
* of the footnote (e.g. HTML).
*/
@ExperimentalReadiumApi
public data class FootnoteContext(
public val noteContent: String
) : LinkContext

@ExperimentalReadiumApi
public interface Listener : Navigator.Listener {

Expand All @@ -26,10 +38,10 @@ public interface HyperlinkNavigator : Navigator {
* 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.
* to the calling app to decide how to display the resource.
*/
@ExperimentalReadiumApi
public fun shouldFollowInternalLink(link: Link): Boolean { return true }
public fun shouldFollowInternalLink(link: Link, context: LinkContext?): Boolean { return true }

/**
* Called when a link to an external URL was activated in the navigator.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,12 @@ import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import android.os.Build
import android.text.Html
import android.util.AttributeSet
import android.view.*
import android.webkit.URLUtil
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.widget.ImageButton
import android.widget.ListPopupWindow
import android.widget.PopupWindow
import android.widget.TextView
import androidx.annotation.RequiresApi
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
Expand All @@ -46,6 +41,7 @@ import org.readium.r2.shared.extensions.tryOrLog
import org.readium.r2.shared.extensions.tryOrNull
import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.publication.Locator
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.data.decodeString
import org.readium.r2.shared.util.flatMap
Expand Down Expand Up @@ -87,6 +83,9 @@ internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebV
@InternalReadiumApi
fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? = null

@InternalReadiumApi
fun shouldFollowFootnoteLink(url: AbsoluteUrl, context: HyperlinkNavigator.FootnoteContext): Boolean

@InternalReadiumApi
fun resourceAtUrl(url: Url): Resource? = null

Expand Down Expand Up @@ -115,7 +114,7 @@ internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebV
var listener: Listener? = null
internal var preferences: SharedPreferences? = null

var resourceUrl: Url? = null
var resourceUrl: AbsoluteUrl? = null

internal val scrollModeFlow = MutableStateFlow(false)

Expand All @@ -128,6 +127,12 @@ internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebV

private val uiScope = CoroutineScope(Dispatchers.Main)

/*
* Url already handled by listener.shouldFollowFootnoteLink,
* Tries to ignore the matching shouldOverrideUrlLoading call.
*/
private var urlNotToOverrideLoading: AbsoluteUrl? = null
qnga marked this conversation as resolved.
Show resolved Hide resolved

init {
setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
}
Expand Down Expand Up @@ -277,8 +282,6 @@ internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebV
return false
}

// FIXME: Let the app handle footnotes.

// We ignore taps on interactive element, unless it's an element we handle ourselves such as
// pop-up footnotes.
if (event.interactiveElement != null) {
Expand Down Expand Up @@ -344,11 +347,13 @@ internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebV

val id = href.fragment ?: return false

val absoluteUrl = resourceUrl.resolve(href).removeFragment()
val absoluteUrl = resourceUrl.resolve(href)

val absoluteUrlWithoutFragment = absoluteUrl.removeFragment()

val aside = runBlocking {
tryOrLog {
listener?.resourceAtUrl(absoluteUrl)
listener?.resourceAtUrl(absoluteUrlWithoutFragment)
?.use { res ->
res.read()
.flatMap { it.decodeString() }
Expand All @@ -358,50 +363,22 @@ internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebV
?.select("#$id")
?.first()?.html()
}
} ?: return false
}?.takeIf { it.isNotBlank() }
?: return false

val safe = Jsoup.clean(aside, Safelist.relaxed())

// Initialize a new instance of LayoutInflater service
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater

// Inflate the custom layout/view
val customView = inflater.inflate(R.layout.readium_navigator_popup_footnote, null)

// Initialize a new instance of popup window
val mPopupWindow = PopupWindow(
customView,
ListPopupWindow.WRAP_CONTENT,
ListPopupWindow.WRAP_CONTENT
val context = HyperlinkNavigator.FootnoteContext(
noteContent = safe
)
mPopupWindow.isOutsideTouchable = true
mPopupWindow.isFocusable = true

// Set an elevation value for popup window
// Call requires API level 21
mPopupWindow.elevation = 5.0f
val shouldFollowLink = listener?.shouldFollowFootnoteLink(absoluteUrl, context) ?: true

val textView = customView.findViewById(R.id.footnote) as TextView
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
textView.text = Html.fromHtml(safe, Html.FROM_HTML_MODE_COMPACT)
} else {
@Suppress("DEPRECATION")
textView.text = Html.fromHtml(safe)
if (shouldFollowLink) {
urlNotToOverrideLoading = absoluteUrl
}

// Get a reference for the custom view close button
val closeButton = customView.findViewById(R.id.ib_close) as ImageButton

// Set a click listener for the popup window close button
closeButton.setOnClickListener {
// Dismiss the popup window
mPopupWindow.dismiss()
}

// Finally, show the popup window at the center location of root relative layout
mPopupWindow.showAtLocation(this, Gravity.CENTER, 0, 0)

return true
// Consume event if the link should not be followed.
return !shouldFollowLink
}

@android.webkit.JavascriptInterface
Expand Down Expand Up @@ -596,9 +573,15 @@ internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebV
}

internal fun shouldOverrideUrlLoading(request: WebResourceRequest): Boolean {
if (resourceUrl == request.url.toUrl()) return false
mickael-menu marked this conversation as resolved.
Show resolved Hide resolved
val requestUrl = request.url.toUrl() ?: return false

return listener?.shouldOverrideUrlLoading(this, request) ?: false
// FIXME: I doubt this can work well. hasGesture considers itself unreliable.
return if (urlNotToOverrideLoading == requestUrl && request.hasGesture()) {
urlNotToOverrideLoading = null
false
} else {
listener?.shouldOverrideUrlLoading(this, request) ?: false
}
}

internal fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import org.readium.r2.shared.publication.ReadingProgression as PublicationReadin
import org.readium.r2.shared.publication.epub.EpubLayout
import org.readium.r2.shared.publication.presentation.presentation
import org.readium.r2.shared.publication.services.positionsByReadingOrder
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.resource.Resource
Expand Down Expand Up @@ -506,7 +507,7 @@ public class EpubNavigatorFragment internal constructor(
}

viewLifecycleOwner.lifecycleScope.launch {
withStarted {
viewLifecycleOwner.withStarted {
// Restore the last locator before a configuration change (e.g. screen rotation), or the
// initial locator when given.
val locator = savedInstanceState?.let {
Expand Down Expand Up @@ -831,6 +832,12 @@ public class EpubNavigatorFragment internal constructor(
return true
}

override fun shouldFollowFootnoteLink(
url: AbsoluteUrl,
context: HyperlinkNavigator.FootnoteContext
): Boolean =
viewModel.shouldFollowFootnoteLink(url, context)

override fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? =
viewModel.shouldInterceptRequest(request)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,19 @@ internal class EpubNavigatorViewModel(
fun navigateToUrl(url: AbsoluteUrl) = viewModelScope.launch {
val link = internalLinkFromUrl(url)
if (link != null) {
if (listener == null || listener.shouldFollowInternalLink(link)) {
if (listener == null || listener.shouldFollowInternalLink(link, null)) {
_events.send(Event.OpenInternalLink(link))
}
} else {
listener?.onExternalLinkActivated(url)
}
}

fun shouldFollowFootnoteLink(url: AbsoluteUrl, context: HyperlinkNavigator.FootnoteContext): Boolean {
val link = internalLinkFromUrl(url) ?: return true
return listener?.shouldFollowInternalLink(link, context) ?: true
}

/**
* Gets the publication [Link] targeted by the given [url].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ 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.util.Url
import org.readium.r2.shared.util.AbsoluteUrl

@OptIn(ExperimentalReadiumApi::class)
internal class R2EpubPageFragment : Fragment() {

private val resourceUrl: Url?
get() = BundleCompat.getParcelable(requireArguments(), "url", Url::class.java)
private val resourceUrl: AbsoluteUrl?
get() = BundleCompat.getParcelable(requireArguments(), "url", AbsoluteUrl::class.java)

internal val link: Link?
get() = BundleCompat.getParcelable(requireArguments(), "link", Link::class.java)
Expand Down Expand Up @@ -436,7 +436,7 @@ internal class R2EpubPageFragment : Fragment() {
private const val textZoomBundleKey = "org.readium.textZoom"

fun newInstance(
url: Url,
url: AbsoluteUrl,
link: Link? = null,
initialLocator: Locator? = null,
positionCount: Int = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.publication.Locator
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.Url

internal class R2PagerAdapter internal constructor(
Expand All @@ -32,7 +33,7 @@ internal class R2PagerAdapter internal constructor(
internal var listener: Listener? = null

internal sealed class PageResource {
data class EpubReflowable(val link: Link, val url: Url, val positionCount: Int) : PageResource()
data class EpubReflowable(val link: Link, val url: AbsoluteUrl, val positionCount: Int) : PageResource()
data class EpubFxl(
val leftLink: Link? = null,
val leftUrl: Url? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.publication.Locator
import org.readium.r2.shared.publication.Publication
import org.readium.r2.testapp.R
import org.readium.r2.testapp.reader.preferences.UserPreferencesBottomSheetDialogFragment
import org.readium.r2.testapp.reader.preferences.MainPreferencesBottomSheetDialogFragment
import org.readium.r2.testapp.utils.UserError

/*
Expand All @@ -48,8 +48,10 @@ abstract class BaseReaderFragment : Fragment() {
}

when (event) {
is ReaderViewModel.FeedbackEvent.BookmarkFailed -> toast(R.string.bookmark_exists)
is ReaderViewModel.FeedbackEvent.BookmarkSuccessfullyAdded -> toast(
is ReaderViewModel.FragmentFeedback.BookmarkFailed -> toast(
R.string.bookmark_exists
)
is ReaderViewModel.FragmentFeedback.BookmarkSuccessfullyAdded -> toast(
R.string.bookmark_added
)
}
Expand Down Expand Up @@ -86,8 +88,7 @@ abstract class BaseReaderFragment : Fragment() {
return true
}
R.id.settings -> {
val settingsModel = checkNotNull(model.settings)
UserPreferencesBottomSheetDialogFragment(settingsModel, "User Settings")
MainPreferencesBottomSheetDialogFragment()
.show(childFragmentManager, "Settings")
return true
}
Expand Down
Loading
Loading