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

Add a new experimental EPUB FXL navigator #567

Open
wants to merge 62 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
8dc9238
Add navigators/web module
qnga Jul 3, 2024
ba0fc5c
Add navigators/demo module
qnga Jul 3, 2024
b11b053
Complete setup
qnga Jul 3, 2024
682be77
Can load Bella
qnga Jul 4, 2024
7ad7f72
Can scroll and zoom
qnga Jul 10, 2024
103f53b
MOSTLY work with nested scroll connection
qnga Jul 18, 2024
740d109
Important fixes
qnga Jul 19, 2024
92ef776
Fix fling removing it between spreads
qnga Jul 23, 2024
479ba7f
Seems to fix fling
qnga Jul 24, 2024
a54583d
Prioritize snapping over Webview flinging
qnga Jul 24, 2024
e9b0adc
Clean up
qnga Jul 24, 2024
c05315b
Disable gestures in pager. Every scroll action is performed in PagerN…
qnga Jul 26, 2024
a5754f5
Use JS to enable double spreads
qnga Aug 27, 2024
f608953
Clean up and add preferences sheet
qnga Aug 28, 2024
86d7e89
Complete cleanup
qnga Sep 3, 2024
0fe8388
Remove scripts
qnga Sep 3, 2024
e32842f
Add TS Code
qnga Sep 3, 2024
c5e093d
Support Page.CENTER
qnga Sep 3, 2024
38af22e
Cosmetic changes
qnga Sep 3, 2024
c2d5f69
Fix various bugs
qnga Sep 6, 2024
c2cdf38
Fix vertical scrolling bug
qnga Sep 10, 2024
3242b37
Renaming
qnga Sep 12, 2024
4fe9aa7
Don't perform fling on both pager and WebView
qnga Sep 16, 2024
68652e5
Cosmetic changes
qnga Sep 17, 2024
223a8c8
Merge branch 'develop' of github.com:readium/kotlin-toolkit into comp…
qnga Sep 17, 2024
4d3a97d
Lint
qnga Sep 17, 2024
92a022d
Remove Bella
qnga Sep 17, 2024
2249045
Rebuild scripts
qnga Sep 17, 2024
2bd77df
Don't use unportable cp syntax
qnga Sep 17, 2024
645c8e0
Fix navigator factory
qnga Sep 18, 2024
1ed7d2c
Fix item key and initial item
qnga Sep 18, 2024
b52b97d
Refactoring
qnga Sep 18, 2024
f93e377
Cosmetic changes
qnga Sep 18, 2024
ef2b01b
Reverse BoxWithConstraints and HorizontalPager order
qnga Sep 21, 2024
48092d4
Renaming
qnga Sep 21, 2024
0427968
Add fullscreen mode, set safe drawing area, handle taps
qnga Sep 25, 2024
14b4d52
Fix null defaults and cosmetic changes
qnga Sep 25, 2024
f227a84
Use prettier for TypeScript
qnga Sep 25, 2024
042f70d
Fix taps in resource iframes.
qnga Sep 25, 2024
e3e7ff6
Fix theme
qnga Sep 26, 2024
93248cf
Add support for background color
qnga Sep 26, 2024
a8fb212
Fix preference filters
qnga Sep 26, 2024
1c68458
Add go
qnga Oct 1, 2024
b39036d
Add composable pdf navigator and refactor demo
qnga Oct 14, 2024
15a8841
Add gesture detection into resource wrappers
qnga Oct 21, 2024
a8a87e4
Fix navigation
qnga Oct 21, 2024
ad23745
Fix tap coordinates
qnga Oct 22, 2024
e78d298
Fix navigation history and clean up
qnga Oct 23, 2024
9d78b03
Show page as soon as we have viewport size
qnga Oct 23, 2024
21ee492
Introduce typed positions
qnga Oct 23, 2024
4584e4f
Renaming
qnga Oct 23, 2024
3546856
Allow state to depend on composition and fix navigation history
qnga Oct 24, 2024
2673219
Enforce hardware acceleration
qnga Oct 24, 2024
17b9e85
Cosmetic changes
qnga Oct 24, 2024
a47afd7
Cosmetic change
qnga Oct 24, 2024
3340241
Renaming
qnga Oct 24, 2024
4f131d0
Split up and clarify fixed web rendition state
qnga Oct 24, 2024
0d31e1d
No longer exposes reading order
qnga Oct 24, 2024
82e2736
Make ReadingOrder internal
qnga Oct 25, 2024
f71e2d3
Improve scroll algorithm to prevent users from being locked on one page.
qnga Oct 26, 2024
9391576
Fix initialization callback
qnga Oct 26, 2024
db92c70
Finish DemoActivity if no book was selected
qnga Oct 26, 2024
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
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SCRIPTS_PATH := readium/navigator/src/main/assets/_scripts
SCRIPTS_NAVIGATOR_WEB_PATH := readium/navigators/web/scripts

help:
@echo "Usage: make <target>\n\n\
Expand All @@ -25,3 +26,11 @@ scripts:
pnpm run format; \
pnpm run lint; \
pnpm run bundle

cd $(SCRIPTS_NAVIGATOR_WEB_PATH); \
corepack install; \
pnpm install --frozen-lockfile; \
pnpm run format; \
pnpm run lint; \
pnpm run bundle; \
cp dist/* ../src/main/assets/readium/navigators/web/
17 changes: 9 additions & 8 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ androidx-cardview = "1.0.0"
# Make sure to align with the Kotlin version
# https://developer.android.com/jetpack/androidx/releases/compose-kotlin
androidx-compose-compiler = "1.5.14"
androidx-compose-animation = "1.6.7"
androidx-compose-foundation = "1.6.7"
androidx-compose-material = "1.6.7"
androidx-compose-material3 = "1.2.1"
androidx-compose-runtime = "1.6.7"
androidx-compose-ui = "1.6.7"
androidx-compose-animation = "1.7.0"
androidx-compose-foundation = "1.7.0"
androidx-compose-material = "1.7.0"
androidx-compose-material3 = "1.3.0"
androidx-compose-runtime = "1.7.0"
androidx-compose-ui = "1.7.0"
androidx-constraintlayout = "2.1.4"
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,102 @@
package org.readium.navigator.common

import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.Url

@ExperimentalReadiumApi
public interface HyperlinkListener {

public fun onReadingOrderLinkActivated(url: Url, context: LinkContext?)

public fun onResourceLinkActivated(url: Url, context: LinkContext?)

public fun onExternalLinkActivated(url: AbsoluteUrl, context: LinkContext?)
}

@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 object NullHyperlinkListener : HyperlinkListener {
override fun onReadingOrderLinkActivated(url: Url, context: LinkContext?) {
}

override fun onResourceLinkActivated(url: Url, context: LinkContext?) {
}

override fun onExternalLinkActivated(url: AbsoluteUrl, context: LinkContext?) {
}
}

@ExperimentalReadiumApi
@Composable
public fun <L : Location> defaultHyperlinkListener(
navigator: Navigator<L, *>,
shouldFollowReadingOrderLink: (Url, LinkContext?) -> Boolean = { _, _ -> true },
// TODO: shouldFollowResourceLink: (Url, LinkContext?) -> Boolean = { _, _ -> true },
onExternalLinkActivated: (AbsoluteUrl, LinkContext?) -> Unit = { _, _ -> }
): HyperlinkListener {
val coroutineScope = rememberCoroutineScope()
val navigationHistory: MutableState<List<L>> = remember { mutableStateOf(emptyList()) }

BackHandler(enabled = navigationHistory.value.isNotEmpty()) {
val previousItem = navigationHistory.value.last()
navigationHistory.value -= previousItem
coroutineScope.launch { navigator.goTo(previousItem) }
}

val onPreFollowingReadingOrder = {
navigationHistory.value += navigator.location.value
}

return DefaultHyperlinkListener(
coroutineScope = coroutineScope,
navigator = navigator,
shouldFollowReadingOrderLink = shouldFollowReadingOrderLink,
onPreFollowingReadingOrderLink = onPreFollowingReadingOrder,
onExternalLinkActivatedDelegate = onExternalLinkActivated
)
}

@ExperimentalReadiumApi
private class DefaultHyperlinkListener<L : Location>(
private val coroutineScope: CoroutineScope,
private val navigator: Navigator<L, *>,
private val shouldFollowReadingOrderLink: (Url, LinkContext?) -> Boolean,
private val onPreFollowingReadingOrderLink: () -> Unit,
private val onExternalLinkActivatedDelegate: (AbsoluteUrl, LinkContext?) -> Unit
) : HyperlinkListener {

override fun onReadingOrderLinkActivated(url: Url, context: LinkContext?) {
if (shouldFollowReadingOrderLink(url, context)) {
onPreFollowingReadingOrderLink()
coroutineScope.launch { navigator.goTo(Link(url)) }
}
}

override fun onResourceLinkActivated(url: Url, context: LinkContext?) {
}

override fun onExternalLinkActivated(url: AbsoluteUrl, context: LinkContext?) {
onExternalLinkActivatedDelegate(url, context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package org.readium.navigator.common

import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.times
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.readium.r2.navigator.preferences.ReadingProgression
import org.readium.r2.navigator.util.DirectionalNavigationAdapter
import org.readium.r2.shared.ExperimentalReadiumApi

@ExperimentalReadiumApi
public interface InputListener {
/**
* Called when the user tapped the content, but nothing handled the event internally (eg.
* by following an internal link).
*/
public fun onTap(event: TapEvent, context: TapContext)
}

/**
* Represents a tap event emitted by a navigator at the given [offset].
*
* All the points are relative to the navigator view.
*/
@ExperimentalReadiumApi
public data class TapEvent(
val offset: DpOffset
)

@ExperimentalReadiumApi
public data class TapContext(
val viewport: DpSize
)

@ExperimentalReadiumApi
public object NullInputListener : InputListener {
override fun onTap(event: TapEvent, context: TapContext) {
// Do nothing
}
}

@ExperimentalReadiumApi
@Composable
public fun defaultInputListener(
navigator: Overflowable,
fallbackListener: InputListener? = null,
tapEdges: Set<DirectionalNavigationAdapter.TapEdge> = setOf(
DirectionalNavigationAdapter.TapEdge.Horizontal
),
handleTapsWhileScrolling: Boolean = false,
minimumHorizontalEdgeSize: Dp = 80.0.dp,
horizontalEdgeThresholdPercent: Double? = 0.3,
minimumVerticalEdgeSize: Dp = 80.0.dp,
verticalEdgeThresholdPercent: Double? = 0.3
): InputListener {
val coroutineScope = rememberCoroutineScope()

return DefaultInputListener(
coroutineScope,
fallbackListener,
navigator,
tapEdges,
handleTapsWhileScrolling,
minimumHorizontalEdgeSize,
horizontalEdgeThresholdPercent,
minimumVerticalEdgeSize,
verticalEdgeThresholdPercent
)
}

@OptIn(ExperimentalReadiumApi::class)
private class DefaultInputListener(
private val coroutineScope: CoroutineScope,
private val fallbackListener: InputListener?,
private val navigator: Overflowable,
private val tapEdges: Set<DirectionalNavigationAdapter.TapEdge>,
private val handleTapsWhileScrolling: Boolean,
private val minimumHorizontalEdgeSize: Dp,
private val horizontalEdgeThresholdPercent: Double?,
private val minimumVerticalEdgeSize: Dp,
private val verticalEdgeThresholdPercent: Double?
) : InputListener {

override fun onTap(event: TapEvent, context: TapContext) {
if (!handleTap(event, context)) {
fallbackListener?.onTap(event, context)
}
}

private fun handleTap(event: TapEvent, context: TapContext): Boolean {
if (navigator.overflow.value.scroll && !handleTapsWhileScrolling) {
return false
}

if (tapEdges.contains(DirectionalNavigationAdapter.TapEdge.Horizontal)) {
val width = context.viewport.width

val horizontalEdgeSize = horizontalEdgeThresholdPercent?.let {
max(minimumHorizontalEdgeSize, it * width)
} ?: minimumHorizontalEdgeSize
val leftRange = 0.0.dp..horizontalEdgeSize
val rightRange = (width - horizontalEdgeSize)..width

if (event.offset.x in rightRange && navigator.canMoveRight) {
coroutineScope.launch { navigator.moveRight() }
return true
} else if (event.offset.x in leftRange && navigator.canMoveLeft) {
coroutineScope.launch { navigator.moveLeft() }
return true
}
}

if (tapEdges.contains(DirectionalNavigationAdapter.TapEdge.Vertical)) {
val height = context.viewport.height

val verticalEdgeSize = verticalEdgeThresholdPercent?.let {
max(minimumVerticalEdgeSize, it * height)
} ?: minimumVerticalEdgeSize
val topRange = 0.0.dp..verticalEdgeSize
val bottomRange = (height - verticalEdgeSize)..height

if (event.offset.y in bottomRange && navigator.canMoveForward) {
coroutineScope.launch { navigator.moveForward() }
return true
} else if (event.offset.y in topRange && navigator.canMoveBackward) {
coroutineScope.launch { navigator.moveBackward() }
return true
}
}

return false
}

private val Overflowable.canMoveLeft get() =
when (overflow.value.readingProgression) {
ReadingProgression.LTR ->
canMoveBackward

ReadingProgression.RTL ->
canMoveForward
}

private val Overflowable.canMoveRight get() =
when (overflow.value.readingProgression) {
ReadingProgression.LTR ->
canMoveForward

ReadingProgression.RTL ->
canMoveBackward
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.readium.navigator.common

import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.publication.Locator

@ExperimentalReadiumApi
public interface LocatorAdapter<L : Location, G : GoLocation> {

public fun Locator.toGoLocation(): G

public fun L.toLocator(): Locator
}
Loading
Loading