diff --git a/Makefile b/Makefile index b03c46f2b2..08cf745e90 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ SCRIPTS_PATH := readium/navigator/src/main/assets/_scripts +SCRIPTS_NAVIGATOR_WEB_PATH := readium/navigators/web/scripts help: @echo "Usage: make \n\n\ @@ -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/ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 611a710205..715917ead8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" @@ -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" } diff --git a/readium/navigators/common/build.gradle.kts b/readium/navigators/common/build.gradle.kts new file mode 100644 index 0000000000..723865ffac --- /dev/null +++ b/readium/navigators/common/build.gradle.kts @@ -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) +} diff --git a/readium/navigators/common/gradle.properties b/readium/navigators/common/gradle.properties new file mode 100644 index 0000000000..0b100abff5 --- /dev/null +++ b/readium/navigators/common/gradle.properties @@ -0,0 +1 @@ +pom.artifactId=readium-navigator-common diff --git a/readium/navigators/common/src/main/AndroidManifest.xml b/readium/navigators/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8072ee00db --- /dev/null +++ b/readium/navigators/common/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/readium/navigators/common/src/main/java/org/readium/navigator/common/Configurable.kt b/readium/navigators/common/src/main/java/org/readium/navigator/common/Configurable.kt new file mode 100644 index 0000000000..369a8f2251 --- /dev/null +++ b/readium/navigators/common/src/main/java/org/readium/navigator/common/Configurable.kt @@ -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 { + + public val preferences: MutableState

+ + public val settings: State +} + +@ExperimentalReadiumApi +public typealias Settings = org.readium.r2.navigator.preferences.Configurable.Settings + +@ExperimentalReadiumApi +public typealias Preferences

= org.readium.r2.navigator.preferences.Configurable.Preferences

diff --git a/readium/navigators/common/src/main/java/org/readium/navigator/common/HyperlinkListener.kt b/readium/navigators/common/src/main/java/org/readium/navigator/common/HyperlinkListener.kt new file mode 100644 index 0000000000..ee864c8028 --- /dev/null +++ b/readium/navigators/common/src/main/java/org/readium/navigator/common/HyperlinkListener.kt @@ -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 defaultHyperlinkListener( + navigator: Navigator, + shouldFollowReadingOrderLink: (Url, LinkContext?) -> Boolean = { _, _ -> true }, + // TODO: shouldFollowResourceLink: (Url, LinkContext?) -> Boolean = { _, _ -> true }, + onExternalLinkActivated: (AbsoluteUrl, LinkContext?) -> Unit = { _, _ -> } +): HyperlinkListener { + val coroutineScope = rememberCoroutineScope() + val navigationHistory: MutableState> = 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( + private val coroutineScope: CoroutineScope, + private val navigator: Navigator, + 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) + } +} diff --git a/readium/navigators/common/src/main/java/org/readium/navigator/common/InputListener.kt b/readium/navigators/common/src/main/java/org/readium/navigator/common/InputListener.kt new file mode 100644 index 0000000000..7d93f449a2 --- /dev/null +++ b/readium/navigators/common/src/main/java/org/readium/navigator/common/InputListener.kt @@ -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 = 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, + 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 + } +} diff --git a/readium/navigators/common/src/main/java/org/readium/navigator/common/LocatorAdapter.kt b/readium/navigators/common/src/main/java/org/readium/navigator/common/LocatorAdapter.kt new file mode 100644 index 0000000000..7c54b921d5 --- /dev/null +++ b/readium/navigators/common/src/main/java/org/readium/navigator/common/LocatorAdapter.kt @@ -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 { + + public fun Locator.toGoLocation(): G + + public fun L.toLocator(): Locator +} diff --git a/readium/navigators/common/src/main/java/org/readium/navigator/common/Navigator.kt b/readium/navigators/common/src/main/java/org/readium/navigator/common/Navigator.kt new file mode 100644 index 0000000000..cca12023df --- /dev/null +++ b/readium/navigators/common/src/main/java/org/readium/navigator/common/Navigator.kt @@ -0,0 +1,30 @@ +package org.readium.navigator.common + +import androidx.compose.runtime.State +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.util.Url + +@ExperimentalReadiumApi +public interface Navigator { + + public val location: State + + public suspend fun goTo(location: L) + + public suspend fun goTo(location: G) + + public suspend fun goTo(link: Link) +} + +/** + * Location of the navigator. + */ +@ExperimentalReadiumApi +public interface Location { + + public val href: Url +} + +@ExperimentalReadiumApi +public interface GoLocation diff --git a/readium/navigators/common/src/main/java/org/readium/navigator/common/Overflowable.kt b/readium/navigators/common/src/main/java/org/readium/navigator/common/Overflowable.kt new file mode 100644 index 0000000000..e1f254398f --- /dev/null +++ b/readium/navigators/common/src/main/java/org/readium/navigator/common/Overflowable.kt @@ -0,0 +1,64 @@ +package org.readium.navigator.common + +import androidx.compose.runtime.State +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * A view with content that can extend beyond the viewport. + * + * The user typically navigates through the viewport by scrolling or tapping its edges. + */ +@ExperimentalReadiumApi +public interface Overflowable { + + /** + * Current presentation rendered by the navigator. + */ + public val overflow: State + + public val canMoveForward: Boolean + + public val canMoveBackward: Boolean + + /** + * Moves to the next content portion (eg. page) in the reading progression direction. + */ + public suspend fun moveForward() + + /** + * Moves to the previous content portion (eg. page) in the reading progression direction. + */ + public suspend fun moveBackward() +} + +@ExperimentalReadiumApi +public typealias Overflow = org.readium.r2.navigator.OverflowableNavigator.Overflow + +/** + * Moves to the left content portion (eg. page) relative to the reading progression direction. + */ +@ExperimentalReadiumApi +public suspend fun Overflowable.moveLeft() { + return when (overflow.value.readingProgression) { + ReadingProgression.LTR -> + moveBackward() + + ReadingProgression.RTL -> + moveForward() + } +} + +/** + * Moves to the right content portion (eg. page) relative to the reading progression direction. + */ +@ExperimentalReadiumApi +public suspend fun Overflowable.moveRight() { + return when (overflow.value.readingProgression) { + ReadingProgression.LTR -> + moveForward() + + ReadingProgression.RTL -> + moveBackward() + } +} diff --git a/readium/navigators/common/src/main/java/org/readium/navigator/common/RenditionState.kt b/readium/navigators/common/src/main/java/org/readium/navigator/common/RenditionState.kt new file mode 100644 index 0000000000..b1a2b94ffc --- /dev/null +++ b/readium/navigators/common/src/main/java/org/readium/navigator/common/RenditionState.kt @@ -0,0 +1,9 @@ +package org.readium.navigator.common + +import org.readium.r2.shared.ExperimentalReadiumApi + +@ExperimentalReadiumApi +public interface RenditionState> { + + public val navigator: N? +} diff --git a/readium/navigators/demo/build.gradle.kts b/readium/navigators/demo/build.gradle.kts new file mode 100644 index 0000000000..9cae3baeb5 --- /dev/null +++ b/readium/navigators/demo/build.gradle.kts @@ -0,0 +1,81 @@ +plugins { + id("com.android.application") + kotlin("android") + kotlin("plugin.parcelize") + alias(libs.plugins.ksp) +} + +android { + namespace = "org.readium.navigator.demo" + compileSdk = 34 + + defaultConfig { + applicationId = "org.readium.navigator.demo" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = true + } + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn" + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + } + buildFeatures { + viewBinding = true + compose = true + buildConfig = true + } + packaging { + resources.excludes.add("META-INF/*") + } + + sourceSets { + getByName("main") { + java.srcDirs("src/main/java") + res.srcDirs("src/main/res") + assets.srcDirs("src/main/assets") + } + } +} + +dependencies { + implementation(project(":readium:readium-shared")) + implementation(project(":readium:readium-streamer")) + implementation(project(":readium:readium-navigator")) + implementation(project(":readium:navigators:readium-navigator-web")) + implementation(project(":readium:navigators:readium-navigator-pdf")) + implementation(project(":readium:adapters:pdfium")) + + coreLibraryDesugaring(libs.desugar.jdk.libs) + + implementation(libs.timber) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.legacy.v4) + implementation(libs.bundles.compose) + implementation(libs.androidx.core) + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.fragment.compose) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.browser) +} diff --git a/readium/navigators/demo/src/main/AndroidManifest.xml b/readium/navigators/demo/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b6ae97f20d --- /dev/null +++ b/readium/navigators/demo/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/DemoActivity.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/DemoActivity.kt new file mode 100644 index 0000000000..e5f61d6087 --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/DemoActivity.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2024 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. + */ + +package org.readium.navigator.demo + +import android.net.Uri +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.core.view.WindowCompat +import androidx.fragment.app.FragmentActivity +import org.readium.navigator.demo.reader.Reader +import org.readium.navigator.demo.util.Fullscreenable +import org.readium.navigator.demo.util.Theme +import org.readium.r2.shared.util.toAbsoluteUrl + +class DemoActivity : FragmentActivity() { + + private val viewModel: DemoViewModel by viewModels() + + private val sharedStoragePickerLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> + uri?.let { + val url = requireNotNull(it.toAbsoluteUrl()) + viewModel.onBookSelected(url) + } ?: run { finish() } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + Theme { + val fullscreenState = remember { mutableStateOf(false) } + + Fullscreenable( + fullscreenState = fullscreenState, + insetsController = WindowCompat.getInsetsController(window, window.decorView) + ) { + val snackbarHostState = remember { SnackbarHostState() } + val state = viewModel.state.collectAsState() + + LaunchedEffect(state.value) { + fullscreenState.value = when (state.value) { + DemoViewModel.State.BookSelection -> true + is DemoViewModel.State.Error -> false + DemoViewModel.State.Loading -> true + is DemoViewModel.State.Reader -> true + } + } + + Box( + modifier = Modifier.fillMaxSize() + ) { + when (val stateNow = state.value) { + DemoViewModel.State.BookSelection -> { + Placeholder() + LaunchedEffect(stateNow) { + sharedStoragePickerLauncher.launch(arrayOf("*/*")) + } + } + + is DemoViewModel.State.Error -> { + Placeholder() + LaunchedEffect(stateNow.error) { + snackbarHostState.showSnackbar( + message = stateNow.error.message, + duration = SnackbarDuration.Short + ) + viewModel.onErrorDisplayed() + } + } + + DemoViewModel.State.Loading -> { + Placeholder() + // Display and do nothing + } + + is DemoViewModel.State.Reader -> { + BackHandler { + viewModel.onBookClosed() + } + + Reader( + readerState = stateNow.readerState, + fullScreenState = fullscreenState + ) + } + } + + SnackbarHost( + modifier = Modifier + .align(Alignment.BottomCenter) + .safeDrawingPadding(), + hostState = snackbarHostState + ) + } + } + } + } + } + + // This is useful for setting a background color. + @Composable + private fun Placeholder() { + Surface(modifier = Modifier.fillMaxSize()) {} + } +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/DemoViewModel.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/DemoViewModel.kt new file mode 100644 index 0000000000..30f1b9cadc --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/DemoViewModel.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2024 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. + */ + +package org.readium.navigator.demo + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.readium.navigator.demo.reader.ReaderOpener +import org.readium.navigator.demo.reader.ReaderState +import org.readium.r2.shared.util.AbsoluteUrl +import timber.log.Timber + +class DemoViewModel( + application: Application +) : AndroidViewModel(application) { + + sealed interface State { + + data object BookSelection : + State + + data object Loading : + State + + data class Error( + val error: org.readium.r2.shared.util.Error + ) : State + + data class Reader( + val readerState: ReaderState<*, *> + ) : State + } + + init { + Timber.plant(Timber.DebugTree()) + } + + private val readerOpener = + ReaderOpener(application) + + private val stateMutable: MutableStateFlow = + MutableStateFlow(State.BookSelection) + + val state: StateFlow = stateMutable.asStateFlow() + + fun onBookSelected(url: AbsoluteUrl) { + stateMutable.value = State.Loading + + viewModelScope.launch { + readerOpener.open(url) + .onFailure { stateMutable.value = State.Error(it) } + .onSuccess { stateMutable.value = State.Reader(it) } + } + } + + fun onBookClosed() { + val stateNow = state.value + check(stateNow is State.Reader) + stateMutable.value = State.BookSelection + stateNow.readerState.close() + } + + fun onErrorDisplayed() { + stateMutable.value = State.BookSelection + } +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/persistence/LocatorRepository.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/persistence/LocatorRepository.kt new file mode 100644 index 0000000000..3614b37f2d --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/persistence/LocatorRepository.kt @@ -0,0 +1,18 @@ +package org.readium.navigator.demo.persistence + +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.AbsoluteUrl + +object LocatorRepository { + + private val savedLocators: MutableMap = + mutableMapOf() + + fun saveLocator(url: AbsoluteUrl, locator: Locator) { + savedLocators[url] = locator + } + + fun getLocator(url: AbsoluteUrl): Locator? { + return savedLocators[url] + } +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/PreferenceViews.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/PreferenceViews.kt new file mode 100644 index 0000000000..37d0cf25ec --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/PreferenceViews.kt @@ -0,0 +1,239 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalReadiumApi::class) + +package org.readium.navigator.demo.preferences + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Backspace +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.readium.navigator.demo.util.Group +import org.readium.navigator.demo.util.ToggleButtonGroup +import org.readium.r2.navigator.preferences.EnumPreference +import org.readium.r2.navigator.preferences.Preference +import org.readium.r2.navigator.preferences.RangePreference +import org.readium.r2.navigator.preferences.clear +import org.readium.r2.navigator.preferences.toggle +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * Component for an [EnumPreference] displayed as a group of mutually exclusive buttons. + * This works best with a small number of enum values. + */ +@Composable +fun ButtonGroupItem( + title: String, + preference: EnumPreference, + commit: () -> Unit, + formatValue: (T) -> String +) { + ButtonGroupItem( + title = title, + options = preference.supportedValues, + isActive = preference.isEffective, + activeOption = preference.effectiveValue, + selectedOption = preference.value, + formatValue = formatValue, + onClear = { preference.clear(); commit() } + .takeIf { preference.value != null }, + onSelectedOptionChanged = { newValue -> + if (newValue == preference.value) { + preference.clear() + } else { + preference.set(newValue) + } + commit() + } + ) +} + +/** + * Group of mutually exclusive buttons. + */ +@Composable +private fun ButtonGroupItem( + title: String, + options: List, + isActive: Boolean, + activeOption: T, + selectedOption: T?, + formatValue: (T) -> String, + onClear: (() -> Unit)?, + onSelectedOptionChanged: (T) -> Unit +) { + Item(title, isActive = isActive, onClear = onClear) { + ToggleButtonGroup( + options = options, + activeOption = activeOption, + selectedOption = selectedOption, + onSelectOption = { option -> onSelectedOptionChanged(option) } + ) { option -> + Text( + text = formatValue(option), + style = MaterialTheme.typography.labelSmall + ) + } + } +} + +/** + * Component for a [RangePreference] with decrement and increment buttons. + */ +@Composable +fun > StepperItem( + title: String, + preference: RangePreference, + commit: () -> Unit +) { + StepperItem( + title = title, + isActive = preference.isEffective, + value = preference.value ?: preference.effectiveValue, + formatValue = preference::formatValue, + onDecrement = { preference.decrement(); commit() }, + onIncrement = { preference.increment(); commit() }, + onClear = { preference.clear(); commit() } + .takeIf { preference.value != null } + ) +} + +/** + * Component for a [RangePreference] with decrement and increment buttons. + */ +@Composable +private fun StepperItem( + title: String, + isActive: Boolean, + value: T, + formatValue: (T) -> String, + onDecrement: () -> Unit, + onIncrement: () -> Unit, + onClear: (() -> Unit)? +) { + Item(title, isActive = isActive, onClear = onClear) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + IconButton( + onClick = onDecrement, + content = { + Icon(Icons.Default.Remove, contentDescription = "Less") + } + ) + + Text( + text = formatValue(value), + modifier = Modifier.widthIn(min = 30.dp), + textAlign = TextAlign.Center + ) + + IconButton( + onClick = onIncrement, + content = { + Icon(Icons.Default.Add, contentDescription = "More") + } + ) + } + } +} + +/** + * Component for a boolean [Preference]. + */ +@Composable +fun SwitchItem( + title: String, + preference: Preference, + commit: () -> Unit +) { + SwitchItem( + title = title, + value = preference.value ?: preference.effectiveValue, + isActive = preference.isEffective, + onCheckedChange = { preference.set(it); commit() }, + onToggle = { preference.toggle(); commit() }, + onClear = { preference.clear(); commit() } + .takeIf { preference.value != null } + ) +} + +/** + * Switch + */ +@Composable +private fun SwitchItem( + title: String, + value: Boolean, + isActive: Boolean, + onCheckedChange: (Boolean) -> Unit, + onToggle: () -> Unit, + onClear: (() -> Unit)? +) { + Item( + title = title, + isActive = isActive, + onClick = onToggle, + onClear = onClear + ) { + Switch( + checked = value, + onCheckedChange = onCheckedChange + ) + } +} + +@Composable +private fun Item( + title: String, + isActive: Boolean = true, + onClick: (() -> Unit)? = null, + onClear: (() -> Unit)? = null, + content: @Composable () -> Unit +) { + ListItem( + modifier = + if (onClick != null) { + Modifier.clickable(onClick = onClick) + } else { + Modifier + }, + headlineContent = { + Group(enabled = isActive) { + Text(title) + } + }, + trailingContent = { + Row { + content() + + IconButton(onClick = onClear ?: {}, enabled = onClear != null) { + Icon( + Icons.AutoMirrored.Filled.Backspace, + contentDescription = "Clear" + ) + } + } + } + ) +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/PreferencesManager.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/PreferencesManager.kt new file mode 100644 index 0000000000..ba6d43ef32 --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/PreferencesManager.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalReadiumApi::class) + +package org.readium.navigator.demo.preferences + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.readium.r2.navigator.preferences.Configurable +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * Trivial user preferences manager. You can add persistence. + */ +class PreferencesManager

>( + initialPreferences: P +) { + private val preferencesMutable: MutableStateFlow

= + MutableStateFlow(initialPreferences) + + val preferences: StateFlow

= + preferencesMutable.asStateFlow() + + fun setPreferences(preferences: P) { + preferencesMutable.value = preferences + } +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/UserPreferences.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/UserPreferences.kt new file mode 100644 index 0000000000..28a0880474 --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/UserPreferences.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalReadiumApi::class) + +package org.readium.navigator.demo.preferences + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.readium.adapter.pdfium.navigator.PdfiumPreferencesEditor +import org.readium.navigator.web.preferences.FixedWebPreferencesEditor +import org.readium.r2.navigator.preferences.Axis +import org.readium.r2.navigator.preferences.Configurable +import org.readium.r2.navigator.preferences.EnumPreference +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.navigator.preferences.Preference +import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.navigator.preferences.RangePreference +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * Stateful user settings component. + */ +@Composable +fun UserPreferences( + model: UserPreferencesViewModel<*, *>, + title: String +) { + val editor by model.editor.collectAsState() + + UserPreferences( + editor = editor, + commit = model::commit, + title = title + ) +} + +@Composable +private fun

, E : PreferencesEditor

> UserPreferences( + editor: E, + commit: () -> Unit, + title: String +) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .verticalScroll(scrollState) + .padding(vertical = 24.dp) + ) { + Text( + text = title, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .fillMaxWidth() + ) + + Row( + modifier = Modifier + .padding(16.dp) + .align(Alignment.End), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = { editor.clear(); commit() } + ) { + Text("Reset") + } + } + + Divider() + + when (editor) { + is FixedWebPreferencesEditor -> + FixedLayoutUserPreferences( + commit = commit, + readingProgression = editor.readingProgression, + fit = editor.fit, + spreads = editor.spreads + ) + is PdfiumPreferencesEditor -> + FixedLayoutUserPreferences( + commit = commit, + readingProgression = editor.readingProgression, + scrollAxis = editor.scrollAxis, + fit = editor.fit, + pageSpacing = editor.pageSpacing + ) + } + } +} + +/** + * User preferences for a publication with a fixed layout, such as fixed-layout EPUB, PDF or comic book. + */ +@Composable +private fun FixedLayoutUserPreferences( + commit: () -> Unit, + readingProgression: EnumPreference? = null, + scrollAxis: EnumPreference? = null, + fit: EnumPreference? = null, + spreads: Preference? = null, + offsetFirstPage: Preference? = null, + pageSpacing: RangePreference? = null +) { + if (readingProgression != null) { + ButtonGroupItem( + title = "Reading progression", + preference = readingProgression, + commit = commit, + formatValue = { it.name } + ) + + Divider() + } + + if (scrollAxis != null) { + ButtonGroupItem( + title = "Scroll axis", + preference = scrollAxis, + commit = commit + ) { value -> + when (value) { + Axis.HORIZONTAL -> "Horizontal" + Axis.VERTICAL -> "Vertical" + } + } + } + + if (spreads != null) { + SwitchItem( + title = "Spreads", + preference = spreads, + commit = commit + ) + + if (offsetFirstPage != null) { + SwitchItem( + title = "Offset", + preference = offsetFirstPage, + commit = commit + ) + } + } + + if (fit != null) { + ButtonGroupItem( + title = "Fit", + preference = fit, + commit = commit + ) { value -> + when (value) { + Fit.CONTAIN -> "Contain" + Fit.COVER -> "Cover" + Fit.WIDTH -> "Width" + Fit.HEIGHT -> "Height" + } + } + } + + if (pageSpacing != null) { + StepperItem( + title = "Page spacing", + preference = pageSpacing, + commit = commit + ) + } +} + +@Composable +private fun Divider() { + HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp)) +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/UserPreferencesViewModel.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/UserPreferencesViewModel.kt new file mode 100644 index 0000000000..ae695730bb --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/preferences/UserPreferencesViewModel.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 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. + */ + +package org.readium.navigator.demo.preferences + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import org.readium.navigator.common.Preferences +import org.readium.navigator.common.Settings +import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.extensions.mapStateIn + +@OptIn(ExperimentalReadiumApi::class, InternalReadiumApi::class) +class UserPreferencesViewModel>( + private val viewModelScope: CoroutineScope, + private val preferencesManager: PreferencesManager

, + createPreferencesEditor: (P) -> PreferencesEditor

+) { + val preferences: StateFlow

= preferencesManager.preferences + + val editor: StateFlow> = preferences + .mapStateIn(viewModelScope, createPreferencesEditor) + + fun commit() { + viewModelScope.launch { + preferencesManager.setPreferences(editor.value.preferences) + } + } +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/reader/Reader.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/reader/Reader.kt new file mode 100644 index 0000000000..5d27fddda0 --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/reader/Reader.kt @@ -0,0 +1,198 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalReadiumApi::class) + +package org.readium.navigator.demo.reader + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.zIndex +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.readium.navigator.common.InputListener +import org.readium.navigator.common.Location +import org.readium.navigator.common.LocatorAdapter +import org.readium.navigator.common.Navigator +import org.readium.navigator.common.NullHyperlinkListener +import org.readium.navigator.common.Overflowable +import org.readium.navigator.common.RenditionState +import org.readium.navigator.common.TapContext +import org.readium.navigator.common.TapEvent +import org.readium.navigator.common.defaultHyperlinkListener +import org.readium.navigator.common.defaultInputListener +import org.readium.navigator.demo.persistence.LocatorRepository +import org.readium.navigator.demo.preferences.UserPreferences +import org.readium.navigator.demo.preferences.UserPreferencesViewModel +import org.readium.navigator.demo.util.launchWebBrowser +import org.readium.navigator.web.FixedWebRendition +import org.readium.navigator.web.FixedWebRenditionState +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.toUri + +data class ReaderState>( + val url: AbsoluteUrl, + val coroutineScope: CoroutineScope, + val publication: Publication, + val renditionState: RenditionState, + val preferencesViewModel: UserPreferencesViewModel<*, *>, + val locatorAdapter: LocatorAdapter, + val onNavigatorCreated: (N) -> Unit +) { + + fun close() { + coroutineScope.cancel() + publication.close() + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun > Reader( + readerState: ReaderState, + fullScreenState: MutableState +) { + val showPreferences = remember { mutableStateOf(false) } + val preferencesSheetState = rememberModalBottomSheetState() + + if (showPreferences.value) { + ModalBottomSheet( + sheetState = preferencesSheetState, + onDismissRequest = { showPreferences.value = false } + ) { + UserPreferences( + model = readerState.preferencesViewModel, + title = "Preferences" + ) + } + } + + Box { + TopBar( + modifier = Modifier.zIndex(1f), + visible = !fullScreenState.value, + onPreferencesActivated = { showPreferences.value = !showPreferences.value } + ) + + val navigatorNow = readerState.renditionState.navigator + + if (navigatorNow != null) { + LaunchedEffect(navigatorNow) { + readerState.onNavigatorCreated(navigatorNow) + } + + LaunchedEffect(navigatorNow) { + snapshotFlow { + navigatorNow.location.value + }.onEach { + val locator = with(readerState.locatorAdapter) { it.toLocator() } + LocatorRepository.saveLocator(readerState.url, locator) + }.launchIn(readerState.coroutineScope) + } + } + + val fallbackInputListener = remember { + object : InputListener { + override fun onTap(event: TapEvent, context: TapContext) { + fullScreenState.value = !fullScreenState.value + } + } + } + + val inputListener = + if (navigatorNow == null) { + fallbackInputListener + } else { + (navigatorNow as? Overflowable)?.let { + defaultInputListener( + navigator = it, + fallbackListener = fallbackInputListener + ) + } ?: fallbackInputListener + } + + val hyperlinkListener = + if (navigatorNow == null) { + NullHyperlinkListener + } else { + val context = LocalContext.current + defaultHyperlinkListener( + navigator = navigatorNow, + onExternalLinkActivated = { url, _ -> launchWebBrowser(context, url.toUri()) } + ) + } + + when (readerState.renditionState) { + is FixedWebRenditionState -> { + FixedWebRendition( + modifier = Modifier.fillMaxSize(), + state = readerState.renditionState, + inputListener = inputListener, + hyperlinkListener = hyperlinkListener + ) + } + /*is PdfNavigatorState<*, *> -> { + PdfNavigator( + modifier = Modifier.fillMaxSize(), + state = state.navigatorState, + inputListener = inputListener + ) + }*/ + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + modifier: Modifier, + visible: Boolean, + onPreferencesActivated: () -> Unit +) { + AnimatedVisibility( + modifier = modifier, + visible = visible, + enter = fadeIn(), + exit = fadeOut() + ) { + TopAppBar( + title = {}, + actions = { + IconButton( + onClick = onPreferencesActivated + ) { + Icon( + imageVector = Icons.Filled.Settings, + contentDescription = "Preferences" + ) + } + } + ) + } +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/reader/ReaderOpener.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/reader/ReaderOpener.kt new file mode 100644 index 0000000000..84bde3c3ef --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/reader/ReaderOpener.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalReadiumApi::class) + +package org.readium.navigator.demo.reader + +import android.app.Application +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.readium.adapter.pdfium.document.PdfiumDocumentFactory +import org.readium.adapter.pdfium.navigator.PdfiumEngineProvider +import org.readium.navigator.demo.persistence.LocatorRepository +import org.readium.navigator.demo.preferences.PreferencesManager +import org.readium.navigator.demo.preferences.UserPreferencesViewModel +import org.readium.navigator.web.FixedWebNavigator +import org.readium.navigator.web.FixedWebNavigatorFactory +import org.readium.navigator.web.location.FixedWebLocation +import org.readium.navigator.web.preferences.FixedWebPreferences +import org.readium.navigator.web.preferences.FixedWebSettings +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.http.DefaultHttpClient +import org.readium.r2.streamer.PublicationOpener +import org.readium.r2.streamer.parser.DefaultPublicationParser + +class ReaderOpener( + private val application: Application +) { + + private val httpClient = + DefaultHttpClient() + + private val assetRetriever = + AssetRetriever(application.contentResolver, httpClient) + + private val pdfiumDocumentFactory = + PdfiumDocumentFactory(application) + + private val publicationParser = + DefaultPublicationParser(application, httpClient, assetRetriever, pdfiumDocumentFactory) + + private val publicationOpener = + PublicationOpener(publicationParser) + + private val pdfEngineProvider = + PdfiumEngineProvider() + + suspend fun open(url: AbsoluteUrl): Try, Error> { + val asset = assetRetriever.retrieve(url) + .getOrElse { return Try.failure(it) } + + val publication = publicationOpener.open(asset, allowUserInteraction = false) + .getOrElse { + asset.close() + return Try.failure(it) + } + + val initialLocator = LocatorRepository.getLocator(url) + + val readerState = when { + publication.conformsTo(Publication.Profile.EPUB) -> + createFixedWebReader(url, publication, initialLocator) + + /*publication.conformsTo(Publication.Profile.PDF) -> + createPdfReader(url, publication, initialLocator)*/ + + else -> + Try.failure(DebugError("Publication not supported")) + }.getOrElse { error -> + publication.close() + return Try.failure(error) + } + + return Try.success(readerState) + } + + private suspend fun createFixedWebReader( + url: AbsoluteUrl, + publication: Publication, + initialLocator: Locator? + ): Try, Error> { + val navigatorFactory = FixedWebNavigatorFactory(application, publication) + ?: return Try.failure(DebugError("Publication not supported")) + + val initialPreferences = FixedWebPreferences() + + val locatorAdapter = navigatorFactory.createLocatorAdapter() + + val initialLocation = with(locatorAdapter) { initialLocator?.toGoLocation() } + + val navigatorState = navigatorFactory.createRenditionState( + initialLocation = initialLocation, + initialPreferences = initialPreferences + ).getOrElse { + return Try.failure(it) + } + + val coroutineScope = MainScope() + + val preferencesViewModel = + UserPreferencesViewModel( + viewModelScope = coroutineScope, + preferencesManager = PreferencesManager(initialPreferences), + createPreferencesEditor = navigatorFactory::createPreferencesEditor + ) + + val onNavigatorCreated: (FixedWebNavigator) -> Unit = { navigator -> + preferencesViewModel.preferences + .onEach { navigator.preferences.value = it } + .launchIn(coroutineScope) + } + + val readerState = ReaderState( + url = url, + coroutineScope = coroutineScope, + publication = publication, + renditionState = navigatorState, + preferencesViewModel = preferencesViewModel, + locatorAdapter = locatorAdapter, + onNavigatorCreated = onNavigatorCreated + ) + + return Try.success(readerState) + } +/* + private fun createPdfReader( + url: AbsoluteUrl, + publication: Publication, + initialLocator: Locator? + ): Try, Error> { + val navigatorFactory = PdfNavigatorFactory(publication, pdfEngineProvider) + + val initialPreferences = PdfiumPreferences() + + val navigatorState = navigatorFactory.createNavigator( + initialLocator = initialLocator, + initialPreferences = initialPreferences + ).getOrElse { + throw IllegalStateException() + } + + val coroutineScope = MainScope() + + val preferencesViewModel = + UserPreferencesViewModel( + viewModelScope = coroutineScope, + preferencesManager = PreferencesManager(initialPreferences), + createPreferencesEditor = navigatorFactory::createPreferencesEditor + ) + + preferencesViewModel.preferences + .onEach { navigatorState.preferences.value = it } + .launchIn(coroutineScope) + + val locatorAdapter = navigatorFactory.createLocatorAdapter() + + val readerState = ReaderState( + url = url, + coroutineScope = coroutineScope, + publication = publication, + navigatorState = navigatorState, + preferencesViewModel = preferencesViewModel, + locatorAdapter = locatorAdapter + ) + + return Try.success(readerState) + } + */ +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/EmphasisProvider.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/EmphasisProvider.kt new file mode 100644 index 0000000000..c22f960388 --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/EmphasisProvider.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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. + */ + +package org.readium.navigator.demo.util + +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ProvidedValue +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight + +val LocalContentEmphasis = compositionLocalOf { Emphasis.Medium } + +enum class Emphasis { + Disabled, + Medium, + High +} + +@Composable +fun EmphasisProvider(emphasis: ProvidedValue, content: @Composable () -> Unit) { + val contentColor = when (emphasis.value) { + Emphasis.Disabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + Emphasis.Medium -> MaterialTheme.colorScheme.onSurfaceVariant + Emphasis.High -> MaterialTheme.colorScheme.onSurface + } + val fontWeight = when (emphasis.value) { + Emphasis.High -> FontWeight.Bold + else -> FontWeight.Normal + } + + CompositionLocalProvider( + LocalContentColor provides contentColor, + content = { + ProvideTextStyle(value = TextStyle(fontWeight = fontWeight)) { + content() + } + } + ) +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/Fullscreenable.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/Fullscreenable.kt new file mode 100644 index 0000000000..49a027a5f8 --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/Fullscreenable.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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. + */ + +package org.readium.navigator.demo.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat + +@Composable +fun Fullscreenable( + fullscreenState: State, + insetsController: WindowInsetsControllerCompat, + content: @Composable () -> Unit +) { + insetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + + LaunchedEffect(fullscreenState.value) { + if (fullscreenState.value) { + insetsController.hide(WindowInsetsCompat.Type.systemBars()) + } else { + insetsController.show(WindowInsetsCompat.Type.systemBars()) + } + } + + content.invoke() +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/Group.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/Group.kt new file mode 100644 index 0000000000..c4b2d88dd9 --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/Group.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 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. + */ + +package org.readium.navigator.demo.util + +import androidx.compose.runtime.Composable + +/** + * Sets the emphasis (alpha) of a group of [Composable] views. + */ +@Composable +fun Group(enabled: Boolean = true, content: @Composable () -> Unit) { + val emphasis = when { + !enabled -> Emphasis.Disabled + else -> Emphasis.Medium + } + EmphasisProvider(LocalContentEmphasis provides emphasis) { + content() + } +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/Theme.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/Theme.kt new file mode 100644 index 0000000000..0965f29764 --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/Theme.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 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. + */ + +package org.readium.navigator.demo.util + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +@Composable +fun Theme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) { + darkColorScheme() + } else { + lightColorScheme() + } + + MaterialTheme( + colorScheme = colors, + shapes = MaterialTheme.shapes, + typography = MaterialTheme.typography, + content = content + ) +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/ToggleButton.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/ToggleButton.kt new file mode 100644 index 0000000000..8650c78f7a --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/ToggleButton.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2024 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. + */ + +package org.readium.navigator.demo.util + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +// A very basic implementation of Material Design's Toggle Button for Compose. +// https://material.io/components/buttons#toggle-button + +@Composable +fun ToggleButtonGroup( + options: List, + activeOption: T?, + selectedOption: T?, + onSelectOption: (T) -> Unit, + enabled: (T) -> Boolean = { true }, + content: @Composable RowScope.(T) -> Unit +) { + Row { + for (option in options) { + ToggleButton( + enabled = enabled(option), + active = activeOption == option, + selected = selectedOption == option, + onClick = { onSelectOption(option) }, + content = { content(option) } + ) + } + } +} + +@Composable +fun ToggleButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + enabled: Boolean = true, + active: Boolean = false, + selected: Boolean = false, + content: @Composable RowScope.() -> Unit +) { + OutlinedButton( + modifier = modifier, + onClick = onClick, + enabled = enabled, + content = content, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onBackground, + containerColor = when { + selected -> + MaterialTheme.colorScheme.onBackground + .copy(alpha = 0.15f) + .compositeOver(MaterialTheme.colorScheme.background) + active -> + MaterialTheme.colorScheme.onBackground + .copy(alpha = 0.05f) + .compositeOver(MaterialTheme.colorScheme.background) + else -> MaterialTheme.colorScheme.surface + } + ), + elevation = + if (selected) { + ButtonDefaults.buttonElevation(defaultElevation = 2.dp) + } else { + null + } + ) +} + +@Composable +@Preview(showBackground = true) +fun PreviewToggleButtonGroup() { + ToggleButtonGroup( + options = listOf("1", "2", "3"), + activeOption = "2", + selectedOption = "2", + onSelectOption = {} + ) { option -> + Text(text = option) + } +} diff --git a/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/WebLauncher.kt b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/WebLauncher.kt new file mode 100644 index 0000000000..ef8fc5cb5e --- /dev/null +++ b/readium/navigators/demo/src/main/java/org/readium/navigator/demo/util/WebLauncher.kt @@ -0,0 +1,35 @@ +package org.readium.navigator.demo.util + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.webkit.URLUtil +import androidx.browser.customtabs.CustomTabsIntent +import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.extensions.tryOrLog + +/** + * Opens the given [uri] with a Chrome Custom Tab or the system browser as a fallback. + */ +@OptIn(InternalReadiumApi::class) +fun launchWebBrowser(context: Context, uri: Uri) { + var url = uri + if (url.scheme == null) { + url = url.buildUpon().scheme("http").build() + } + + if (!URLUtil.isNetworkUrl(url.toString())) { + return + } + + tryOrLog { + try { + CustomTabsIntent.Builder() + .build() + .launchUrl(context, url) + } catch (e: ActivityNotFoundException) { + context.startActivity(Intent(Intent.ACTION_VIEW, url)) + } + } +} diff --git a/readium/navigators/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml b/readium/navigators/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..6107c771b7 --- /dev/null +++ b/readium/navigators/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/readium/navigators/demo/src/main/res/drawable/ic_launcher_background.xml b/readium/navigators/demo/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..6b753e2187 --- /dev/null +++ b/readium/navigators/demo/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/readium/navigators/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/readium/navigators/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..6f3b755bf5 --- /dev/null +++ b/readium/navigators/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/readium/navigators/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/readium/navigators/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..6f3b755bf5 --- /dev/null +++ b/readium/navigators/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/readium/navigators/demo/src/main/res/mipmap-hdpi/ic_launcher.webp b/readium/navigators/demo/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000..c209e78ecd Binary files /dev/null and b/readium/navigators/demo/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/readium/navigators/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/readium/navigators/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..b2dfe3d1ba Binary files /dev/null and b/readium/navigators/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/readium/navigators/demo/src/main/res/mipmap-mdpi/ic_launcher.webp b/readium/navigators/demo/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000..4f0f1d64e5 Binary files /dev/null and b/readium/navigators/demo/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/readium/navigators/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/readium/navigators/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..62b611da08 Binary files /dev/null and b/readium/navigators/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/readium/navigators/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp b/readium/navigators/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000..948a3070fe Binary files /dev/null and b/readium/navigators/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/readium/navigators/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/readium/navigators/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..1b9a6956b3 Binary files /dev/null and b/readium/navigators/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/readium/navigators/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/readium/navigators/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000..28d4b77f9f Binary files /dev/null and b/readium/navigators/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/readium/navigators/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/readium/navigators/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..9287f50836 Binary files /dev/null and b/readium/navigators/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/readium/navigators/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/readium/navigators/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000..aa7d6427e6 Binary files /dev/null and b/readium/navigators/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/readium/navigators/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/readium/navigators/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..9126ae37cb Binary files /dev/null and b/readium/navigators/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/readium/navigators/demo/src/main/res/values/colors.xml b/readium/navigators/demo/src/main/res/values/colors.xml new file mode 100644 index 0000000000..045e125f3d --- /dev/null +++ b/readium/navigators/demo/src/main/res/values/colors.xml @@ -0,0 +1,3 @@ + + + diff --git a/readium/navigators/demo/src/main/res/values/strings.xml b/readium/navigators/demo/src/main/res/values/strings.xml new file mode 100644 index 0000000000..19f975c416 --- /dev/null +++ b/readium/navigators/demo/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Readium Navigator Demo + diff --git a/readium/navigators/demo/src/main/res/xml/network_security_config.xml b/readium/navigators/demo/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..b45ff69724 --- /dev/null +++ b/readium/navigators/demo/src/main/res/xml/network_security_config.xml @@ -0,0 +1,10 @@ + + + + developer.android.com + google.github.io + android-developers.googleblog.com + io.google + + + diff --git a/readium/navigators/pdf/build.gradle.kts b/readium/navigators/pdf/build.gradle.kts new file mode 100644 index 0000000000..2b2698d313 --- /dev/null +++ b/readium/navigators/pdf/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * 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.pdf" + + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + } + buildFeatures { + compose = true + } +} + +dependencies { + api(project(":readium:readium-shared")) + api(project(":readium:readium-navigator")) + api(project(":readium:navigators:readium-navigator-common")) + + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.fragment.compose) + implementation(libs.kotlinx.serialization.json) + implementation(libs.bundles.compose) + implementation(libs.timber) + implementation(libs.kotlinx.coroutines.android) +} diff --git a/readium/navigators/pdf/gradle.properties b/readium/navigators/pdf/gradle.properties new file mode 100644 index 0000000000..952f725727 --- /dev/null +++ b/readium/navigators/pdf/gradle.properties @@ -0,0 +1 @@ +pom.artifactId=readium-navigator-pdf diff --git a/readium/navigators/pdf/src/main/AndroidManifest.xml b/readium/navigators/pdf/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8072ee00db --- /dev/null +++ b/readium/navigators/pdf/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfLocations.kt b/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfLocations.kt new file mode 100644 index 0000000000..6ae89f486e --- /dev/null +++ b/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfLocations.kt @@ -0,0 +1,42 @@ +package org.readium.navigator.pdf + +import org.readium.navigator.common.GoLocation +import org.readium.navigator.common.Location +import org.readium.navigator.common.LocatorAdapter +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.util.Url + +@ExperimentalReadiumApi +public sealed interface PdfGoLocation : GoLocation + +@ExperimentalReadiumApi +public data class PageLocation( + override val href: Url, + val page: Int +) : Location, PdfGoLocation + +@ExperimentalReadiumApi +public data class PositionLocation( + val position: Int +) : PdfGoLocation + +@ExperimentalReadiumApi +public data class PdfLocation( + override val href: Url, + val page: Int +) : Location + +@ExperimentalReadiumApi +public class PdfLocatorAdapter( + private val publication: Publication +) : LocatorAdapter { + override fun Locator.toGoLocation(): PdfGoLocation = + PositionLocation(position = locations.position!!) + + override fun PdfLocation.toLocator(): Locator = + publication.locatorFromLink(Link(href))!! + .copyWithLocations(position = page) +} diff --git a/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfNavigator.kt b/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfNavigator.kt new file mode 100644 index 0000000000..022689297d --- /dev/null +++ b/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfNavigator.kt @@ -0,0 +1,175 @@ +package org.readium.navigator.pdf + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.compose.runtime.Composable +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentContainerView +import androidx.fragment.app.FragmentFactory +import androidx.fragment.app.viewModels +import androidx.fragment.compose.AndroidFragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.readium.navigator.common.InputListener +import org.readium.navigator.common.Preferences +import org.readium.navigator.common.Settings +import org.readium.navigator.common.TapContext +import org.readium.r2.navigator.input.TapEvent +import org.readium.r2.navigator.pdf.PdfNavigatorFactory +import org.readium.r2.navigator.pdf.PdfNavigatorFragment +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Locator + +@ExperimentalReadiumApi +@Composable +public fun > PdfNavigator( + modifier: Modifier = Modifier, + state: PdfNavigatorState, + inputListener: InputListener +) { + val preferencesFlow = snapshotFlow { state.preferences.value } + + val pendingPositionFlow = snapshotFlow { state.pendingLocator.value } + + AndroidFragment>( + modifier = modifier, + onUpdate = { + val onFragmentCreated: (PdfNavigatorFragment) -> Unit = { fragment -> + + val legacyInputListener = + object : org.readium.r2.navigator.input.InputListener { + override fun onTap(event: TapEvent): Boolean { + val viewport = DpSize( + width = fragment.publicationView.width.toFloat().dp, + height = fragment.publicationView.height.toFloat().dp + ) + val offset = DpOffset(event.point.x.dp, event.point.y.dp) + val dpEvent = org.readium.navigator.common.TapEvent(offset) + inputListener.onTap(dpEvent, TapContext(viewport)) + return true + } + } + fragment.addInputListener(legacyInputListener) + + fragment.currentLocator + .onEach { locator -> + state.locator.value = locator + }.launchIn(fragment.lifecycleScope) + + preferencesFlow + .onEach { preferences -> fragment.submitPreferences(preferences) } + .launchIn(fragment.lifecycleScope) + + pendingPositionFlow + .onEach { locator -> + if (locator != null) { + fragment.go(locator) + state.pendingLocator.value = null + } + } + .launchIn(fragment.lifecycleScope) + } + + it.setNavigatorFactory( + state.pdfNavigatorFactory, + state.locator.value, + state.preferences.value, + onFragmentCreated + ) + } + ) +} + +@OptIn(ExperimentalReadiumApi::class) +public // Visible for Android +class PdfFragment> : Fragment() { + + private val viewModel: PdfFragmentViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + childFragmentManager.fragmentFactory = viewModel.fragmentFactory + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val fragmentContainer = FragmentContainerView(requireContext()) + fragmentContainer.layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + fragmentContainer.id = viewModel.navigatorViewId + return fragmentContainer + } + + internal fun setNavigatorFactory( + navigatorFactory: PdfNavigatorFactory, + locator: Locator, + preferences: P, + onFragmentCreated: (fragment: PdfNavigatorFragment) -> Unit + ) { + viewModel.fragmentFactory.factory = + navigatorFactory.createFragmentFactory( + initialLocator = locator, + initialPreferences = preferences + ) + + childFragmentManager.beginTransaction() + .replace( + viewModel.navigatorViewId, + PdfNavigatorFragment::class.java, + Bundle(), + "PdfNavigator" + ) + .commitNow() + + @Suppress("Unchecked_cast") + val fragment = childFragmentManager.fragments[0] as PdfNavigatorFragment + onFragmentCreated(fragment) + } +} + +@ExperimentalReadiumApi +public class PdfFragmentViewModel : ViewModel() { + + internal val navigatorViewId: Int = View.generateViewId() + + internal val fragmentFactory = MutableFragmentFactory(CoolFragmentFactory()) +} + +private class CoolFragmentFactory : FragmentFactory() { + override fun instantiate(classLoader: ClassLoader, className: String): Fragment { + return object : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return FrameLayout(requireContext()) + } + } + } +} + +public class MutableFragmentFactory(public var factory: FragmentFactory? = null) : FragmentFactory() { + override fun instantiate(classLoader: ClassLoader, className: String): Fragment = + when (val factoryNow = factory) { + null -> super.instantiate(classLoader, className) + else -> factoryNow.instantiate(classLoader, className) + } +} diff --git a/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfNavigatorFactory.kt b/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfNavigatorFactory.kt new file mode 100644 index 0000000000..0deaa4e91b --- /dev/null +++ b/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfNavigatorFactory.kt @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package org.readium.navigator.pdf + +import org.readium.navigator.common.Preferences +import org.readium.navigator.common.Settings +import org.readium.r2.navigator.pdf.PdfEngineProvider +import org.readium.r2.navigator.pdf.PdfNavigatorFactory +import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Try + +@ExperimentalReadiumApi +public class PdfNavigatorFactory, E : PreferencesEditor

>( + private val publication: Publication, + private val pdfEngineProvider: PdfEngineProvider +) { + + public fun createNavigator( + initialLocator: Locator? = null, + initialPreferences: P? = null + ): Try, Nothing> { + val actualInitialLocator = initialLocator + ?: publication.locatorFromLink(publication.readingOrder[0])!! + + val actualInitialPreferences = initialPreferences + ?: pdfEngineProvider.createEmptyPreferences() + + val legacyNavigatorFactory = + PdfNavigatorFactory(publication, pdfEngineProvider) + + val settingsResolver = { preferences: P -> + pdfEngineProvider.computeSettings(publication.metadata, preferences) + } + + val navigatorState = + PdfNavigatorState( + legacyNavigatorFactory, + settingsResolver, + actualInitialLocator, + actualInitialPreferences + ) + + return Try.success(navigatorState) + } + + public fun createPreferencesEditor( + currentPreferences: P + ): E = + pdfEngineProvider.createPreferenceEditor(publication, currentPreferences) + + public fun createLocatorAdapter(): PdfLocatorAdapter = + PdfLocatorAdapter(publication) +} diff --git a/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfNavigatorState.kt b/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfNavigatorState.kt new file mode 100644 index 0000000000..dcc903c41a --- /dev/null +++ b/readium/navigators/pdf/src/main/java/org/readium/navigator/pdf/PdfNavigatorState.kt @@ -0,0 +1,59 @@ +package org.readium.navigator.pdf + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import org.readium.navigator.common.Configurable +import org.readium.navigator.common.Navigator +import org.readium.navigator.common.Preferences +import org.readium.navigator.common.Settings +import org.readium.r2.navigator.pdf.PdfNavigatorFactory +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Locator + +@ExperimentalReadiumApi +@Stable +public class PdfNavigatorState> internal constructor( + internal val pdfNavigatorFactory: PdfNavigatorFactory, + private val settingsResolver: (P) -> S, + initialLocator: Locator, + initialPreferences: P +) : Navigator, Configurable { + + override val preferences: MutableState

= + mutableStateOf(initialPreferences) + + override val settings: State = + derivedStateOf { settingsResolver.invoke(preferences.value) } + + internal val locator: MutableState = + mutableStateOf(initialLocator) + + internal val pendingLocator: MutableState = + mutableStateOf(null) + + override val location: State = + derivedStateOf { + PdfLocation(href = locator.value.href, page = locator.value.locations.position!!) + } + + override suspend fun goTo(link: Link) { + goTo(PageLocation(link.url(), 0)) + } + + override suspend fun goTo(location: PdfLocation) { + pendingLocator.value = locator.value.copyWithLocations(position = location.page) + } + + override suspend fun goTo(location: PdfGoLocation) { + pendingLocator.value = when (location) { + is PositionLocation -> locator.value.copyWithLocations( + position = location.position + ) + is PageLocation -> locator.value.copyWithLocations(position = location.page) + } + } +} diff --git a/readium/navigators/web/build.gradle.kts b/readium/navigators/web/build.gradle.kts new file mode 100644 index 0000000000..be938e68e3 --- /dev/null +++ b/readium/navigators/web/build.gradle.kts @@ -0,0 +1,33 @@ +/* + * 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.web" + + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + } + buildFeatures { + compose = true + } +} + +dependencies { + api(project(":readium:readium-shared")) + api(project(":readium:readium-navigator")) + api(project(":readium:navigators:readium-navigator-common")) + + implementation(libs.kotlinx.serialization.json) + implementation(libs.bundles.compose) + implementation(libs.timber) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.webkit) +} diff --git a/readium/navigators/web/gradle.properties b/readium/navigators/web/gradle.properties new file mode 100644 index 0000000000..d695de80f0 --- /dev/null +++ b/readium/navigators/web/gradle.properties @@ -0,0 +1 @@ +pom.artifactId=readium-navigator-web diff --git a/readium/navigators/web/scripts/.eslintrc.json b/readium/navigators/web/scripts/.eslintrc.json new file mode 100644 index 0000000000..3d82a75f1d --- /dev/null +++ b/readium/navigators/web/scripts/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-namespace": "off" + } + } diff --git a/readium/navigators/web/scripts/.gitignore b/readium/navigators/web/scripts/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/readium/navigators/web/scripts/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/readium/navigators/web/scripts/.prettierignore b/readium/navigators/web/scripts/.prettierignore new file mode 100644 index 0000000000..cee46cca66 --- /dev/null +++ b/readium/navigators/web/scripts/.prettierignore @@ -0,0 +1 @@ +src/vendor diff --git a/readium/navigators/web/scripts/.prettierrc.json b/readium/navigators/web/scripts/.prettierrc.json new file mode 100644 index 0000000000..cce9d3c080 --- /dev/null +++ b/readium/navigators/web/scripts/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "semi": false +} diff --git a/readium/navigators/web/scripts/README.md b/readium/navigators/web/scripts/README.md new file mode 100644 index 0000000000..8b722f603f --- /dev/null +++ b/readium/navigators/web/scripts/README.md @@ -0,0 +1,5 @@ +# Readium JS (Kotlin) + +A set of JavaScript files used by the Kotlin EPUB navigator. + +This folder starts with an underscore to prevent Gradle from embedding it as an asset. diff --git a/readium/navigators/web/scripts/babel.config.json b/readium/navigators/web/scripts/babel.config.json new file mode 100644 index 0000000000..40e295724c --- /dev/null +++ b/readium/navigators/web/scripts/babel.config.json @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env"] +} diff --git a/readium/navigators/web/scripts/dist/fixed-double-index.html b/readium/navigators/web/scripts/dist/fixed-double-index.html new file mode 100644 index 0000000000..0a15204dda --- /dev/null +++ b/readium/navigators/web/scripts/dist/fixed-double-index.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + +
+
+ +
+
+
+ + +
+ + + + diff --git a/readium/navigators/web/scripts/dist/fixed-double-script.js b/readium/navigators/web/scripts/dist/fixed-double-script.js new file mode 100644 index 0000000000..6b1ac6b812 --- /dev/null +++ b/readium/navigators/web/scripts/dist/fixed-double-script.js @@ -0,0 +1,2 @@ +!function(){"use strict";class t{constructor(t,e,i){if(this.margins={top:0,right:0,bottom:0,left:0},!e.contentWindow)throw Error("Iframe argument must have been attached to DOM.");this.listener=i,this.iframe=e}setMessagePort(t){t.onmessage=t=>{this.onMessageFromIframe(t)}}show(){this.iframe.style.display="unset"}hide(){this.iframe.style.display="none"}setMargins(t){this.margins!=t&&(this.iframe.style.marginTop=this.margins.top+"px",this.iframe.style.marginLeft=this.margins.left+"px",this.iframe.style.marginBottom=this.margins.bottom+"px",this.iframe.style.marginRight=this.margins.right+"px")}loadPage(t){this.iframe.src=t}setPlaceholder(t){this.iframe.style.visibility="hidden",this.iframe.style.width=t.width+"px",this.iframe.style.height=t.height+"px",this.size=t}onMessageFromIframe(t){const e=t.data;switch(e.kind){case"contentSize":return this.onContentSizeAvailable(e.size);case"tap":return this.listener.onTap({x:e.x,y:e.y});case"linkActivated":return this.listener.onLinkActivated(e.href)}}onContentSizeAvailable(t){t&&(this.iframe.style.width=t.width+"px",this.iframe.style.height=t.height+"px",this.size=t,this.listener.onIframeLoaded())}}class e{setInitialScale(t){return this.initialScale=t,this}setMinimumScale(t){return this.minimumScale=t,this}setWidth(t){return this.width=t,this}setHeight(t){return this.height=t,this}build(){const t=[];return this.initialScale&&t.push("initial-scale="+this.initialScale),this.minimumScale&&t.push("minimum-scale="+this.minimumScale),this.width&&t.push("width="+this.width),this.height&&t.push("height="+this.height),t.join(", ")}}class i{constructor(t,e){this.window=t,this.listener=e,document.addEventListener("click",(t=>{this.onClick(t)}),!1)}onClick(t){if(t.defaultPrevented)return;const e=this.window.getSelection();if(e&&"Range"==e.type)return;let i;i=t.target instanceof HTMLElement?this.nearestInteractiveElement(t.target):null,i?i instanceof HTMLAnchorElement&&this.listener.onLinkActivated(i.href):this.listener.onTap(t),t.stopPropagation(),t.preventDefault()}nearestInteractiveElement(t){return null==t?null:-1!=["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(t.nodeName.toLowerCase())||t.hasAttribute("contenteditable")&&"false"!=t.getAttribute("contenteditable").toLowerCase()?t:t.parentElement?this.nearestInteractiveElement(t.parentElement):null}}class s{constructor(e,s,n,a,h){this.fit="contain",this.insets={top:0,right:0,bottom:0,left:0},e.addEventListener("message",(t=>{t.ports[0]&&(t.source===s.contentWindow?this.leftPage.setMessagePort(t.ports[0]):t.source==n.contentWindow&&this.rightPage.setMessagePort(t.ports[0]))})),new i(e,{onTap:t=>{const e={x:(t.clientX-visualViewport.offsetLeft)*visualViewport.scale,y:(t.clientY-visualViewport.offsetTop)*visualViewport.scale};h.onTap(e)},onLinkActivated:t=>{throw Error("No interactive element in the root document.")}});const o={onIframeLoaded:()=>{this.layout()},onTap:t=>{const e=s.getBoundingClientRect(),i={x:(t.x+e.left-visualViewport.offsetLeft)*visualViewport.scale,y:(t.y+e.top-visualViewport.offsetTop)*visualViewport.scale};h.onTap(i)},onLinkActivated:t=>{h.onLinkActivated(t)}},r={onIframeLoaded:()=>{this.layout()},onTap:t=>{const e=n.getBoundingClientRect(),i={x:(t.x+e.left-visualViewport.offsetLeft)*visualViewport.scale,y:(t.y+e.top-visualViewport.offsetTop)*visualViewport.scale};h.onTap(i)},onLinkActivated:t=>{h.onLinkActivated(t)}};this.leftPage=new t(e,s,o),this.rightPage=new t(e,n,r),this.metaViewport=a}loadSpread(t){this.leftPage.hide(),this.rightPage.hide(),this.spread=t,t.left&&this.leftPage.loadPage(t.left),t.right&&this.rightPage.loadPage(t.right)}setViewport(t,e){this.viewport==t&&this.insets==e||(this.viewport=t,this.insets=e,this.layout())}setFit(t){this.fit!=t&&(this.fit=t,this.layout())}layout(){if(!this.viewport||!this.leftPage.size&&this.spread.left||!this.rightPage.size&&this.spread.right)return;const t={top:this.insets.top,right:0,bottom:this.insets.bottom,left:this.insets.left};this.leftPage.setMargins(t);const i={top:this.insets.top,right:this.insets.right,bottom:this.insets.bottom,left:0};this.rightPage.setMargins(i),this.spread.right?this.spread.left||this.leftPage.setPlaceholder(this.rightPage.size):this.rightPage.setPlaceholder(this.leftPage.size);const s=this.leftPage.size.width+this.rightPage.size.width,n=Math.max(this.leftPage.size.height,this.rightPage.size.height),a={width:s,height:n},h={width:this.viewport.width-this.insets.left-this.insets.right,height:this.viewport.height-this.insets.top-this.insets.bottom},o=function(t,e,i){switch(t){case"contain":return function(t,e){const i=e.width/t.width,s=e.height/t.height;return Math.min(i,s)}(e,i);case"width":return function(t,e){return e.width/t.width}(e,i);case"height":return function(t,e){return e.height/t.height}(e,i)}}(this.fit,a,h);this.metaViewport.content=(new e).setInitialScale(o).setMinimumScale(o).setWidth(s).setHeight(n).build(),this.leftPage.show(),this.rightPage.show()}}class n{constructor(t){this.nativeApi=t}onTap(t){this.nativeApi.onTap(JSON.stringify(t))}onLinkActivated(t){this.nativeApi.onLinkActivated(t)}}const a=document.getElementById("page-left"),h=document.getElementById("page-right"),o=document.querySelector("meta[name=viewport]");Window.prototype.doubleArea=new class{constructor(t,e,i,a,h){const o=new n(h);this.manager=new s(t,e,i,a,o)}loadSpread(t){this.manager.loadSpread(t)}setViewport(t,e,i,s,n,a){const h={width:t,height:e},o={top:i,left:a,bottom:n,right:s};this.manager.setViewport(h,o)}setFit(t){if("contain"!=t&&"width"!=t&&"height"!=t)throw Error(`Invalid fit value: ${t}`);this.manager.setFit(t)}}(window,a,h,o,window.gestures),window.initialization.onScriptsLoaded()}(); +//# sourceMappingURL=fixed-double-script.js.map \ No newline at end of file diff --git a/readium/navigators/web/scripts/dist/fixed-double-script.js.map b/readium/navigators/web/scripts/dist/fixed-double-script.js.map new file mode 100644 index 0000000000..dc50214d91 --- /dev/null +++ b/readium/navigators/web/scripts/dist/fixed-double-script.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fixed-double-script.js","mappings":"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","sources":["webpack://readium-js/./src/fixed/page-manager.ts","webpack://readium-js/./src/util/viewport.ts","webpack://readium-js/./src/common/gestures.ts","webpack://readium-js/./src/fixed/double-area-manager.ts","webpack://readium-js/./src/util/fit.ts","webpack://readium-js/./src/bridge/fixed-gestures-bridge.ts","webpack://readium-js/./src/index-fixed-double.ts","webpack://readium-js/./src/bridge/fixed-double-bridge.ts"],"sourcesContent":["/** Manages a fixed layout resource embedded in an iframe. */\nexport class PageManager {\n constructor(window, iframe, listener) {\n this.margins = { top: 0, right: 0, bottom: 0, left: 0 };\n if (!iframe.contentWindow) {\n throw Error(\"Iframe argument must have been attached to DOM.\");\n }\n this.listener = listener;\n this.iframe = iframe;\n }\n setMessagePort(messagePort) {\n messagePort.onmessage = (message) => {\n this.onMessageFromIframe(message);\n };\n }\n show() {\n this.iframe.style.display = \"unset\";\n }\n hide() {\n this.iframe.style.display = \"none\";\n }\n /** Sets page margins. */\n setMargins(margins) {\n if (this.margins == margins) {\n return;\n }\n this.iframe.style.marginTop = this.margins.top + \"px\";\n this.iframe.style.marginLeft = this.margins.left + \"px\";\n this.iframe.style.marginBottom = this.margins.bottom + \"px\";\n this.iframe.style.marginRight = this.margins.right + \"px\";\n }\n /** Loads page content. */\n loadPage(url) {\n this.iframe.src = url;\n }\n /** Sets the size of this page without content. */\n setPlaceholder(size) {\n this.iframe.style.visibility = \"hidden\";\n this.iframe.style.width = size.width + \"px\";\n this.iframe.style.height = size.height + \"px\";\n this.size = size;\n }\n onMessageFromIframe(event) {\n const message = event.data;\n switch (message.kind) {\n case \"contentSize\":\n return this.onContentSizeAvailable(message.size);\n case \"tap\":\n return this.listener.onTap({ x: message.x, y: message.y });\n case \"linkActivated\":\n return this.listener.onLinkActivated(message.href);\n }\n }\n onContentSizeAvailable(size) {\n if (!size) {\n //FIXME: handle edge case\n return;\n }\n this.iframe.style.width = size.width + \"px\";\n this.iframe.style.height = size.height + \"px\";\n this.size = size;\n this.listener.onIframeLoaded();\n }\n}\n","export class ViewportStringBuilder {\n setInitialScale(scale) {\n this.initialScale = scale;\n return this;\n }\n setMinimumScale(scale) {\n this.minimumScale = scale;\n return this;\n }\n setWidth(width) {\n this.width = width;\n return this;\n }\n setHeight(height) {\n this.height = height;\n return this;\n }\n build() {\n const components = [];\n if (this.initialScale) {\n components.push(\"initial-scale=\" + this.initialScale);\n }\n if (this.minimumScale) {\n components.push(\"minimum-scale=\" + this.minimumScale);\n }\n if (this.width) {\n components.push(\"width=\" + this.width);\n }\n if (this.height) {\n components.push(\"height=\" + this.height);\n }\n return components.join(\", \");\n }\n}\nexport function parseViewportString(viewportString) {\n const regex = /(\\w+) *= *([^\\s,]+)/g;\n const properties = new Map();\n let match;\n while ((match = regex.exec(viewportString))) {\n if (match != null) {\n properties.set(match[1], match[2]);\n }\n }\n const width = parseFloat(properties.get(\"width\"));\n const height = parseFloat(properties.get(\"height\"));\n if (width && height) {\n return { width, height };\n }\n else {\n return undefined;\n }\n}\n","export class GesturesDetector {\n constructor(window, listener) {\n this.window = window;\n this.listener = listener;\n document.addEventListener(\"click\", (event) => {\n this.onClick(event);\n }, false);\n }\n onClick(event) {\n if (event.defaultPrevented) {\n return;\n }\n const selection = this.window.getSelection();\n if (selection && selection.type == \"Range\") {\n // There's an on-going selection, the tap will dismiss it so we don't forward it.\n // selection.type might be None (collapsed) or Caret with a collapsed range\n // when there is not true selection.\n return;\n }\n let nearestElement;\n if (event.target instanceof HTMLElement) {\n nearestElement = this.nearestInteractiveElement(event.target);\n }\n else {\n nearestElement = null;\n }\n if (nearestElement) {\n if (nearestElement instanceof HTMLAnchorElement) {\n this.listener.onLinkActivated(nearestElement.href);\n }\n }\n else {\n this.listener.onTap(event);\n }\n event.stopPropagation();\n event.preventDefault();\n }\n // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling\n nearestInteractiveElement(element) {\n if (element == null) {\n return null;\n }\n const interactiveTags = [\n \"a\",\n \"audio\",\n \"button\",\n \"canvas\",\n \"details\",\n \"input\",\n \"label\",\n \"option\",\n \"select\",\n \"submit\",\n \"textarea\",\n \"video\",\n ];\n if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) {\n return element;\n }\n // Checks whether the element is editable by the user.\n if (element.hasAttribute(\"contenteditable\") &&\n element.getAttribute(\"contenteditable\").toLowerCase() != \"false\") {\n return element;\n }\n // Checks parents recursively because the touch might be for example on an inside a .\n if (element.parentElement) {\n return this.nearestInteractiveElement(element.parentElement);\n }\n return null;\n }\n}\n","import { computeScale } from \"../util/fit\";\nimport { PageManager } from \"./page-manager\";\nimport { ViewportStringBuilder } from \"../util/viewport\";\nimport { GesturesDetector } from \"../common/gestures\";\nexport class DoubleAreaManager {\n constructor(window, leftIframe, rightIframe, metaViewport, listener) {\n this.fit = \"contain\" /* Fit.Contain */;\n this.insets = { top: 0, right: 0, bottom: 0, left: 0 };\n window.addEventListener(\"message\", (event) => {\n if (!event.ports[0]) {\n return;\n }\n if (event.source === leftIframe.contentWindow) {\n this.leftPage.setMessagePort(event.ports[0]);\n }\n else if (event.source == rightIframe.contentWindow) {\n this.rightPage.setMessagePort(event.ports[0]);\n }\n });\n const wrapperGesturesListener = {\n onTap: (event) => {\n const tapEvent = {\n x: (event.clientX - visualViewport.offsetLeft) *\n visualViewport.scale,\n y: (event.clientY - visualViewport.offsetTop) * visualViewport.scale,\n };\n listener.onTap(tapEvent);\n },\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLinkActivated: (_) => {\n throw Error(\"No interactive element in the root document.\");\n },\n };\n new GesturesDetector(window, wrapperGesturesListener);\n const leftPageListener = {\n onIframeLoaded: () => {\n this.layout();\n },\n onTap: (event) => {\n const boundingRect = leftIframe.getBoundingClientRect();\n const tapEvent = {\n x: (event.x + boundingRect.left - visualViewport.offsetLeft) *\n visualViewport.scale,\n y: (event.y + boundingRect.top - visualViewport.offsetTop) *\n visualViewport.scale,\n };\n listener.onTap(tapEvent);\n },\n onLinkActivated: (href) => {\n listener.onLinkActivated(href);\n },\n };\n const rightPageListener = {\n onIframeLoaded: () => {\n this.layout();\n },\n onTap: (event) => {\n const boundingRect = rightIframe.getBoundingClientRect();\n const tapEvent = {\n x: (event.x + boundingRect.left - visualViewport.offsetLeft) *\n visualViewport.scale,\n y: (event.y + boundingRect.top - visualViewport.offsetTop) *\n visualViewport.scale,\n };\n listener.onTap(tapEvent);\n },\n onLinkActivated: (href) => {\n listener.onLinkActivated(href);\n },\n };\n this.leftPage = new PageManager(window, leftIframe, leftPageListener);\n this.rightPage = new PageManager(window, rightIframe, rightPageListener);\n this.metaViewport = metaViewport;\n }\n loadSpread(spread) {\n this.leftPage.hide();\n this.rightPage.hide();\n this.spread = spread;\n if (spread.left) {\n this.leftPage.loadPage(spread.left);\n }\n if (spread.right) {\n this.rightPage.loadPage(spread.right);\n }\n }\n setViewport(size, insets) {\n if (this.viewport == size && this.insets == insets) {\n return;\n }\n this.viewport = size;\n this.insets = insets;\n this.layout();\n }\n setFit(fit) {\n if (this.fit == fit) {\n return;\n }\n this.fit = fit;\n this.layout();\n }\n layout() {\n if (!this.viewport ||\n (!this.leftPage.size && this.spread.left) ||\n (!this.rightPage.size && this.spread.right)) {\n return;\n }\n const leftMargins = {\n top: this.insets.top,\n right: 0,\n bottom: this.insets.bottom,\n left: this.insets.left,\n };\n this.leftPage.setMargins(leftMargins);\n const rightMargins = {\n top: this.insets.top,\n right: this.insets.right,\n bottom: this.insets.bottom,\n left: 0,\n };\n this.rightPage.setMargins(rightMargins);\n if (!this.spread.right) {\n this.rightPage.setPlaceholder(this.leftPage.size);\n }\n else if (!this.spread.left) {\n this.leftPage.setPlaceholder(this.rightPage.size);\n }\n const contentWidth = this.leftPage.size.width + this.rightPage.size.width;\n const contentHeight = Math.max(this.leftPage.size.height, this.rightPage.size.height);\n const contentSize = { width: contentWidth, height: contentHeight };\n const safeDrawingSize = {\n width: this.viewport.width - this.insets.left - this.insets.right,\n height: this.viewport.height - this.insets.top - this.insets.bottom,\n };\n const scale = computeScale(this.fit, contentSize, safeDrawingSize);\n this.metaViewport.content = new ViewportStringBuilder()\n .setInitialScale(scale)\n .setMinimumScale(scale)\n .setWidth(contentWidth)\n .setHeight(contentHeight)\n .build();\n this.leftPage.show();\n this.rightPage.show();\n }\n}\n","export function computeScale(fit, content, container) {\n switch (fit) {\n case \"contain\" /* Fit.Contain */:\n return fitContain(content, container);\n case \"width\" /* Fit.Width */:\n return fitWidth(content, container);\n case \"height\" /* Fit.Height */:\n return fitHeight(content, container);\n }\n}\nfunction fitContain(content, container) {\n const widthRatio = container.width / content.width;\n const heightRatio = container.height / content.height;\n return Math.min(widthRatio, heightRatio);\n}\nfunction fitWidth(content, container) {\n return container.width / content.width;\n}\nfunction fitHeight(content, container) {\n return container.height / content.height;\n}\n","export class BridgeGesturesAdapter {\n constructor(gesturesApi) {\n this.nativeApi = gesturesApi;\n }\n onTap(event) {\n this.nativeApi.onTap(JSON.stringify(event));\n }\n onLinkActivated(href) {\n this.nativeApi.onLinkActivated(href);\n }\n}\n","//\n// Copyright 2024 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\nimport { FixedDoubleBridge } from \"./bridge/fixed-double-bridge\";\nconst leftIframe = document.getElementById(\"page-left\");\nconst rightIframe = document.getElementById(\"page-right\");\nconst metaViewport = document.querySelector(\"meta[name=viewport]\");\nWindow.prototype.doubleArea = new FixedDoubleBridge(window, leftIframe, rightIframe, metaViewport, window.gestures);\nwindow.initialization.onScriptsLoaded();\n","import { DoubleAreaManager } from \"../fixed/double-area-manager\";\nimport { BridgeGesturesAdapter } from \"./fixed-gestures-bridge\";\nexport class FixedDoubleBridge {\n constructor(window, leftIframe, rightIframe, metaViewport, gestures) {\n const listener = new BridgeGesturesAdapter(gestures);\n this.manager = new DoubleAreaManager(window, leftIframe, rightIframe, metaViewport, listener);\n }\n loadSpread(spread) {\n this.manager.loadSpread(spread);\n }\n setViewport(viewporttWidth, viewportHeight, insetTop, insetRight, insetBottom, insetLeft) {\n const viewport = { width: viewporttWidth, height: viewportHeight };\n const insets = {\n top: insetTop,\n left: insetLeft,\n bottom: insetBottom,\n right: insetRight,\n };\n this.manager.setViewport(viewport, insets);\n }\n setFit(fit) {\n if (fit != \"contain\" && fit != \"width\" && fit != \"height\") {\n throw Error(`Invalid fit value: ${fit}`);\n }\n this.manager.setFit(fit);\n }\n}\n"],"names":["PageManager","constructor","window","iframe","listener","this","margins","top","right","bottom","left","contentWindow","Error","setMessagePort","messagePort","onmessage","message","onMessageFromIframe","show","style","display","hide","setMargins","marginTop","marginLeft","marginBottom","marginRight","loadPage","url","src","setPlaceholder","size","visibility","width","height","event","data","kind","onContentSizeAvailable","onTap","x","y","onLinkActivated","href","onIframeLoaded","ViewportStringBuilder","setInitialScale","scale","initialScale","setMinimumScale","minimumScale","setWidth","setHeight","build","components","push","join","GesturesDetector","document","addEventListener","onClick","defaultPrevented","selection","getSelection","type","nearestElement","target","HTMLElement","nearestInteractiveElement","HTMLAnchorElement","stopPropagation","preventDefault","element","indexOf","nodeName","toLowerCase","hasAttribute","getAttribute","parentElement","DoubleAreaManager","leftIframe","rightIframe","metaViewport","fit","insets","ports","source","leftPage","rightPage","tapEvent","clientX","visualViewport","offsetLeft","clientY","offsetTop","_","leftPageListener","layout","boundingRect","getBoundingClientRect","rightPageListener","loadSpread","spread","setViewport","viewport","setFit","leftMargins","rightMargins","contentWidth","contentHeight","Math","max","contentSize","safeDrawingSize","content","container","widthRatio","heightRatio","min","fitContain","fitWidth","fitHeight","computeScale","BridgeGesturesAdapter","gesturesApi","nativeApi","JSON","stringify","getElementById","querySelector","Window","prototype","doubleArea","gestures","manager","viewporttWidth","viewportHeight","insetTop","insetRight","insetBottom","insetLeft","initialization","onScriptsLoaded"],"sourceRoot":""} \ No newline at end of file diff --git a/readium/navigators/web/scripts/dist/fixed-injectable-script.js b/readium/navigators/web/scripts/dist/fixed-injectable-script.js new file mode 100644 index 0000000000..61b11ef4c1 --- /dev/null +++ b/readium/navigators/web/scripts/dist/fixed-injectable-script.js @@ -0,0 +1,2 @@ +!function(){"use strict";const e=new MessageChannel;window.parent.postMessage("Init","*",[e.port2]);const t=new class{constructor(e){this.messagePort=e}send(e){this.messagePort.postMessage(e)}}(e.port1),n=function(e){const t=window.document.querySelector("meta[name=viewport]");if(t&&t instanceof HTMLMetaElement)return function(e){const t=/(\w+) *= *([^\s,]+)/g,n=new Map;let s;for(;s=t.exec(e);)null!=s&&n.set(s[1],s[2]);const i=parseFloat(n.get("width")),o=parseFloat(n.get("height"));return i&&o?{width:i,height:o}:void 0}(t.content)}();t.send({kind:"contentSize",size:n});const s=new class{constructor(e){this.messageSender=e}onTap(e){this.messageSender.send({kind:"tap",x:e.clientX,y:e.clientY})}onLinkActivated(e){this.messageSender.send({kind:"linkActivated",href:e})}}(t);new class{constructor(e,t){this.window=e,this.listener=t,document.addEventListener("click",(e=>{this.onClick(e)}),!1)}onClick(e){if(e.defaultPrevented)return;const t=this.window.getSelection();if(t&&"Range"==t.type)return;let n;n=e.target instanceof HTMLElement?this.nearestInteractiveElement(e.target):null,n?n instanceof HTMLAnchorElement&&this.listener.onLinkActivated(n.href):this.listener.onTap(e),e.stopPropagation(),e.preventDefault()}nearestInteractiveElement(e){return null==e?null:-1!=["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(e.nodeName.toLowerCase())||e.hasAttribute("contenteditable")&&"false"!=e.getAttribute("contenteditable").toLowerCase()?e:e.parentElement?this.nearestInteractiveElement(e.parentElement):null}}(window,s)}(); +//# sourceMappingURL=fixed-injectable-script.js.map \ No newline at end of file diff --git a/readium/navigators/web/scripts/dist/fixed-injectable-script.js.map b/readium/navigators/web/scripts/dist/fixed-injectable-script.js.map new file mode 100644 index 0000000000..990608b525 --- /dev/null +++ b/readium/navigators/web/scripts/dist/fixed-injectable-script.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fixed-injectable-script.js","mappings":"yBAWA,MAAMA,EAAiB,IAAIC,eAC3BC,OAAOC,OAAOC,YAAY,OAAQ,IAAK,CAACJ,EAAeK,QACvD,MAAMC,EAAgB,ICbf,MACH,WAAAC,CAAYC,GACRC,KAAKD,YAAcA,CACvB,CACA,IAAAE,CAAKC,GACDF,KAAKD,YAAYJ,YAAYO,EACjC,GDO0CX,EAAeY,OACvDC,EAeN,SAA0BC,GACtB,MAAMC,EAhB4Bb,OAAOY,SAgBfE,cAAc,uBACxC,GAAKD,GAAcA,aAAoBE,gBAGvC,OEAG,SAA6BC,GAChC,MAAMC,EAAQ,uBACRC,EAAa,IAAIC,IACvB,IAAIC,EACJ,KAAQA,EAAQH,EAAMI,KAAKL,IACV,MAATI,GACAF,EAAWI,IAAIF,EAAM,GAAIA,EAAM,IAGvC,MAAMG,EAAQC,WAAWN,EAAWO,IAAI,UAClCC,EAASF,WAAWN,EAAWO,IAAI,WACzC,OAAIF,GAASG,EACF,CAAEH,QAAOG,eAGhB,CAER,CFjBWC,CAAoBd,EAASe,QACxC,CArBqBC,GACrBzB,EAAcI,KAAK,CAAEsB,KAAM,cAAeC,KAAMpB,IAYhD,MAAMqB,EAAoB,IAX1B,MACI,WAAA3B,CAAYD,GACRG,KAAKH,cAAgBA,CACzB,CACA,KAAA6B,CAAMC,GACF3B,KAAKH,cAAcI,KAAK,CAAEsB,KAAM,MAAOK,EAAGD,EAAME,QAASC,EAAGH,EAAMI,SACtE,CACA,eAAAC,CAAgBC,GACZjC,KAAKH,cAAcI,KAAK,CAAEsB,KAAM,gBAAiBU,KAAMA,GAC3D,GAEoDpC,GACxD,IG5BO,MACH,WAAAC,CAAYL,EAAQyC,GAChBlC,KAAKP,OAASA,EACdO,KAAKkC,SAAWA,EAChB7B,SAAS8B,iBAAiB,SAAUR,IAChC3B,KAAKoC,QAAQT,EAAM,IACpB,EACP,CACA,OAAAS,CAAQT,GACJ,GAAIA,EAAMU,iBACN,OAEJ,MAAMC,EAAYtC,KAAKP,OAAO8C,eAC9B,GAAID,GAA+B,SAAlBA,EAAUE,KAIvB,OAEJ,IAAIC,EAEAA,EADAd,EAAMe,kBAAkBC,YACP3C,KAAK4C,0BAA0BjB,EAAMe,QAGrC,KAEjBD,EACIA,aAA0BI,mBAC1B7C,KAAKkC,SAASF,gBAAgBS,EAAeR,MAIjDjC,KAAKkC,SAASR,MAAMC,GAExBA,EAAMmB,kBACNnB,EAAMoB,gBACV,CAEA,yBAAAH,CAA0BI,GACtB,OAAe,MAAXA,EACO,MAgBqD,GAdxC,CACpB,IACA,QACA,SACA,SACA,UACA,QACA,QACA,SACA,SACA,SACA,WACA,SAEgBC,QAAQD,EAAQE,SAASC,gBAIzCH,EAAQI,aAAa,oBACoC,SAAzDJ,EAAQK,aAAa,mBAAmBF,cAJjCH,EAQPA,EAAQM,cACDtD,KAAK4C,0BAA0BI,EAAQM,eAE3C,IACX,GHzCiB7D,OAAQgC,E","sources":["webpack://readium-js/./src/index-fixed-injectable.ts","webpack://readium-js/./src/fixed/iframe-message.ts","webpack://readium-js/./src/util/viewport.ts","webpack://readium-js/./src/common/gestures.ts"],"sourcesContent":["//\n// Copyright 2024 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n/**\n * Script loaded by fixed layout resources.\n */\nimport { GesturesDetector } from \"./common/gestures\";\nimport { IframeMessageSender } from \"./fixed/iframe-message\";\nimport { parseViewportString } from \"./util/viewport\";\nconst messageChannel = new MessageChannel();\nwindow.parent.postMessage(\"Init\", \"*\", [messageChannel.port2]);\nconst messageSender = new IframeMessageSender(messageChannel.port1);\nconst viewportSize = parseContentSize(window.document);\nmessageSender.send({ kind: \"contentSize\", size: viewportSize });\nclass MessagingGesturesListener {\n constructor(messageSender) {\n this.messageSender = messageSender;\n }\n onTap(event) {\n this.messageSender.send({ kind: \"tap\", x: event.clientX, y: event.clientY });\n }\n onLinkActivated(href) {\n this.messageSender.send({ kind: \"linkActivated\", href: href });\n }\n}\nconst messagingListener = new MessagingGesturesListener(messageSender);\nnew GesturesDetector(window, messagingListener);\nfunction parseContentSize(document) {\n const viewport = document.querySelector(\"meta[name=viewport]\");\n if (!viewport || !(viewport instanceof HTMLMetaElement)) {\n return undefined;\n }\n return parseViewportString(viewport.content);\n}\n","export class IframeMessageSender {\n constructor(messagePort) {\n this.messagePort = messagePort;\n }\n send(message) {\n this.messagePort.postMessage(message);\n }\n}\n","export class ViewportStringBuilder {\n setInitialScale(scale) {\n this.initialScale = scale;\n return this;\n }\n setMinimumScale(scale) {\n this.minimumScale = scale;\n return this;\n }\n setWidth(width) {\n this.width = width;\n return this;\n }\n setHeight(height) {\n this.height = height;\n return this;\n }\n build() {\n const components = [];\n if (this.initialScale) {\n components.push(\"initial-scale=\" + this.initialScale);\n }\n if (this.minimumScale) {\n components.push(\"minimum-scale=\" + this.minimumScale);\n }\n if (this.width) {\n components.push(\"width=\" + this.width);\n }\n if (this.height) {\n components.push(\"height=\" + this.height);\n }\n return components.join(\", \");\n }\n}\nexport function parseViewportString(viewportString) {\n const regex = /(\\w+) *= *([^\\s,]+)/g;\n const properties = new Map();\n let match;\n while ((match = regex.exec(viewportString))) {\n if (match != null) {\n properties.set(match[1], match[2]);\n }\n }\n const width = parseFloat(properties.get(\"width\"));\n const height = parseFloat(properties.get(\"height\"));\n if (width && height) {\n return { width, height };\n }\n else {\n return undefined;\n }\n}\n","export class GesturesDetector {\n constructor(window, listener) {\n this.window = window;\n this.listener = listener;\n document.addEventListener(\"click\", (event) => {\n this.onClick(event);\n }, false);\n }\n onClick(event) {\n if (event.defaultPrevented) {\n return;\n }\n const selection = this.window.getSelection();\n if (selection && selection.type == \"Range\") {\n // There's an on-going selection, the tap will dismiss it so we don't forward it.\n // selection.type might be None (collapsed) or Caret with a collapsed range\n // when there is not true selection.\n return;\n }\n let nearestElement;\n if (event.target instanceof HTMLElement) {\n nearestElement = this.nearestInteractiveElement(event.target);\n }\n else {\n nearestElement = null;\n }\n if (nearestElement) {\n if (nearestElement instanceof HTMLAnchorElement) {\n this.listener.onLinkActivated(nearestElement.href);\n }\n }\n else {\n this.listener.onTap(event);\n }\n event.stopPropagation();\n event.preventDefault();\n }\n // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling\n nearestInteractiveElement(element) {\n if (element == null) {\n return null;\n }\n const interactiveTags = [\n \"a\",\n \"audio\",\n \"button\",\n \"canvas\",\n \"details\",\n \"input\",\n \"label\",\n \"option\",\n \"select\",\n \"submit\",\n \"textarea\",\n \"video\",\n ];\n if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) {\n return element;\n }\n // Checks whether the element is editable by the user.\n if (element.hasAttribute(\"contenteditable\") &&\n element.getAttribute(\"contenteditable\").toLowerCase() != \"false\") {\n return element;\n }\n // Checks parents recursively because the touch might be for example on an inside a .\n if (element.parentElement) {\n return this.nearestInteractiveElement(element.parentElement);\n }\n return null;\n }\n}\n"],"names":["messageChannel","MessageChannel","window","parent","postMessage","port2","messageSender","constructor","messagePort","this","send","message","port1","viewportSize","document","viewport","querySelector","HTMLMetaElement","viewportString","regex","properties","Map","match","exec","set","width","parseFloat","get","height","parseViewportString","content","parseContentSize","kind","size","messagingListener","onTap","event","x","clientX","y","clientY","onLinkActivated","href","listener","addEventListener","onClick","defaultPrevented","selection","getSelection","type","nearestElement","target","HTMLElement","nearestInteractiveElement","HTMLAnchorElement","stopPropagation","preventDefault","element","indexOf","nodeName","toLowerCase","hasAttribute","getAttribute","parentElement"],"sourceRoot":""} \ No newline at end of file diff --git a/readium/navigators/web/scripts/dist/fixed-single-index.html b/readium/navigators/web/scripts/dist/fixed-single-index.html new file mode 100644 index 0000000000..4c8982b165 --- /dev/null +++ b/readium/navigators/web/scripts/dist/fixed-single-index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + +
+ +
+ + + diff --git a/readium/navigators/web/scripts/dist/fixed-single-script.js b/readium/navigators/web/scripts/dist/fixed-single-script.js new file mode 100644 index 0000000000..d5ed6229a4 --- /dev/null +++ b/readium/navigators/web/scripts/dist/fixed-single-script.js @@ -0,0 +1,2 @@ +!function(){"use strict";class t{constructor(t,e,i){if(this.margins={top:0,right:0,bottom:0,left:0},!e.contentWindow)throw Error("Iframe argument must have been attached to DOM.");this.listener=i,this.iframe=e}setMessagePort(t){t.onmessage=t=>{this.onMessageFromIframe(t)}}show(){this.iframe.style.display="unset"}hide(){this.iframe.style.display="none"}setMargins(t){this.margins!=t&&(this.iframe.style.marginTop=this.margins.top+"px",this.iframe.style.marginLeft=this.margins.left+"px",this.iframe.style.marginBottom=this.margins.bottom+"px",this.iframe.style.marginRight=this.margins.right+"px")}loadPage(t){this.iframe.src=t}setPlaceholder(t){this.iframe.style.visibility="hidden",this.iframe.style.width=t.width+"px",this.iframe.style.height=t.height+"px",this.size=t}onMessageFromIframe(t){const e=t.data;switch(e.kind){case"contentSize":return this.onContentSizeAvailable(e.size);case"tap":return this.listener.onTap({x:e.x,y:e.y});case"linkActivated":return this.listener.onLinkActivated(e.href)}}onContentSizeAvailable(t){t&&(this.iframe.style.width=t.width+"px",this.iframe.style.height=t.height+"px",this.size=t,this.listener.onIframeLoaded())}}class e{setInitialScale(t){return this.initialScale=t,this}setMinimumScale(t){return this.minimumScale=t,this}setWidth(t){return this.width=t,this}setHeight(t){return this.height=t,this}build(){const t=[];return this.initialScale&&t.push("initial-scale="+this.initialScale),this.minimumScale&&t.push("minimum-scale="+this.minimumScale),this.width&&t.push("width="+this.width),this.height&&t.push("height="+this.height),t.join(", ")}}class i{constructor(t,e){this.window=t,this.listener=e,document.addEventListener("click",(t=>{this.onClick(t)}),!1)}onClick(t){if(t.defaultPrevented)return;const e=this.window.getSelection();if(e&&"Range"==e.type)return;let i;i=t.target instanceof HTMLElement?this.nearestInteractiveElement(t.target):null,i?i instanceof HTMLAnchorElement&&this.listener.onLinkActivated(i.href):this.listener.onTap(t),t.stopPropagation(),t.preventDefault()}nearestInteractiveElement(t){return null==t?null:-1!=["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(t.nodeName.toLowerCase())||t.hasAttribute("contenteditable")&&"false"!=t.getAttribute("contenteditable").toLowerCase()?t:t.parentElement?this.nearestInteractiveElement(t.parentElement):null}}class s{constructor(e,s,n,a){this.fit="contain",this.insets={top:0,right:0,bottom:0,left:0},this.scale=1,e.addEventListener("message",(t=>{t.source===s.contentWindow&&t.ports[0]&&this.page.setMessagePort(t.ports[0])})),new i(e,{onTap:t=>{const e={x:(t.clientX-visualViewport.offsetLeft)*visualViewport.scale,y:(t.clientY-visualViewport.offsetTop)*visualViewport.scale};a.onTap(e)},onLinkActivated:t=>{throw Error("No interactive element in the root document.")}}),this.metaViewport=n;const h={onIframeLoaded:()=>{this.onIframeLoaded()},onTap:t=>{const e=s.getBoundingClientRect(),i={x:(t.x+e.left-visualViewport.offsetLeft)*visualViewport.scale,y:(t.y+e.top-visualViewport.offsetTop)*visualViewport.scale};a.onTap(i)},onLinkActivated:t=>{a.onLinkActivated(t)}};this.page=new t(e,s,h)}setViewport(t,e){this.viewport==t&&this.insets==e||(this.viewport=t,this.insets=e,this.layout())}setFit(t){this.fit!=t&&(this.fit=t,this.layout())}loadResource(t){this.page.hide(),this.page.loadPage(t)}onIframeLoaded(){this.page.size&&this.layout()}layout(){if(!this.page.size||!this.viewport)return;const t={top:this.insets.top,right:this.insets.right,bottom:this.insets.bottom,left:this.insets.left};this.page.setMargins(t);const i={width:this.viewport.width-this.insets.left-this.insets.right,height:this.viewport.height-this.insets.top-this.insets.bottom},s=function(t,e,i){switch(t){case"contain":return function(t,e){const i=e.width/t.width,s=e.height/t.height;return Math.min(i,s)}(e,i);case"width":return function(t,e){return e.width/t.width}(e,i);case"height":return function(t,e){return e.height/t.height}(e,i)}}(this.fit,this.page.size,i);this.metaViewport.content=(new e).setInitialScale(s).setMinimumScale(s).setWidth(this.page.size.width).setHeight(this.page.size.height).build(),this.scale=s,this.page.show()}}class n{constructor(t){this.nativeApi=t}onTap(t){this.nativeApi.onTap(JSON.stringify(t))}onLinkActivated(t){this.nativeApi.onLinkActivated(t)}}const a=document.getElementById("page"),h=document.querySelector("meta[name=viewport]");window.singleArea=new class{constructor(t,e,i,a){const h=new n(a);this.manager=new s(t,e,i,h)}loadResource(t){this.manager.loadResource(t)}setViewport(t,e,i,s,n,a){const h={width:t,height:e},o={top:i,left:a,bottom:n,right:s};this.manager.setViewport(h,o)}setFit(t){if("contain"!=t&&"width"!=t&&"height"!=t)throw Error(`Invalid fit value: ${t}`);this.manager.setFit(t)}}(window,a,h,window.gestures),window.initialization.onScriptsLoaded()}(); +//# sourceMappingURL=fixed-single-script.js.map \ No newline at end of file diff --git a/readium/navigators/web/scripts/dist/fixed-single-script.js.map b/readium/navigators/web/scripts/dist/fixed-single-script.js.map new file mode 100644 index 0000000000..a823bae92d --- /dev/null +++ b/readium/navigators/web/scripts/dist/fixed-single-script.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fixed-single-script.js","mappings":"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","sources":["webpack://readium-js/./src/fixed/page-manager.ts","webpack://readium-js/./src/util/viewport.ts","webpack://readium-js/./src/common/gestures.ts","webpack://readium-js/./src/fixed/single-area-manager.ts","webpack://readium-js/./src/util/fit.ts","webpack://readium-js/./src/bridge/fixed-gestures-bridge.ts","webpack://readium-js/./src/index-fixed-single.ts","webpack://readium-js/./src/bridge/fixed-single-bridge.ts"],"sourcesContent":["/** Manages a fixed layout resource embedded in an iframe. */\nexport class PageManager {\n constructor(window, iframe, listener) {\n this.margins = { top: 0, right: 0, bottom: 0, left: 0 };\n if (!iframe.contentWindow) {\n throw Error(\"Iframe argument must have been attached to DOM.\");\n }\n this.listener = listener;\n this.iframe = iframe;\n }\n setMessagePort(messagePort) {\n messagePort.onmessage = (message) => {\n this.onMessageFromIframe(message);\n };\n }\n show() {\n this.iframe.style.display = \"unset\";\n }\n hide() {\n this.iframe.style.display = \"none\";\n }\n /** Sets page margins. */\n setMargins(margins) {\n if (this.margins == margins) {\n return;\n }\n this.iframe.style.marginTop = this.margins.top + \"px\";\n this.iframe.style.marginLeft = this.margins.left + \"px\";\n this.iframe.style.marginBottom = this.margins.bottom + \"px\";\n this.iframe.style.marginRight = this.margins.right + \"px\";\n }\n /** Loads page content. */\n loadPage(url) {\n this.iframe.src = url;\n }\n /** Sets the size of this page without content. */\n setPlaceholder(size) {\n this.iframe.style.visibility = \"hidden\";\n this.iframe.style.width = size.width + \"px\";\n this.iframe.style.height = size.height + \"px\";\n this.size = size;\n }\n onMessageFromIframe(event) {\n const message = event.data;\n switch (message.kind) {\n case \"contentSize\":\n return this.onContentSizeAvailable(message.size);\n case \"tap\":\n return this.listener.onTap({ x: message.x, y: message.y });\n case \"linkActivated\":\n return this.listener.onLinkActivated(message.href);\n }\n }\n onContentSizeAvailable(size) {\n if (!size) {\n //FIXME: handle edge case\n return;\n }\n this.iframe.style.width = size.width + \"px\";\n this.iframe.style.height = size.height + \"px\";\n this.size = size;\n this.listener.onIframeLoaded();\n }\n}\n","export class ViewportStringBuilder {\n setInitialScale(scale) {\n this.initialScale = scale;\n return this;\n }\n setMinimumScale(scale) {\n this.minimumScale = scale;\n return this;\n }\n setWidth(width) {\n this.width = width;\n return this;\n }\n setHeight(height) {\n this.height = height;\n return this;\n }\n build() {\n const components = [];\n if (this.initialScale) {\n components.push(\"initial-scale=\" + this.initialScale);\n }\n if (this.minimumScale) {\n components.push(\"minimum-scale=\" + this.minimumScale);\n }\n if (this.width) {\n components.push(\"width=\" + this.width);\n }\n if (this.height) {\n components.push(\"height=\" + this.height);\n }\n return components.join(\", \");\n }\n}\nexport function parseViewportString(viewportString) {\n const regex = /(\\w+) *= *([^\\s,]+)/g;\n const properties = new Map();\n let match;\n while ((match = regex.exec(viewportString))) {\n if (match != null) {\n properties.set(match[1], match[2]);\n }\n }\n const width = parseFloat(properties.get(\"width\"));\n const height = parseFloat(properties.get(\"height\"));\n if (width && height) {\n return { width, height };\n }\n else {\n return undefined;\n }\n}\n","export class GesturesDetector {\n constructor(window, listener) {\n this.window = window;\n this.listener = listener;\n document.addEventListener(\"click\", (event) => {\n this.onClick(event);\n }, false);\n }\n onClick(event) {\n if (event.defaultPrevented) {\n return;\n }\n const selection = this.window.getSelection();\n if (selection && selection.type == \"Range\") {\n // There's an on-going selection, the tap will dismiss it so we don't forward it.\n // selection.type might be None (collapsed) or Caret with a collapsed range\n // when there is not true selection.\n return;\n }\n let nearestElement;\n if (event.target instanceof HTMLElement) {\n nearestElement = this.nearestInteractiveElement(event.target);\n }\n else {\n nearestElement = null;\n }\n if (nearestElement) {\n if (nearestElement instanceof HTMLAnchorElement) {\n this.listener.onLinkActivated(nearestElement.href);\n }\n }\n else {\n this.listener.onTap(event);\n }\n event.stopPropagation();\n event.preventDefault();\n }\n // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling\n nearestInteractiveElement(element) {\n if (element == null) {\n return null;\n }\n const interactiveTags = [\n \"a\",\n \"audio\",\n \"button\",\n \"canvas\",\n \"details\",\n \"input\",\n \"label\",\n \"option\",\n \"select\",\n \"submit\",\n \"textarea\",\n \"video\",\n ];\n if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) {\n return element;\n }\n // Checks whether the element is editable by the user.\n if (element.hasAttribute(\"contenteditable\") &&\n element.getAttribute(\"contenteditable\").toLowerCase() != \"false\") {\n return element;\n }\n // Checks parents recursively because the touch might be for example on an inside a
.\n if (element.parentElement) {\n return this.nearestInteractiveElement(element.parentElement);\n }\n return null;\n }\n}\n","import { computeScale } from \"../util/fit\";\nimport { PageManager } from \"./page-manager\";\nimport { ViewportStringBuilder } from \"../util/viewport\";\nimport { GesturesDetector } from \"../common/gestures\";\nexport class SingleAreaManager {\n constructor(window, iframe, metaViewport, listener) {\n this.fit = \"contain\" /* Fit.Contain */;\n this.insets = { top: 0, right: 0, bottom: 0, left: 0 };\n this.scale = 1;\n window.addEventListener(\"message\", (event) => {\n if (event.source === iframe.contentWindow && event.ports[0]) {\n this.page.setMessagePort(event.ports[0]);\n }\n });\n const wrapperGesturesListener = {\n onTap: (event) => {\n const tapEvent = {\n x: (event.clientX - visualViewport.offsetLeft) *\n visualViewport.scale,\n y: (event.clientY - visualViewport.offsetTop) * visualViewport.scale,\n };\n listener.onTap(tapEvent);\n },\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLinkActivated: (_) => {\n throw Error(\"No interactive element in the root document.\");\n },\n };\n new GesturesDetector(window, wrapperGesturesListener);\n this.metaViewport = metaViewport;\n const pageListener = {\n onIframeLoaded: () => {\n this.onIframeLoaded();\n },\n onTap: (event) => {\n const boundingRect = iframe.getBoundingClientRect();\n const tapEvent = {\n x: (event.x + boundingRect.left - visualViewport.offsetLeft) *\n visualViewport.scale,\n y: (event.y + boundingRect.top - visualViewport.offsetTop) *\n visualViewport.scale,\n };\n listener.onTap(tapEvent);\n },\n onLinkActivated: (href) => {\n listener.onLinkActivated(href);\n },\n };\n this.page = new PageManager(window, iframe, pageListener);\n }\n setViewport(viewport, insets) {\n if (this.viewport == viewport && this.insets == insets) {\n return;\n }\n this.viewport = viewport;\n this.insets = insets;\n this.layout();\n }\n setFit(fit) {\n if (this.fit == fit) {\n return;\n }\n this.fit = fit;\n this.layout();\n }\n loadResource(url) {\n this.page.hide();\n this.page.loadPage(url);\n }\n onIframeLoaded() {\n if (!this.page.size) {\n // FIXME: raise error\n }\n else {\n this.layout();\n }\n }\n layout() {\n if (!this.page.size || !this.viewport) {\n return;\n }\n const margins = {\n top: this.insets.top,\n right: this.insets.right,\n bottom: this.insets.bottom,\n left: this.insets.left,\n };\n this.page.setMargins(margins);\n const safeDrawingSize = {\n width: this.viewport.width - this.insets.left - this.insets.right,\n height: this.viewport.height - this.insets.top - this.insets.bottom,\n };\n const scale = computeScale(this.fit, this.page.size, safeDrawingSize);\n this.metaViewport.content = new ViewportStringBuilder()\n .setInitialScale(scale)\n .setMinimumScale(scale)\n .setWidth(this.page.size.width)\n .setHeight(this.page.size.height)\n .build();\n this.scale = scale;\n this.page.show();\n }\n}\n","export function computeScale(fit, content, container) {\n switch (fit) {\n case \"contain\" /* Fit.Contain */:\n return fitContain(content, container);\n case \"width\" /* Fit.Width */:\n return fitWidth(content, container);\n case \"height\" /* Fit.Height */:\n return fitHeight(content, container);\n }\n}\nfunction fitContain(content, container) {\n const widthRatio = container.width / content.width;\n const heightRatio = container.height / content.height;\n return Math.min(widthRatio, heightRatio);\n}\nfunction fitWidth(content, container) {\n return container.width / content.width;\n}\nfunction fitHeight(content, container) {\n return container.height / content.height;\n}\n","export class BridgeGesturesAdapter {\n constructor(gesturesApi) {\n this.nativeApi = gesturesApi;\n }\n onTap(event) {\n this.nativeApi.onTap(JSON.stringify(event));\n }\n onLinkActivated(href) {\n this.nativeApi.onLinkActivated(href);\n }\n}\n","//\n// Copyright 2024 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\nimport { FixedSingleBridge } from \"./bridge/fixed-single-bridge\";\nconst iframe = document.getElementById(\"page\");\nconst metaViewport = document.querySelector(\"meta[name=viewport]\");\nwindow.singleArea = new FixedSingleBridge(window, iframe, metaViewport, window.gestures);\nwindow.initialization.onScriptsLoaded();\n","import { SingleAreaManager } from \"../fixed/single-area-manager\";\nimport { BridgeGesturesAdapter } from \"./fixed-gestures-bridge\";\nexport class FixedSingleBridge {\n constructor(window, iframe, metaViewport, gestures) {\n const listener = new BridgeGesturesAdapter(gestures);\n this.manager = new SingleAreaManager(window, iframe, metaViewport, listener);\n }\n loadResource(url) {\n this.manager.loadResource(url);\n }\n setViewport(viewporttWidth, viewportHeight, insetTop, insetRight, insetBottom, insetLeft) {\n const viewport = { width: viewporttWidth, height: viewportHeight };\n const insets = {\n top: insetTop,\n left: insetLeft,\n bottom: insetBottom,\n right: insetRight,\n };\n this.manager.setViewport(viewport, insets);\n }\n setFit(fit) {\n if (fit != \"contain\" && fit != \"width\" && fit != \"height\") {\n throw Error(`Invalid fit value: ${fit}`);\n }\n this.manager.setFit(fit);\n }\n}\n"],"names":["PageManager","constructor","window","iframe","listener","this","margins","top","right","bottom","left","contentWindow","Error","setMessagePort","messagePort","onmessage","message","onMessageFromIframe","show","style","display","hide","setMargins","marginTop","marginLeft","marginBottom","marginRight","loadPage","url","src","setPlaceholder","size","visibility","width","height","event","data","kind","onContentSizeAvailable","onTap","x","y","onLinkActivated","href","onIframeLoaded","ViewportStringBuilder","setInitialScale","scale","initialScale","setMinimumScale","minimumScale","setWidth","setHeight","build","components","push","join","GesturesDetector","document","addEventListener","onClick","defaultPrevented","selection","getSelection","type","nearestElement","target","HTMLElement","nearestInteractiveElement","HTMLAnchorElement","stopPropagation","preventDefault","element","indexOf","nodeName","toLowerCase","hasAttribute","getAttribute","parentElement","SingleAreaManager","metaViewport","fit","insets","source","ports","page","tapEvent","clientX","visualViewport","offsetLeft","clientY","offsetTop","_","pageListener","boundingRect","getBoundingClientRect","setViewport","viewport","layout","setFit","loadResource","safeDrawingSize","content","container","widthRatio","heightRatio","Math","min","fitContain","fitWidth","fitHeight","computeScale","BridgeGesturesAdapter","gesturesApi","nativeApi","JSON","stringify","getElementById","querySelector","singleArea","gestures","manager","viewporttWidth","viewportHeight","insetTop","insetRight","insetBottom","insetLeft","initialization","onScriptsLoaded"],"sourceRoot":""} \ No newline at end of file diff --git a/readium/navigators/web/scripts/dist/prepaginated-double-index.html b/readium/navigators/web/scripts/dist/prepaginated-double-index.html new file mode 100644 index 0000000000..d6041ae251 --- /dev/null +++ b/readium/navigators/web/scripts/dist/prepaginated-double-index.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + +
+ + + +
+ + + + diff --git a/readium/navigators/web/scripts/dist/prepaginated-single-index.html b/readium/navigators/web/scripts/dist/prepaginated-single-index.html new file mode 100644 index 0000000000..dfae3d6ce2 --- /dev/null +++ b/readium/navigators/web/scripts/dist/prepaginated-single-index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + +
+ +
+ + + diff --git a/readium/navigators/web/scripts/package.json b/readium/navigators/web/scripts/package.json new file mode 100644 index 0000000000..b9670beee1 --- /dev/null +++ b/readium/navigators/web/scripts/package.json @@ -0,0 +1,40 @@ +{ + "name": "readium-js", + "author": "Readium Foundation", + "version": "0.1.0", + "license": "BSD-3-Clause", + "description": "A set of scripts for the EPUB navigator", + "private": true, + "scripts": { + "dev": "vite dev", + "bundle": "webpack", + "lint": "eslint 'src/**'", + "checkformat": "prettier --check '**/*.js'", + "format": "prettier --list-different --write '**/*.js' '**/*.ts'" + }, + "browserslist": [ + "Android >= 4" + ], + "devDependencies": { + "copy-webpack-plugin": "^12.0.2", + "@babel/core": "^7.23.0", + "@babel/preset-env": "^7.22.20", + "babel-loader": "^8.3.0", + "eslint": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^8.3.0", + "@typescript-eslint/parser": "^8.3.0", + "prettier": "2.3.1", + "vite": "^5.4.1", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "approx-string-match": "^1.1.0", + "css-selector-generator": "^3.6.6", + "hash.js": "^1.1.7", + "source-map-loader": "^5.0.0", + "string.prototype.matchall": "^4.0.10", + "ts-loader": "^9.5.1" + }, + "packageManager": "pnpm@8.8.0+sha256.d713a5750e41c3660d1e090608c7f607ad00d1dd5ba9b6552b5f390bf37924e9" +} diff --git a/readium/navigators/web/scripts/pnpm-lock.yaml b/readium/navigators/web/scripts/pnpm-lock.yaml new file mode 100644 index 0000000000..3ba6fb5827 --- /dev/null +++ b/readium/navigators/web/scripts/pnpm-lock.yaml @@ -0,0 +1,4067 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + approx-string-match: + specifier: ^1.1.0 + version: 1.1.0 + css-selector-generator: + specifier: ^3.6.6 + version: 3.6.6 + hash.js: + specifier: ^1.1.7 + version: 1.1.7 + source-map-loader: + specifier: ^5.0.0 + version: 5.0.0(webpack@5.88.2) + string.prototype.matchall: + specifier: ^4.0.10 + version: 4.0.10 + ts-loader: + specifier: ^9.5.1 + version: 9.5.1(typescript@5.5.4)(webpack@5.88.2) + +devDependencies: + '@babel/core': + specifier: ^7.23.0 + version: 7.23.0 + '@babel/preset-env': + specifier: ^7.22.20 + version: 7.22.20(@babel/core@7.23.0) + '@typescript-eslint/eslint-plugin': + specifier: ^8.3.0 + version: 8.3.0(@typescript-eslint/parser@8.3.0)(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/parser': + specifier: ^8.3.0 + version: 8.3.0(eslint@8.57.0)(typescript@5.5.4) + babel-loader: + specifier: ^8.3.0 + version: 8.3.0(@babel/core@7.23.0)(webpack@5.88.2) + copy-webpack-plugin: + specifier: ^12.0.2 + version: 12.0.2(webpack@5.88.2) + eslint: + specifier: ^8.57.0 + version: 8.57.0 + prettier: + specifier: 2.3.1 + version: 2.3.1 + vite: + specifier: ^5.4.1 + version: 5.4.1 + webpack: + specifier: ^5.88.2 + version: 5.88.2(webpack-cli@5.1.4) + webpack-cli: + specifier: ^5.1.4 + version: 5.1.4(webpack@5.88.2) + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + dev: true + + /@babel/code-frame@7.22.13: + resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.22.20 + chalk: 2.4.2 + dev: true + + /@babel/compat-data@7.22.20: + resolution: {integrity: sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.23.0: + resolution: {integrity: sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.0) + '@babel/helpers': 7.23.1 + '@babel/parser': 7.23.0 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.0 + '@babel/types': 7.23.0 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.23.0: + resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + jsesc: 2.5.2 + dev: true + + /@babel/helper-annotate-as-pure@7.22.5: + resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: + resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-compilation-targets@7.22.15: + resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.22.20 + '@babel/helper-validator-option': 7.22.15 + browserslist: 4.22.1 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: true + + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + regexpu-core: 5.3.2 + semver: 6.3.1 + dev: true + + /@babel/helper-define-polyfill-provider@0.4.2(@babel/core@7.23.0): + resolution: {integrity: sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + debug: 4.3.4 + lodash.debounce: 4.0.8 + resolve: 1.22.6 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-member-expression-to-functions@7.23.0: + resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-module-transforms@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/helper-optimise-call-expression@7.22.5: + resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.0): + resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-wrap-function': 7.22.20 + dev: true + + /@babel/helper-replace-supers@7.22.20(@babel/core@7.23.0): + resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-skip-transparent-expression-wrappers@7.22.5: + resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-string-parser@7.22.5: + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.22.15: + resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-wrap-function@7.22.20: + resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.23.0 + '@babel/template': 7.22.15 + '@babel/types': 7.23.0 + dev: true + + /@babel/helpers@7.23.1: + resolution: {integrity: sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.0 + '@babel/types': 7.23.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight@7.22.20: + resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.23.0: + resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.23.0) + dev: true + + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.0): + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + dev: true + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.0): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.0): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.0): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-attributes@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.0): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.0): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.0): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.0): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.0): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.0): + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-async-generator-functions@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.0) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-block-scoping@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-class-properties@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-class-static-block@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-classes@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.0) + '@babel/helper-split-export-declaration': 7.22.6 + globals: 11.12.0 + dev: true + + /@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/template': 7.22.15 + dev: true + + /@babel/plugin-transform-destructuring@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-dynamic-import@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-export-namespace-from@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-for-of@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-function-name@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-json-strings@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-literals@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-logical-assignment-operators@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-amd@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-commonjs@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-systemjs@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-new-target@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-nullish-coalescing-operator@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-numeric-separator@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-object-rest-spread@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.20 + '@babel/core': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-object-super@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-optional-catch-binding@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-optional-chaining@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-parameters@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-private-methods@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-private-property-in-object@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-regenerator@7.22.10(@babel/core@7.23.0): + resolution: {integrity: sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + regenerator-transform: 0.15.2 + dev: true + + /@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-spread@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + dev: true + + /@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-escapes@7.22.10(@babel/core@7.23.0): + resolution: {integrity: sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-property-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-sets-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/preset-env@7.22.20(@babel/core@7.23.0): + resolution: {integrity: sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.20 + '@babel/core': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.0) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-syntax-import-attributes': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.23.0) + '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-async-generator-functions': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-block-scoping': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-class-properties': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-class-static-block': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-classes': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-destructuring': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-dynamic-import': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-export-namespace-from': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-for-of': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-json-strings': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-logical-assignment-operators': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-modules-amd': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-modules-commonjs': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-modules-systemjs': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-numeric-separator': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-object-rest-spread': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-optional-catch-binding': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-transform-private-methods': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-private-property-in-object': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-regenerator': 7.22.10(@babel/core@7.23.0) + '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-unicode-escapes': 7.22.10(@babel/core@7.23.0) + '@babel/plugin-transform-unicode-property-regex': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-unicode-sets-regex': 7.22.5(@babel/core@7.23.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.23.0) + '@babel/types': 7.23.0 + babel-plugin-polyfill-corejs2: 0.4.5(@babel/core@7.23.0) + babel-plugin-polyfill-corejs3: 0.8.4(@babel/core@7.23.0) + babel-plugin-polyfill-regenerator: 0.5.2(@babel/core@7.23.0) + core-js-compat: 3.33.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.23.0): + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/types': 7.23.0 + esutils: 2.0.3 + dev: true + + /@babel/regjsgen@0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + dev: true + + /@babel/runtime@7.23.1: + resolution: {integrity: sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: true + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.13 + '@babel/parser': 7.23.0 + '@babel/types': 7.23.0 + dev: true + + /@babel/traverse@7.23.0: + resolution: {integrity: sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.23.0 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.0 + '@babel/types': 7.23.0 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.23.0: + resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@discoveryjs/json-ext@0.5.7: + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + + /@esbuild/aix-ppc64@0.21.5: + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.21.5: + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.21.5: + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.21.5: + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.21.5: + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.21.5: + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.21.5: + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.21.5: + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.21.5: + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.21.5: + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.21.5: + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.21.5: + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.21.5: + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.21.5: + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.21.5: + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.21.5: + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.21.5: + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.21.5: + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.21.5: + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.21.5: + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.21.5: + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.21.5: + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.21.5: + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.11.0: + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.22.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.57.0: + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.3: + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.19 + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/source-map@0.3.5: + resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: true + + /@rollup/rollup-android-arm-eabi@4.20.0: + resolution: {integrity: sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.20.0: + resolution: {integrity: sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.20.0: + resolution: {integrity: sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.20.0: + resolution: {integrity: sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.20.0: + resolution: {integrity: sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-musleabihf@4.20.0: + resolution: {integrity: sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.20.0: + resolution: {integrity: sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.20.0: + resolution: {integrity: sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-powerpc64le-gnu@4.20.0: + resolution: {integrity: sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.20.0: + resolution: {integrity: sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-s390x-gnu@4.20.0: + resolution: {integrity: sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.20.0: + resolution: {integrity: sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.20.0: + resolution: {integrity: sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.20.0: + resolution: {integrity: sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.20.0: + resolution: {integrity: sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.20.0: + resolution: {integrity: sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@sindresorhus/merge-streams@2.3.0: + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + dev: true + + /@types/eslint-scope@3.7.5: + resolution: {integrity: sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==} + dependencies: + '@types/eslint': 8.44.3 + '@types/estree': 1.0.2 + + /@types/eslint@8.44.3: + resolution: {integrity: sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==} + dependencies: + '@types/estree': 1.0.2 + '@types/json-schema': 7.0.13 + + /@types/estree@1.0.2: + resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==} + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + + /@types/json-schema@7.0.13: + resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} + + /@types/node@20.8.2: + resolution: {integrity: sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==} + + /@typescript-eslint/eslint-plugin@8.3.0(@typescript-eslint/parser@8.3.0)(eslint@8.57.0)(typescript@5.5.4): + resolution: {integrity: sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 8.3.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.3.0 + '@typescript-eslint/type-utils': 8.3.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.3.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.3.0 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@8.3.0(eslint@8.57.0)(typescript@5.5.4): + resolution: {integrity: sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 8.3.0 + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.3.0 + debug: 4.3.4 + eslint: 8.57.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@8.3.0: + resolution: {integrity: sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/visitor-keys': 8.3.0 + dev: true + + /@typescript-eslint/type-utils@8.3.0(eslint@8.57.0)(typescript@5.5.4): + resolution: {integrity: sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.3.0(eslint@8.57.0)(typescript@5.5.4) + debug: 4.3.4 + ts-api-utils: 1.3.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + dev: true + + /@typescript-eslint/types@8.3.0: + resolution: {integrity: sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + + /@typescript-eslint/typescript-estree@8.3.0(typescript@5.5.4): + resolution: {integrity: sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/visitor-keys': 8.3.0 + debug: 4.3.4 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@8.3.0(eslint@8.57.0)(typescript@5.5.4): + resolution: {integrity: sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@typescript-eslint/scope-manager': 8.3.0 + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + eslint: 8.57.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@8.3.0: + resolution: {integrity: sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.3.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@webassemblyjs/ast@1.11.6: + resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} + dependencies: + '@webassemblyjs/helper-numbers': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + + /@webassemblyjs/floating-point-hex-parser@1.11.6: + resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + + /@webassemblyjs/helper-api-error@1.11.6: + resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + + /@webassemblyjs/helper-buffer@1.11.6: + resolution: {integrity: sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==} + + /@webassemblyjs/helper-numbers@1.11.6: + resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@xtuc/long': 4.2.2 + + /@webassemblyjs/helper-wasm-bytecode@1.11.6: + resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + + /@webassemblyjs/helper-wasm-section@1.11.6: + resolution: {integrity: sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + + /@webassemblyjs/ieee754@1.11.6: + resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + dependencies: + '@xtuc/ieee754': 1.2.0 + + /@webassemblyjs/leb128@1.11.6: + resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + dependencies: + '@xtuc/long': 4.2.2 + + /@webassemblyjs/utf8@1.11.6: + resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + + /@webassemblyjs/wasm-edit@1.11.6: + resolution: {integrity: sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-wasm-section': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-opt': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + '@webassemblyjs/wast-printer': 1.11.6 + + /@webassemblyjs/wasm-gen@1.11.6: + resolution: {integrity: sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + + /@webassemblyjs/wasm-opt@1.11.6: + resolution: {integrity: sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + + /@webassemblyjs/wasm-parser@1.11.6: + resolution: {integrity: sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + + /@webassemblyjs/wast-printer@1.11.6: + resolution: {integrity: sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@xtuc/long': 4.2.2 + + /@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.88.2): + resolution: {integrity: sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + dependencies: + webpack: 5.88.2(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.88.2) + + /@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.88.2): + resolution: {integrity: sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + dependencies: + webpack: 5.88.2(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.88.2) + + /@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.88.2): + resolution: {integrity: sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + webpack-dev-server: '*' + peerDependenciesMeta: + webpack-dev-server: + optional: true + dependencies: + webpack: 5.88.2(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.88.2) + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + /acorn-import-assertions@1.9.0(acorn@8.10.0): + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.10.0 + + /acorn-jsx@5.3.2(acorn@8.10.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.10.0 + dev: true + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + + /ajv-formats@2.1.1(ajv@8.12.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.12.0 + dev: true + + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + + /ajv-keywords@5.1.0(ajv@8.12.0): + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + dependencies: + ajv: 8.12.0 + fast-deep-equal: 3.1.3 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /approx-string-match@1.1.0: + resolution: {integrity: sha512-j1yQB9XhfGWsvTfHEuNsR/SrUT4XQDkAc0PEjMifyi97931LmNQyLsO6HbuvZ3HeMx+3Dvk8m8XGkUF+8lCeqw==} + dev: false + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: false + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: false + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: false + + /babel-loader@8.3.0(@babel/core@7.23.0)(webpack@5.88.2): + resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} + engines: {node: '>= 8.9'} + peerDependencies: + '@babel/core': ^7.0.0 + webpack: '>=2' + dependencies: + '@babel/core': 7.23.0 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 5.88.2(webpack-cli@5.1.4) + dev: true + + /babel-plugin-polyfill-corejs2@0.4.5(@babel/core@7.23.0): + resolution: {integrity: sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/compat-data': 7.22.20 + '@babel/core': 7.23.0 + '@babel/helper-define-polyfill-provider': 0.4.2(@babel/core@7.23.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3@0.8.4(@babel/core@7.23.0): + resolution: {integrity: sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-define-polyfill-provider': 0.4.2(@babel/core@7.23.0) + core-js-compat: 3.33.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator@0.5.2(@babel/core@7.23.0): + resolution: {integrity: sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-define-polyfill-provider': 0.4.2(@babel/core@7.23.0) + transitivePeerDependencies: + - supports-color + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + + /browserslist@4.22.1: + resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001651 + electron-to-chromium: 1.4.542 + node-releases: 2.0.13 + update-browserslist-db: 1.0.13(browserslist@4.22.1) + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: false + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /caniuse-lite@1.0.30001651: + resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /chrome-trace-event@1.0.3: + resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} + engines: {node: '>=6.0'} + + /clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + /commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /copy-webpack-plugin@12.0.2(webpack@5.88.2): + resolution: {integrity: sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: ^5.1.0 + dependencies: + fast-glob: 3.3.2 + glob-parent: 6.0.2 + globby: 14.0.2 + normalize-path: 3.0.0 + schema-utils: 4.2.0 + serialize-javascript: 6.0.2 + webpack: 5.88.2(webpack-cli@5.1.4) + dev: true + + /core-js-compat@3.33.0: + resolution: {integrity: sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==} + dependencies: + browserslist: 4.22.1 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /css-selector-generator@3.6.6: + resolution: {integrity: sha512-tNy7zBawE2EjuR0Htl3AbY8ZQ5TJBt3YFNh8xIPkLahDtE1mmFKDp2uHF4TdByElpFnzNy3HbDF0ITazZ/Lp5w==} + dev: false + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: false + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: false + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /electron-to-chromium@1.4.542: + resolution: {integrity: sha512-6+cpa00G09N3sfh2joln4VUXHquWrOFx3FLZqiVQvl45+zS9DskDBTPvob+BhvFRmTBkyDSk0vvLMMRo/qc6mQ==} + + /emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + dev: true + + /enhanced-resolve@5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + /envinfo@7.10.0: + resolution: {integrity: sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==} + engines: {node: '>=4'} + hasBin: true + + /es-abstract@1.22.2: + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.4 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: false + + /es-module-lexer@1.3.1: + resolution: {integrity: sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==} + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.4 + has-tostringtag: 1.0.0 + dev: false + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: false + + /esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.11.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.22.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.1.0 + dev: true + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.1.0: + resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==} + engines: {node: '>=12.0.0'} + dependencies: + flatted: 3.2.9 + keyv: 4.5.3 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: false + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + functions-have-names: 1.2.3 + dev: false + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: false + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.4 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: false + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.22.0: + resolution: {integrity: sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: false + + /globby@14.0.2: + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.2 + ignore: 5.3.2 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: false + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: false + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + + /hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: false + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.4 + side-channel: 1.0.4 + dev: false + + /interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: false + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: false + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.4 + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: false + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: false + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: false + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: false + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: false + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.8.2 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /keyv@4.5.3: + resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + /loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: false + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + /node-releases@2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + /picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + + /postcss@8.4.41: + resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier@2.3.1: + resolution: {integrity: sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + + /rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + dependencies: + resolve: 1.22.6 + + /regenerate-unicode-properties@10.1.1: + resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: true + + /regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + dependencies: + '@babel/runtime': 7.23.1 + dev: true + + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: false + + /regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.1 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + dev: true + + /regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + dev: true + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + /resolve@1.22.6: + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup@4.20.0: + resolution: {integrity: sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.20.0 + '@rollup/rollup-android-arm64': 4.20.0 + '@rollup/rollup-darwin-arm64': 4.20.0 + '@rollup/rollup-darwin-x64': 4.20.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.20.0 + '@rollup/rollup-linux-arm-musleabihf': 4.20.0 + '@rollup/rollup-linux-arm64-gnu': 4.20.0 + '@rollup/rollup-linux-arm64-musl': 4.20.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.20.0 + '@rollup/rollup-linux-riscv64-gnu': 4.20.0 + '@rollup/rollup-linux-s390x-gnu': 4.20.0 + '@rollup/rollup-linux-x64-gnu': 4.20.0 + '@rollup/rollup-linux-x64-musl': 4.20.0 + '@rollup/rollup-win32-arm64-msvc': 4.20.0 + '@rollup/rollup-win32-ia32-msvc': 4.20.0 + '@rollup/rollup-win32-x64-msvc': 4.20.0 + fsevents: 2.3.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /schema-utils@2.7.1: + resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} + engines: {node: '>= 8.9.0'} + dependencies: + '@types/json-schema': 7.0.13 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: true + + /schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.13 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + /schema-utils@4.2.0: + resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} + engines: {node: '>= 12.13.0'} + dependencies: + '@types/json-schema': 7.0.13 + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + ajv-keywords: 5.1.0(ajv@8.12.0) + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: false + + /semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /serialize-javascript@6.0.1: + resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} + dependencies: + randombytes: 2.1.0 + + /serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + dependencies: + randombytes: 2.1.0 + dev: true + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.0 + dev: false + + /shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + dependencies: + kind-of: 6.0.3 + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: false + + /slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + dev: true + + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + /source-map-loader@5.0.0(webpack@5.88.2): + resolution: {integrity: sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: ^5.72.1 + dependencies: + iconv-lite: 0.6.3 + source-map-js: 1.2.0 + webpack: 5.88.2(webpack-cli@5.1.4) + dev: false + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: false + + /string.prototype.matchall@4.0.10: + resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + regexp.prototype.flags: 1.5.1 + set-function-name: 2.0.1 + side-channel: 1.0.4 + dev: false + + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: false + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: false + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + /terser-webpack-plugin@5.3.9(webpack@5.88.2): + resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.1 + terser: 5.21.0 + webpack: 5.88.2(webpack-cli@5.1.4) + + /terser@5.21.0: + resolution: {integrity: sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.5 + acorn: 8.10.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /ts-api-utils@1.3.0(typescript@5.5.4): + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.5.4 + dev: true + + /ts-loader@9.5.1(typescript@5.5.4)(webpack@5.88.2): + resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.15.0 + micromatch: 4.0.7 + semver: 7.5.4 + source-map: 0.7.4 + typescript: 5.5.4 + webpack: 5.88.2(webpack-cli@5.1.4) + dev: false + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: false + + /typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: false + + /unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + dev: true + + /unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + + /unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + dev: true + + /update-browserslist-db@1.0.13(browserslist@4.22.1): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.22.1 + escalade: 3.1.1 + picocolors: 1.0.0 + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + + /vite@5.4.1: + resolution: {integrity: sha512-1oE6yuNXssjrZdblI9AfBbHCC41nnyoVoEZxQnID6yvQZAFBzxxkqoFLtHUMkYunL8hwOLEjgTuxpkRxvba3kA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.21.5 + postcss: 8.4.41 + rollup: 4.20.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + /webpack-cli@5.1.4(webpack@5.88.2): + resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==} + engines: {node: '>=14.15.0'} + hasBin: true + peerDependencies: + '@webpack-cli/generators': '*' + webpack: 5.x.x + webpack-bundle-analyzer: '*' + webpack-dev-server: '*' + peerDependenciesMeta: + '@webpack-cli/generators': + optional: true + webpack-bundle-analyzer: + optional: true + webpack-dev-server: + optional: true + dependencies: + '@discoveryjs/json-ext': 0.5.7 + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.88.2) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.88.2) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.88.2) + colorette: 2.0.20 + commander: 10.0.1 + cross-spawn: 7.0.3 + envinfo: 7.10.0 + fastest-levenshtein: 1.0.16 + import-local: 3.1.0 + interpret: 3.1.1 + rechoir: 0.8.0 + webpack: 5.88.2(webpack-cli@5.1.4) + webpack-merge: 5.9.0 + + /webpack-merge@5.9.0: + resolution: {integrity: sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==} + engines: {node: '>=10.0.0'} + dependencies: + clone-deep: 4.0.1 + wildcard: 2.0.1 + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + /webpack@5.88.2(webpack-cli@5.1.4): + resolution: {integrity: sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.5 + '@types/estree': 1.0.2 + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/wasm-edit': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + acorn: 8.10.0 + acorn-import-assertions: 1.9.0(acorn@8.10.0) + browserslist: 4.22.1 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.15.0 + es-module-lexer: 1.3.1 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.9(webpack@5.88.2) + watchpack: 2.4.0 + webpack-cli: 5.1.4(webpack@5.88.2) + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: false + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: false + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: false + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/readium/navigators/web/scripts/public/fixed-double-index.html b/readium/navigators/web/scripts/public/fixed-double-index.html new file mode 100644 index 0000000000..0a15204dda --- /dev/null +++ b/readium/navigators/web/scripts/public/fixed-double-index.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + +
+
+ +
+
+
+ + +
+ + + + diff --git a/readium/navigators/web/scripts/public/fixed-single-index.html b/readium/navigators/web/scripts/public/fixed-single-index.html new file mode 100644 index 0000000000..4c8982b165 --- /dev/null +++ b/readium/navigators/web/scripts/public/fixed-single-index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + +
+ +
+ + + diff --git a/readium/navigators/web/scripts/src/bridge/fixed-double-bridge.ts b/readium/navigators/web/scripts/src/bridge/fixed-double-bridge.ts new file mode 100644 index 0000000000..d5be6cd6f1 --- /dev/null +++ b/readium/navigators/web/scripts/src/bridge/fixed-double-bridge.ts @@ -0,0 +1,55 @@ +import { Insets, Size } from "../common/types" +import { DoubleAreaManager } from "../fixed/double-area-manager" +import { GesturesBridge, BridgeGesturesAdapter } from "./fixed-gestures-bridge" +import { Fit } from "../util/fit" + +export class FixedDoubleBridge { + private readonly manager: DoubleAreaManager + + constructor( + window: Window, + leftIframe: HTMLIFrameElement, + rightIframe: HTMLIFrameElement, + metaViewport: HTMLMetaElement, + gestures: GesturesBridge + ) { + const listener = new BridgeGesturesAdapter(gestures) + this.manager = new DoubleAreaManager( + window, + leftIframe, + rightIframe, + metaViewport, + listener + ) + } + + loadSpread(spread: { left?: string; right?: string }) { + this.manager.loadSpread(spread) + } + + setViewport( + viewporttWidth: number, + viewportHeight: number, + insetTop: number, + insetRight: number, + insetBottom: number, + insetLeft: number + ) { + const viewport: Size = { width: viewporttWidth, height: viewportHeight } + const insets: Insets = { + top: insetTop, + left: insetLeft, + bottom: insetBottom, + right: insetRight, + } + this.manager.setViewport(viewport, insets) + } + + setFit(fit: string) { + if (fit != "contain" && fit != "width" && fit != "height") { + throw Error(`Invalid fit value: ${fit}`) + } + + this.manager.setFit(fit as Fit) + } +} diff --git a/readium/navigators/web/scripts/src/bridge/fixed-gestures-bridge.ts b/readium/navigators/web/scripts/src/bridge/fixed-gestures-bridge.ts new file mode 100644 index 0000000000..bb1dd452be --- /dev/null +++ b/readium/navigators/web/scripts/src/bridge/fixed-gestures-bridge.ts @@ -0,0 +1,27 @@ +import { AreaManager } from "../fixed/area-manager" + +export interface GesturesBridge { + onTap(event: string): void + onLinkActivated(href: string): void +} + +export interface TapEvent { + x: number + y: number +} + +export class BridgeGesturesAdapter implements AreaManager.Listener { + readonly nativeApi: GesturesBridge + + constructor(gesturesApi: GesturesBridge) { + this.nativeApi = gesturesApi + } + + onTap(event: TapEvent): void { + this.nativeApi.onTap(JSON.stringify(event)) + } + + onLinkActivated(href: string): void { + this.nativeApi.onLinkActivated(href) + } +} diff --git a/readium/navigators/web/scripts/src/bridge/fixed-initialization-bridge.ts b/readium/navigators/web/scripts/src/bridge/fixed-initialization-bridge.ts new file mode 100644 index 0000000000..059196892f --- /dev/null +++ b/readium/navigators/web/scripts/src/bridge/fixed-initialization-bridge.ts @@ -0,0 +1,3 @@ +export interface InitializationBridge { + onScriptsLoaded: () => void +} diff --git a/readium/navigators/web/scripts/src/bridge/fixed-single-bridge.ts b/readium/navigators/web/scripts/src/bridge/fixed-single-bridge.ts new file mode 100644 index 0000000000..96e8fb7fb0 --- /dev/null +++ b/readium/navigators/web/scripts/src/bridge/fixed-single-bridge.ts @@ -0,0 +1,48 @@ +import { Insets, Size } from "../common/types" +import { SingleAreaManager } from "../fixed/single-area-manager" +import { Fit } from "../util/fit" +import { GesturesBridge, BridgeGesturesAdapter } from "./fixed-gestures-bridge" + +export class FixedSingleBridge { + private readonly manager: SingleAreaManager + + constructor( + window: Window, + iframe: HTMLIFrameElement, + metaViewport: HTMLMetaElement, + gestures: GesturesBridge + ) { + const listener = new BridgeGesturesAdapter(gestures) + this.manager = new SingleAreaManager(window, iframe, metaViewport, listener) + } + + loadResource(url: string) { + this.manager.loadResource(url) + } + + setViewport( + viewporttWidth: number, + viewportHeight: number, + insetTop: number, + insetRight: number, + insetBottom: number, + insetLeft: number + ) { + const viewport: Size = { width: viewporttWidth, height: viewportHeight } + const insets: Insets = { + top: insetTop, + left: insetLeft, + bottom: insetBottom, + right: insetRight, + } + this.manager.setViewport(viewport, insets) + } + + setFit(fit: string) { + if (fit != "contain" && fit != "width" && fit != "height") { + throw Error(`Invalid fit value: ${fit}`) + } + + this.manager.setFit(fit as Fit) + } +} diff --git a/readium/navigators/web/scripts/src/common/events.ts b/readium/navigators/web/scripts/src/common/events.ts new file mode 100644 index 0000000000..5a6af1ab2f --- /dev/null +++ b/readium/navigators/web/scripts/src/common/events.ts @@ -0,0 +1,4 @@ +export interface TapEvent { + x: number + y: number +} diff --git a/readium/navigators/web/scripts/src/common/gestures.ts b/readium/navigators/web/scripts/src/common/gestures.ts new file mode 100644 index 0000000000..00a7a31caa --- /dev/null +++ b/readium/navigators/web/scripts/src/common/gestures.ts @@ -0,0 +1,93 @@ +export interface GesturesListener { + onTap(event: MouseEvent): void + onLinkActivated(href: string): void +} + +export class GesturesDetector { + private readonly listener: GesturesListener + + private readonly window: Window + + constructor(window: Window, listener: GesturesListener) { + this.window = window + this.listener = listener + document.addEventListener( + "click", + (event) => { + this.onClick(event) + }, + false + ) + } + + private onClick(event: MouseEvent) { + if (event.defaultPrevented) { + return + } + + const selection = this.window.getSelection() + if (selection && selection.type == "Range") { + // There's an on-going selection, the tap will dismiss it so we don't forward it. + // selection.type might be None (collapsed) or Caret with a collapsed range + // when there is not true selection. + return + } + + let nearestElement: Element | null + if (event.target instanceof HTMLElement) { + nearestElement = this.nearestInteractiveElement(event.target) + } else { + nearestElement = null + } + + if (nearestElement) { + if (nearestElement instanceof HTMLAnchorElement) { + this.listener.onLinkActivated(nearestElement.href) + } + } else { + this.listener.onTap(event) + } + + event.stopPropagation() + event.preventDefault() + } + + // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling + private nearestInteractiveElement(element: Element): Element | null { + if (element == null) { + return null + } + const interactiveTags = [ + "a", + "audio", + "button", + "canvas", + "details", + "input", + "label", + "option", + "select", + "submit", + "textarea", + "video", + ] + if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) { + return element + } + + // Checks whether the element is editable by the user. + if ( + element.hasAttribute("contenteditable") && + element.getAttribute("contenteditable")!.toLowerCase() != "false" + ) { + return element + } + + // Checks parents recursively because the touch might be for example on an inside a
. + if (element.parentElement) { + return this.nearestInteractiveElement(element.parentElement) + } + + return null + } +} diff --git a/readium/navigators/web/scripts/src/common/types.ts b/readium/navigators/web/scripts/src/common/types.ts new file mode 100644 index 0000000000..094b35f76d --- /dev/null +++ b/readium/navigators/web/scripts/src/common/types.ts @@ -0,0 +1,18 @@ +export interface Size { + width: number + height: number +} + +export interface Margins { + top: number + left: number + bottom: number + right: number +} + +export interface Insets { + top: number + left: number + bottom: number + right: number +} diff --git a/readium/navigators/web/scripts/src/fixed/area-manager.ts b/readium/navigators/web/scripts/src/fixed/area-manager.ts new file mode 100644 index 0000000000..24641faf9e --- /dev/null +++ b/readium/navigators/web/scripts/src/fixed/area-manager.ts @@ -0,0 +1,8 @@ +import { TapEvent } from "../common/events" + +export namespace AreaManager { + export interface Listener { + onTap(event: TapEvent): void + onLinkActivated(href: string): void + } +} diff --git a/readium/navigators/web/scripts/src/fixed/double-area-manager.ts b/readium/navigators/web/scripts/src/fixed/double-area-manager.ts new file mode 100644 index 0000000000..7e8b647bdc --- /dev/null +++ b/readium/navigators/web/scripts/src/fixed/double-area-manager.ts @@ -0,0 +1,192 @@ +import { Size, Insets } from "../common/types" +import { computeScale, Fit } from "../util/fit" +import { PageManager } from "./page-manager" +import { AreaManager } from "./area-manager" +import { ViewportStringBuilder } from "../util/viewport" +import { GesturesDetector } from "../common/gestures" +import { TapEvent } from "../common/events" + +export class DoubleAreaManager { + private readonly metaViewport: HTMLMetaElement + + private readonly leftPage: PageManager + + private readonly rightPage: PageManager + + private fit: Fit = Fit.Contain + + private insets: Insets = { top: 0, right: 0, bottom: 0, left: 0 } + + private viewport?: Size + + private spread?: { left?: string; right?: string } + + constructor( + window: Window, + leftIframe: HTMLIFrameElement, + rightIframe: HTMLIFrameElement, + metaViewport: HTMLMetaElement, + listener: AreaManager.Listener + ) { + window.addEventListener("message", (event) => { + if (!event.ports[0]) { + return + } + + if (event.source === leftIframe.contentWindow) { + this.leftPage.setMessagePort(event.ports[0]) + } else if (event.source == rightIframe.contentWindow) { + this.rightPage.setMessagePort(event.ports[0]) + } + }) + + const wrapperGesturesListener = { + onTap: (event: MouseEvent) => { + const tapEvent = { + x: + (event.clientX - visualViewport!.offsetLeft) * + visualViewport!.scale, + y: + (event.clientY - visualViewport!.offsetTop) * visualViewport!.scale, + } + listener.onTap(tapEvent) + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onLinkActivated: (_: string) => { + throw Error("No interactive element in the root document.") + }, + } + new GesturesDetector(window, wrapperGesturesListener) + + const leftPageListener = { + onIframeLoaded: () => { + this.layout() + }, + onTap: (event: TapEvent) => { + const boundingRect = leftIframe.getBoundingClientRect() + const tapEvent = { + x: + (event.x + boundingRect.left - visualViewport!.offsetLeft) * + visualViewport!.scale, + y: + (event.y + boundingRect.top - visualViewport!.offsetTop) * + visualViewport!.scale, + } + listener.onTap(tapEvent) + }, + onLinkActivated: (href: string) => { + listener.onLinkActivated(href) + }, + } + + const rightPageListener = { + onIframeLoaded: () => { + this.layout() + }, + onTap: (event: TapEvent) => { + const boundingRect = rightIframe.getBoundingClientRect() + const tapEvent = { + x: + (event.x + boundingRect.left - visualViewport!.offsetLeft) * + visualViewport!.scale, + y: + (event.y + boundingRect.top - visualViewport!.offsetTop) * + visualViewport!.scale, + } + listener.onTap(tapEvent) + }, + onLinkActivated: (href: string) => { + listener.onLinkActivated(href) + }, + } + this.leftPage = new PageManager(window, leftIframe, leftPageListener) + this.rightPage = new PageManager(window, rightIframe, rightPageListener) + this.metaViewport = metaViewport + } + + loadSpread(spread: { left?: string; right?: string }) { + this.leftPage.hide() + this.rightPage.hide() + this.spread = spread + + if (spread.left) { + this.leftPage.loadPage(spread.left) + } + + if (spread.right) { + this.rightPage.loadPage(spread.right) + } + } + + setViewport(size: Size, insets: Insets) { + if (this.viewport == size && this.insets == insets) { + return + } + + this.viewport = size + this.insets = insets + this.layout() + } + + setFit(fit: Fit) { + if (this.fit == fit) { + return + } + + this.fit = fit + this.layout() + } + + private layout() { + if ( + !this.viewport || + (!this.leftPage.size && this.spread!.left) || + (!this.rightPage.size && this.spread!.right) + ) { + return + } + + const leftMargins = { + top: this.insets.top, + right: 0, + bottom: this.insets.bottom, + left: this.insets.left, + } + this.leftPage.setMargins(leftMargins) + const rightMargins = { + top: this.insets.top, + right: this.insets.right, + bottom: this.insets.bottom, + left: 0, + } + this.rightPage.setMargins(rightMargins) + + if (!this.spread!.right) { + this.rightPage.setPlaceholder(this.leftPage.size!) + } else if (!this.spread!.left) { + this.leftPage.setPlaceholder(this.rightPage.size!) + } + + const contentWidth = this.leftPage.size!.width + this.rightPage.size!.width + const contentHeight = Math.max( + this.leftPage.size!.height, + this.rightPage.size!.height + ) + const contentSize = { width: contentWidth, height: contentHeight } + const safeDrawingSize = { + width: this.viewport.width - this.insets.left - this.insets.right, + height: this.viewport.height - this.insets.top - this.insets.bottom, + } + const scale = computeScale(this.fit, contentSize, safeDrawingSize) + + this.metaViewport.content = new ViewportStringBuilder() + .setInitialScale(scale) + .setMinimumScale(scale) + .setWidth(contentWidth) + .setHeight(contentHeight) + .build() + + this.leftPage.show() + this.rightPage.show() + } +} diff --git a/readium/navigators/web/scripts/src/fixed/iframe-message.ts b/readium/navigators/web/scripts/src/fixed/iframe-message.ts new file mode 100644 index 0000000000..0a268750ec --- /dev/null +++ b/readium/navigators/web/scripts/src/fixed/iframe-message.ts @@ -0,0 +1,34 @@ +import { Size } from "../common/types" + +interface ContentSizeMessage { + kind: "contentSize" + size?: Size +} + +interface TapMessage { + kind: "tap" + x: number + y: number +} + +interface LinkActivatedMessage { + kind: "linkActivated" + href: string +} + +export type IframeMessage = + | ContentSizeMessage + | TapMessage + | LinkActivatedMessage + +export class IframeMessageSender { + private messagePort: MessagePort + + constructor(messagePort: MessagePort) { + this.messagePort = messagePort + } + + send(message: IframeMessage) { + this.messagePort.postMessage(message) + } +} diff --git a/readium/navigators/web/scripts/src/fixed/page-manager.ts b/readium/navigators/web/scripts/src/fixed/page-manager.ts new file mode 100644 index 0000000000..8a0708d6f2 --- /dev/null +++ b/readium/navigators/web/scripts/src/fixed/page-manager.ts @@ -0,0 +1,100 @@ +import { Margins, Size } from "../common/types" +import { TapEvent } from "../common/events" +import { IframeMessage } from "./iframe-message" + +/** Manages a fixed layout resource embedded in an iframe. */ +export class PageManager { + private readonly iframe: HTMLIFrameElement + + private readonly listener: PageManager.Listener + + private margins: Margins = { top: 0, right: 0, bottom: 0, left: 0 } + + private messagePort?: MessagePort + + size?: Size + + constructor( + window: Window, + iframe: HTMLIFrameElement, + listener: PageManager.Listener + ) { + if (!iframe.contentWindow) { + throw Error("Iframe argument must have been attached to DOM.") + } + + this.listener = listener + this.iframe = iframe + } + + setMessagePort(messagePort: MessagePort) { + messagePort.onmessage = (message) => { + this.onMessageFromIframe(message) + } + } + + show() { + this.iframe.style.display = "unset" + } + + hide() { + this.iframe.style.display = "none" + } + + /** Sets page margins. */ + setMargins(margins: Margins) { + if (this.margins == margins) { + return + } + + this.iframe.style.marginTop = this.margins.top + "px" + this.iframe.style.marginLeft = this.margins.left + "px" + this.iframe.style.marginBottom = this.margins.bottom + "px" + this.iframe.style.marginRight = this.margins.right + "px" + } + + /** Loads page content. */ + loadPage(url: string) { + this.iframe.src = url + } + + /** Sets the size of this page without content. */ + setPlaceholder(size: Size) { + this.iframe.style.visibility = "hidden" + this.iframe.style.width = size.width + "px" + this.iframe.style.height = size.height + "px" + this.size = size + } + + private onMessageFromIframe(event: MessageEvent) { + const message = event.data as IframeMessage + switch (message.kind) { + case "contentSize": + return this.onContentSizeAvailable(message.size) + case "tap": + return this.listener.onTap({ x: message.x, y: message.y }) + case "linkActivated": + return this.listener.onLinkActivated(message.href) + } + } + + private onContentSizeAvailable(size?: Size) { + if (!size) { + //FIXME: handle edge case + return + } + this.iframe.style.width = size.width + "px" + this.iframe.style.height = size.height + "px" + this.size = size + + this.listener.onIframeLoaded() + } +} + +export namespace PageManager { + export interface Listener { + onIframeLoaded(): void + onTap(event: TapEvent): void + onLinkActivated(href: string): void + } +} diff --git a/readium/navigators/web/scripts/src/fixed/single-area-manager.ts b/readium/navigators/web/scripts/src/fixed/single-area-manager.ts new file mode 100644 index 0000000000..3b41995bd6 --- /dev/null +++ b/readium/navigators/web/scripts/src/fixed/single-area-manager.ts @@ -0,0 +1,139 @@ +import { Insets, Size } from "../common/types" +import { computeScale, Fit } from "../util/fit" +import { PageManager } from "./page-manager" +import { ViewportStringBuilder } from "../util/viewport" +import { AreaManager } from "./area-manager" +import { GesturesDetector } from "../common/gestures" +import { TapEvent } from "../common/events" + +export class SingleAreaManager { + private readonly metaViewport: HTMLMetaElement + + private readonly page: PageManager + + private fit: Fit = Fit.Contain + + private insets: Insets = { top: 0, right: 0, bottom: 0, left: 0 } + + private viewport?: Size + + private scale: number = 1 + + constructor( + window: Window, + iframe: HTMLIFrameElement, + metaViewport: HTMLMetaElement, + listener: AreaManager.Listener + ) { + window.addEventListener("message", (event) => { + if (event.source === iframe.contentWindow && event.ports[0]) { + this.page.setMessagePort(event.ports[0]) + } + }) + + const wrapperGesturesListener = { + onTap: (event: MouseEvent) => { + const tapEvent = { + x: + (event.clientX - visualViewport!.offsetLeft) * + visualViewport!.scale, + y: + (event.clientY - visualViewport!.offsetTop) * visualViewport!.scale, + } + listener.onTap(tapEvent) + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onLinkActivated: (_: string) => { + throw Error("No interactive element in the root document.") + }, + } + new GesturesDetector(window, wrapperGesturesListener) + + this.metaViewport = metaViewport + const pageListener = { + onIframeLoaded: () => { + this.onIframeLoaded() + }, + onTap: (event: TapEvent) => { + const boundingRect = iframe.getBoundingClientRect() + const tapEvent = { + x: + (event.x + boundingRect.left - visualViewport!.offsetLeft) * + visualViewport!.scale, + y: + (event.y + boundingRect.top - visualViewport!.offsetTop) * + visualViewport!.scale, + } + listener.onTap(tapEvent) + }, + onLinkActivated: (href: string) => { + listener.onLinkActivated(href) + }, + } + this.page = new PageManager(window, iframe, pageListener) + } + + setViewport(viewport: Size, insets: Insets) { + if (this.viewport == viewport && this.insets == insets) { + return + } + + this.viewport = viewport + this.insets = insets + this.layout() + } + + setFit(fit: Fit) { + if (this.fit == fit) { + return + } + + this.fit = fit + this.layout() + } + + loadResource(url: string) { + this.page.hide() + this.page.loadPage(url) + } + + private onIframeLoaded() { + if (!this.page.size) { + // FIXME: raise error + } else { + this.layout() + } + } + + private layout() { + if (!this.page.size || !this.viewport) { + return + } + + const margins = { + top: this.insets.top, + right: this.insets.right, + bottom: this.insets.bottom, + left: this.insets.left, + } + this.page.setMargins(margins) + + const safeDrawingSize = { + width: this.viewport.width - this.insets.left - this.insets.right, + height: this.viewport.height - this.insets.top - this.insets.bottom, + } + + const scale = computeScale(this.fit, this.page.size, safeDrawingSize) + + this.metaViewport.content = new ViewportStringBuilder() + .setInitialScale(scale) + .setMinimumScale(scale) + .setWidth(this.page.size.width) + .setHeight(this.page.size.height) + .build() + + this.scale = scale + + this.page.show() + } +} diff --git a/readium/navigators/web/scripts/src/index-fixed-double.ts b/readium/navigators/web/scripts/src/index-fixed-double.ts new file mode 100644 index 0000000000..e8f9e61b6f --- /dev/null +++ b/readium/navigators/web/scripts/src/index-fixed-double.ts @@ -0,0 +1,39 @@ +// +// Copyright 2024 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. +// + +/** + * Script loaded by the single area HTML wrapper for fixed layout resources. + */ + +import { GesturesBridge } from "./bridge/fixed-gestures-bridge" +import { FixedDoubleBridge } from "./bridge/fixed-double-bridge" +import { InitializationBridge } from "./bridge/fixed-initialization-bridge" + +declare global { + interface Window { + initialization: InitializationBridge + doubleArea: FixedDoubleBridge + gestures: GesturesBridge + } +} + +const leftIframe = document.getElementById("page-left") as HTMLIFrameElement + +const rightIframe = document.getElementById("page-right") as HTMLIFrameElement + +const metaViewport = document.querySelector( + "meta[name=viewport]" +) as HTMLMetaElement + +Window.prototype.doubleArea = new FixedDoubleBridge( + window, + leftIframe, + rightIframe, + metaViewport, + window.gestures +) + +window.initialization.onScriptsLoaded() diff --git a/readium/navigators/web/scripts/src/index-fixed-injectable.ts b/readium/navigators/web/scripts/src/index-fixed-injectable.ts new file mode 100644 index 0000000000..35ac30a5b4 --- /dev/null +++ b/readium/navigators/web/scripts/src/index-fixed-injectable.ts @@ -0,0 +1,50 @@ +// +// Copyright 2024 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. +// + +/** + * Script loaded by fixed layout resources. + */ + +import { GesturesDetector, GesturesListener } from "./common/gestures" +import { Size } from "./common/types" +import { IframeMessageSender } from "./fixed/iframe-message" +import { parseViewportString } from "./util/viewport" + +const messageChannel = new MessageChannel() +window.parent.postMessage("Init", "*", [messageChannel.port2]) +const messageSender = new IframeMessageSender(messageChannel.port1) + +const viewportSize = parseContentSize(window.document) +messageSender.send({ kind: "contentSize", size: viewportSize }) + +class MessagingGesturesListener implements GesturesListener { + readonly messageSender: IframeMessageSender + + constructor(messageSender: IframeMessageSender) { + this.messageSender = messageSender + } + + onTap(event: MouseEvent): void { + this.messageSender.send({ kind: "tap", x: event.clientX, y: event.clientY }) + } + + onLinkActivated(href: string): void { + this.messageSender.send({ kind: "linkActivated", href: href }) + } +} + +const messagingListener = new MessagingGesturesListener(messageSender) +new GesturesDetector(window, messagingListener) + +function parseContentSize(document: Document): Size | undefined { + const viewport = document.querySelector("meta[name=viewport]") + + if (!viewport || !(viewport instanceof HTMLMetaElement)) { + return undefined + } + + return parseViewportString(viewport.content) +} diff --git a/readium/navigators/web/scripts/src/index-fixed-single.ts b/readium/navigators/web/scripts/src/index-fixed-single.ts new file mode 100644 index 0000000000..4901130417 --- /dev/null +++ b/readium/navigators/web/scripts/src/index-fixed-single.ts @@ -0,0 +1,36 @@ +// +// Copyright 2024 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. +// + +/** + * Script loaded by the single area HTML wrapper for fixed layout resources. + */ + +import { GesturesBridge } from "./bridge/fixed-gestures-bridge" +import { InitializationBridge } from "./bridge/fixed-initialization-bridge" +import { FixedSingleBridge } from "./bridge/fixed-single-bridge" + +declare global { + interface Window { + initialization: InitializationBridge + singleArea: FixedSingleBridge + gestures: GesturesBridge + } +} + +const iframe = document.getElementById("page") as HTMLIFrameElement + +const metaViewport = document.querySelector( + "meta[name=viewport]" +) as HTMLMetaElement + +window.singleArea = new FixedSingleBridge( + window, + iframe, + metaViewport, + window.gestures +) + +window.initialization.onScriptsLoaded() diff --git a/readium/navigators/web/scripts/src/util/fit.ts b/readium/navigators/web/scripts/src/util/fit.ts new file mode 100644 index 0000000000..d33258b5cb --- /dev/null +++ b/readium/navigators/web/scripts/src/util/fit.ts @@ -0,0 +1,32 @@ +import { Size } from "../common/types" + +export const enum Fit { + Contain = "contain", + Width = "width", + Height = "height", +} + +export function computeScale(fit: Fit, content: Size, container: Size): number { + switch (fit) { + case Fit.Contain: + return fitContain(content, container) + case Fit.Width: + return fitWidth(content, container) + case Fit.Height: + return fitHeight(content, container) + } +} + +function fitContain(content: Size, container: Size): number { + const widthRatio = container.width / content.width + const heightRatio = container.height / content.height + return Math.min(widthRatio, heightRatio) +} + +function fitWidth(content: Size, container: Size): number { + return container.width / content.width +} + +function fitHeight(content: Size, container: Size): number { + return container.height / content.height +} diff --git a/readium/navigators/web/scripts/src/util/viewport.ts b/readium/navigators/web/scripts/src/util/viewport.ts new file mode 100644 index 0000000000..958fdb5445 --- /dev/null +++ b/readium/navigators/web/scripts/src/util/viewport.ts @@ -0,0 +1,71 @@ +import { Size } from "../common/types" + +export class ViewportStringBuilder { + private initialScale?: number + + private minimumScale?: number + + private width?: number + + private height?: number + + setInitialScale(scale: number): ViewportStringBuilder { + this.initialScale = scale + return this + } + + setMinimumScale(scale: number): ViewportStringBuilder { + this.minimumScale = scale + return this + } + + setWidth(width: number): ViewportStringBuilder { + this.width = width + return this + } + + setHeight(height: number): ViewportStringBuilder { + this.height = height + return this + } + + build(): string { + const components: string[] = [] + + if (this.initialScale) { + components.push("initial-scale=" + this.initialScale) + } + + if (this.minimumScale) { + components.push("minimum-scale=" + this.minimumScale) + } + + if (this.width) { + components.push("width=" + this.width) + } + + if (this.height) { + components.push("height=" + this.height) + } + + return components.join(", ") + } +} + +export function parseViewportString(viewportString: string): Size | undefined { + const regex = /(\w+) *= *([^\s,]+)/g + const properties = new Map() + let match + while ((match = regex.exec(viewportString))) { + if (match != null) { + properties.set(match[1], match[2]) + } + } + const width = parseFloat(properties.get("width")) + const height = parseFloat(properties.get("height")) + if (width && height) { + return { width, height } + } else { + return undefined + } +} diff --git a/readium/navigators/web/scripts/tsconfig.json b/readium/navigators/web/scripts/tsconfig.json new file mode 100644 index 0000000000..e7b0c5cf37 --- /dev/null +++ b/readium/navigators/web/scripts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "noImplicitAny": true, + "module": "es6", + "target": "es6", + "jsx": "react", + "allowJs": true, + "moduleResolution": "node", + "strict": true + } +} diff --git a/readium/navigators/web/scripts/webpack.config.js b/readium/navigators/web/scripts/webpack.config.js new file mode 100644 index 0000000000..a811aba7f1 --- /dev/null +++ b/readium/navigators/web/scripts/webpack.config.js @@ -0,0 +1,40 @@ +const path = require("path") +const CopyPlugin = require("copy-webpack-plugin") + +module.exports = { + mode: "production", + devtool: "source-map", + entry: { + "fixed-single-script": "./src/index-fixed-single.ts", + "fixed-double-script": "./src/index-fixed-double.ts", + "fixed-injectable-script": "./src/index-fixed-injectable.ts", + }, + resolve: { + // Add '.ts' and '.tsx' as resolvable extensions. + extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"], + }, + module: { + rules: [ + // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'. + { + test: /\.tsx?$/, + loader: "ts-loader", + }, + { + test: /\.m?js$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: ["@babel/preset-env"], + }, + }, + }, + ], + }, + plugins: [ + new CopyPlugin({ + patterns: [{ from: "public" }], + }), + ], +} diff --git a/readium/navigators/web/src/main/AndroidManifest.xml b/readium/navigators/web/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8072ee00db --- /dev/null +++ b/readium/navigators/web/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-double-index.html b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-double-index.html new file mode 100644 index 0000000000..0a15204dda --- /dev/null +++ b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-double-index.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + +
+
+ +
+
+
+ + +
+ + + + diff --git a/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-double-script.js b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-double-script.js new file mode 100644 index 0000000000..6b1ac6b812 --- /dev/null +++ b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-double-script.js @@ -0,0 +1,2 @@ +!function(){"use strict";class t{constructor(t,e,i){if(this.margins={top:0,right:0,bottom:0,left:0},!e.contentWindow)throw Error("Iframe argument must have been attached to DOM.");this.listener=i,this.iframe=e}setMessagePort(t){t.onmessage=t=>{this.onMessageFromIframe(t)}}show(){this.iframe.style.display="unset"}hide(){this.iframe.style.display="none"}setMargins(t){this.margins!=t&&(this.iframe.style.marginTop=this.margins.top+"px",this.iframe.style.marginLeft=this.margins.left+"px",this.iframe.style.marginBottom=this.margins.bottom+"px",this.iframe.style.marginRight=this.margins.right+"px")}loadPage(t){this.iframe.src=t}setPlaceholder(t){this.iframe.style.visibility="hidden",this.iframe.style.width=t.width+"px",this.iframe.style.height=t.height+"px",this.size=t}onMessageFromIframe(t){const e=t.data;switch(e.kind){case"contentSize":return this.onContentSizeAvailable(e.size);case"tap":return this.listener.onTap({x:e.x,y:e.y});case"linkActivated":return this.listener.onLinkActivated(e.href)}}onContentSizeAvailable(t){t&&(this.iframe.style.width=t.width+"px",this.iframe.style.height=t.height+"px",this.size=t,this.listener.onIframeLoaded())}}class e{setInitialScale(t){return this.initialScale=t,this}setMinimumScale(t){return this.minimumScale=t,this}setWidth(t){return this.width=t,this}setHeight(t){return this.height=t,this}build(){const t=[];return this.initialScale&&t.push("initial-scale="+this.initialScale),this.minimumScale&&t.push("minimum-scale="+this.minimumScale),this.width&&t.push("width="+this.width),this.height&&t.push("height="+this.height),t.join(", ")}}class i{constructor(t,e){this.window=t,this.listener=e,document.addEventListener("click",(t=>{this.onClick(t)}),!1)}onClick(t){if(t.defaultPrevented)return;const e=this.window.getSelection();if(e&&"Range"==e.type)return;let i;i=t.target instanceof HTMLElement?this.nearestInteractiveElement(t.target):null,i?i instanceof HTMLAnchorElement&&this.listener.onLinkActivated(i.href):this.listener.onTap(t),t.stopPropagation(),t.preventDefault()}nearestInteractiveElement(t){return null==t?null:-1!=["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(t.nodeName.toLowerCase())||t.hasAttribute("contenteditable")&&"false"!=t.getAttribute("contenteditable").toLowerCase()?t:t.parentElement?this.nearestInteractiveElement(t.parentElement):null}}class s{constructor(e,s,n,a,h){this.fit="contain",this.insets={top:0,right:0,bottom:0,left:0},e.addEventListener("message",(t=>{t.ports[0]&&(t.source===s.contentWindow?this.leftPage.setMessagePort(t.ports[0]):t.source==n.contentWindow&&this.rightPage.setMessagePort(t.ports[0]))})),new i(e,{onTap:t=>{const e={x:(t.clientX-visualViewport.offsetLeft)*visualViewport.scale,y:(t.clientY-visualViewport.offsetTop)*visualViewport.scale};h.onTap(e)},onLinkActivated:t=>{throw Error("No interactive element in the root document.")}});const o={onIframeLoaded:()=>{this.layout()},onTap:t=>{const e=s.getBoundingClientRect(),i={x:(t.x+e.left-visualViewport.offsetLeft)*visualViewport.scale,y:(t.y+e.top-visualViewport.offsetTop)*visualViewport.scale};h.onTap(i)},onLinkActivated:t=>{h.onLinkActivated(t)}},r={onIframeLoaded:()=>{this.layout()},onTap:t=>{const e=n.getBoundingClientRect(),i={x:(t.x+e.left-visualViewport.offsetLeft)*visualViewport.scale,y:(t.y+e.top-visualViewport.offsetTop)*visualViewport.scale};h.onTap(i)},onLinkActivated:t=>{h.onLinkActivated(t)}};this.leftPage=new t(e,s,o),this.rightPage=new t(e,n,r),this.metaViewport=a}loadSpread(t){this.leftPage.hide(),this.rightPage.hide(),this.spread=t,t.left&&this.leftPage.loadPage(t.left),t.right&&this.rightPage.loadPage(t.right)}setViewport(t,e){this.viewport==t&&this.insets==e||(this.viewport=t,this.insets=e,this.layout())}setFit(t){this.fit!=t&&(this.fit=t,this.layout())}layout(){if(!this.viewport||!this.leftPage.size&&this.spread.left||!this.rightPage.size&&this.spread.right)return;const t={top:this.insets.top,right:0,bottom:this.insets.bottom,left:this.insets.left};this.leftPage.setMargins(t);const i={top:this.insets.top,right:this.insets.right,bottom:this.insets.bottom,left:0};this.rightPage.setMargins(i),this.spread.right?this.spread.left||this.leftPage.setPlaceholder(this.rightPage.size):this.rightPage.setPlaceholder(this.leftPage.size);const s=this.leftPage.size.width+this.rightPage.size.width,n=Math.max(this.leftPage.size.height,this.rightPage.size.height),a={width:s,height:n},h={width:this.viewport.width-this.insets.left-this.insets.right,height:this.viewport.height-this.insets.top-this.insets.bottom},o=function(t,e,i){switch(t){case"contain":return function(t,e){const i=e.width/t.width,s=e.height/t.height;return Math.min(i,s)}(e,i);case"width":return function(t,e){return e.width/t.width}(e,i);case"height":return function(t,e){return e.height/t.height}(e,i)}}(this.fit,a,h);this.metaViewport.content=(new e).setInitialScale(o).setMinimumScale(o).setWidth(s).setHeight(n).build(),this.leftPage.show(),this.rightPage.show()}}class n{constructor(t){this.nativeApi=t}onTap(t){this.nativeApi.onTap(JSON.stringify(t))}onLinkActivated(t){this.nativeApi.onLinkActivated(t)}}const a=document.getElementById("page-left"),h=document.getElementById("page-right"),o=document.querySelector("meta[name=viewport]");Window.prototype.doubleArea=new class{constructor(t,e,i,a,h){const o=new n(h);this.manager=new s(t,e,i,a,o)}loadSpread(t){this.manager.loadSpread(t)}setViewport(t,e,i,s,n,a){const h={width:t,height:e},o={top:i,left:a,bottom:n,right:s};this.manager.setViewport(h,o)}setFit(t){if("contain"!=t&&"width"!=t&&"height"!=t)throw Error(`Invalid fit value: ${t}`);this.manager.setFit(t)}}(window,a,h,o,window.gestures),window.initialization.onScriptsLoaded()}(); +//# sourceMappingURL=fixed-double-script.js.map \ No newline at end of file diff --git a/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-double-script.js.map b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-double-script.js.map new file mode 100644 index 0000000000..dc50214d91 --- /dev/null +++ b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-double-script.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fixed-double-script.js","mappings":"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","sources":["webpack://readium-js/./src/fixed/page-manager.ts","webpack://readium-js/./src/util/viewport.ts","webpack://readium-js/./src/common/gestures.ts","webpack://readium-js/./src/fixed/double-area-manager.ts","webpack://readium-js/./src/util/fit.ts","webpack://readium-js/./src/bridge/fixed-gestures-bridge.ts","webpack://readium-js/./src/index-fixed-double.ts","webpack://readium-js/./src/bridge/fixed-double-bridge.ts"],"sourcesContent":["/** Manages a fixed layout resource embedded in an iframe. */\nexport class PageManager {\n constructor(window, iframe, listener) {\n this.margins = { top: 0, right: 0, bottom: 0, left: 0 };\n if (!iframe.contentWindow) {\n throw Error(\"Iframe argument must have been attached to DOM.\");\n }\n this.listener = listener;\n this.iframe = iframe;\n }\n setMessagePort(messagePort) {\n messagePort.onmessage = (message) => {\n this.onMessageFromIframe(message);\n };\n }\n show() {\n this.iframe.style.display = \"unset\";\n }\n hide() {\n this.iframe.style.display = \"none\";\n }\n /** Sets page margins. */\n setMargins(margins) {\n if (this.margins == margins) {\n return;\n }\n this.iframe.style.marginTop = this.margins.top + \"px\";\n this.iframe.style.marginLeft = this.margins.left + \"px\";\n this.iframe.style.marginBottom = this.margins.bottom + \"px\";\n this.iframe.style.marginRight = this.margins.right + \"px\";\n }\n /** Loads page content. */\n loadPage(url) {\n this.iframe.src = url;\n }\n /** Sets the size of this page without content. */\n setPlaceholder(size) {\n this.iframe.style.visibility = \"hidden\";\n this.iframe.style.width = size.width + \"px\";\n this.iframe.style.height = size.height + \"px\";\n this.size = size;\n }\n onMessageFromIframe(event) {\n const message = event.data;\n switch (message.kind) {\n case \"contentSize\":\n return this.onContentSizeAvailable(message.size);\n case \"tap\":\n return this.listener.onTap({ x: message.x, y: message.y });\n case \"linkActivated\":\n return this.listener.onLinkActivated(message.href);\n }\n }\n onContentSizeAvailable(size) {\n if (!size) {\n //FIXME: handle edge case\n return;\n }\n this.iframe.style.width = size.width + \"px\";\n this.iframe.style.height = size.height + \"px\";\n this.size = size;\n this.listener.onIframeLoaded();\n }\n}\n","export class ViewportStringBuilder {\n setInitialScale(scale) {\n this.initialScale = scale;\n return this;\n }\n setMinimumScale(scale) {\n this.minimumScale = scale;\n return this;\n }\n setWidth(width) {\n this.width = width;\n return this;\n }\n setHeight(height) {\n this.height = height;\n return this;\n }\n build() {\n const components = [];\n if (this.initialScale) {\n components.push(\"initial-scale=\" + this.initialScale);\n }\n if (this.minimumScale) {\n components.push(\"minimum-scale=\" + this.minimumScale);\n }\n if (this.width) {\n components.push(\"width=\" + this.width);\n }\n if (this.height) {\n components.push(\"height=\" + this.height);\n }\n return components.join(\", \");\n }\n}\nexport function parseViewportString(viewportString) {\n const regex = /(\\w+) *= *([^\\s,]+)/g;\n const properties = new Map();\n let match;\n while ((match = regex.exec(viewportString))) {\n if (match != null) {\n properties.set(match[1], match[2]);\n }\n }\n const width = parseFloat(properties.get(\"width\"));\n const height = parseFloat(properties.get(\"height\"));\n if (width && height) {\n return { width, height };\n }\n else {\n return undefined;\n }\n}\n","export class GesturesDetector {\n constructor(window, listener) {\n this.window = window;\n this.listener = listener;\n document.addEventListener(\"click\", (event) => {\n this.onClick(event);\n }, false);\n }\n onClick(event) {\n if (event.defaultPrevented) {\n return;\n }\n const selection = this.window.getSelection();\n if (selection && selection.type == \"Range\") {\n // There's an on-going selection, the tap will dismiss it so we don't forward it.\n // selection.type might be None (collapsed) or Caret with a collapsed range\n // when there is not true selection.\n return;\n }\n let nearestElement;\n if (event.target instanceof HTMLElement) {\n nearestElement = this.nearestInteractiveElement(event.target);\n }\n else {\n nearestElement = null;\n }\n if (nearestElement) {\n if (nearestElement instanceof HTMLAnchorElement) {\n this.listener.onLinkActivated(nearestElement.href);\n }\n }\n else {\n this.listener.onTap(event);\n }\n event.stopPropagation();\n event.preventDefault();\n }\n // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling\n nearestInteractiveElement(element) {\n if (element == null) {\n return null;\n }\n const interactiveTags = [\n \"a\",\n \"audio\",\n \"button\",\n \"canvas\",\n \"details\",\n \"input\",\n \"label\",\n \"option\",\n \"select\",\n \"submit\",\n \"textarea\",\n \"video\",\n ];\n if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) {\n return element;\n }\n // Checks whether the element is editable by the user.\n if (element.hasAttribute(\"contenteditable\") &&\n element.getAttribute(\"contenteditable\").toLowerCase() != \"false\") {\n return element;\n }\n // Checks parents recursively because the touch might be for example on an inside a
.\n if (element.parentElement) {\n return this.nearestInteractiveElement(element.parentElement);\n }\n return null;\n }\n}\n","import { computeScale } from \"../util/fit\";\nimport { PageManager } from \"./page-manager\";\nimport { ViewportStringBuilder } from \"../util/viewport\";\nimport { GesturesDetector } from \"../common/gestures\";\nexport class DoubleAreaManager {\n constructor(window, leftIframe, rightIframe, metaViewport, listener) {\n this.fit = \"contain\" /* Fit.Contain */;\n this.insets = { top: 0, right: 0, bottom: 0, left: 0 };\n window.addEventListener(\"message\", (event) => {\n if (!event.ports[0]) {\n return;\n }\n if (event.source === leftIframe.contentWindow) {\n this.leftPage.setMessagePort(event.ports[0]);\n }\n else if (event.source == rightIframe.contentWindow) {\n this.rightPage.setMessagePort(event.ports[0]);\n }\n });\n const wrapperGesturesListener = {\n onTap: (event) => {\n const tapEvent = {\n x: (event.clientX - visualViewport.offsetLeft) *\n visualViewport.scale,\n y: (event.clientY - visualViewport.offsetTop) * visualViewport.scale,\n };\n listener.onTap(tapEvent);\n },\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLinkActivated: (_) => {\n throw Error(\"No interactive element in the root document.\");\n },\n };\n new GesturesDetector(window, wrapperGesturesListener);\n const leftPageListener = {\n onIframeLoaded: () => {\n this.layout();\n },\n onTap: (event) => {\n const boundingRect = leftIframe.getBoundingClientRect();\n const tapEvent = {\n x: (event.x + boundingRect.left - visualViewport.offsetLeft) *\n visualViewport.scale,\n y: (event.y + boundingRect.top - visualViewport.offsetTop) *\n visualViewport.scale,\n };\n listener.onTap(tapEvent);\n },\n onLinkActivated: (href) => {\n listener.onLinkActivated(href);\n },\n };\n const rightPageListener = {\n onIframeLoaded: () => {\n this.layout();\n },\n onTap: (event) => {\n const boundingRect = rightIframe.getBoundingClientRect();\n const tapEvent = {\n x: (event.x + boundingRect.left - visualViewport.offsetLeft) *\n visualViewport.scale,\n y: (event.y + boundingRect.top - visualViewport.offsetTop) *\n visualViewport.scale,\n };\n listener.onTap(tapEvent);\n },\n onLinkActivated: (href) => {\n listener.onLinkActivated(href);\n },\n };\n this.leftPage = new PageManager(window, leftIframe, leftPageListener);\n this.rightPage = new PageManager(window, rightIframe, rightPageListener);\n this.metaViewport = metaViewport;\n }\n loadSpread(spread) {\n this.leftPage.hide();\n this.rightPage.hide();\n this.spread = spread;\n if (spread.left) {\n this.leftPage.loadPage(spread.left);\n }\n if (spread.right) {\n this.rightPage.loadPage(spread.right);\n }\n }\n setViewport(size, insets) {\n if (this.viewport == size && this.insets == insets) {\n return;\n }\n this.viewport = size;\n this.insets = insets;\n this.layout();\n }\n setFit(fit) {\n if (this.fit == fit) {\n return;\n }\n this.fit = fit;\n this.layout();\n }\n layout() {\n if (!this.viewport ||\n (!this.leftPage.size && this.spread.left) ||\n (!this.rightPage.size && this.spread.right)) {\n return;\n }\n const leftMargins = {\n top: this.insets.top,\n right: 0,\n bottom: this.insets.bottom,\n left: this.insets.left,\n };\n this.leftPage.setMargins(leftMargins);\n const rightMargins = {\n top: this.insets.top,\n right: this.insets.right,\n bottom: this.insets.bottom,\n left: 0,\n };\n this.rightPage.setMargins(rightMargins);\n if (!this.spread.right) {\n this.rightPage.setPlaceholder(this.leftPage.size);\n }\n else if (!this.spread.left) {\n this.leftPage.setPlaceholder(this.rightPage.size);\n }\n const contentWidth = this.leftPage.size.width + this.rightPage.size.width;\n const contentHeight = Math.max(this.leftPage.size.height, this.rightPage.size.height);\n const contentSize = { width: contentWidth, height: contentHeight };\n const safeDrawingSize = {\n width: this.viewport.width - this.insets.left - this.insets.right,\n height: this.viewport.height - this.insets.top - this.insets.bottom,\n };\n const scale = computeScale(this.fit, contentSize, safeDrawingSize);\n this.metaViewport.content = new ViewportStringBuilder()\n .setInitialScale(scale)\n .setMinimumScale(scale)\n .setWidth(contentWidth)\n .setHeight(contentHeight)\n .build();\n this.leftPage.show();\n this.rightPage.show();\n }\n}\n","export function computeScale(fit, content, container) {\n switch (fit) {\n case \"contain\" /* Fit.Contain */:\n return fitContain(content, container);\n case \"width\" /* Fit.Width */:\n return fitWidth(content, container);\n case \"height\" /* Fit.Height */:\n return fitHeight(content, container);\n }\n}\nfunction fitContain(content, container) {\n const widthRatio = container.width / content.width;\n const heightRatio = container.height / content.height;\n return Math.min(widthRatio, heightRatio);\n}\nfunction fitWidth(content, container) {\n return container.width / content.width;\n}\nfunction fitHeight(content, container) {\n return container.height / content.height;\n}\n","export class BridgeGesturesAdapter {\n constructor(gesturesApi) {\n this.nativeApi = gesturesApi;\n }\n onTap(event) {\n this.nativeApi.onTap(JSON.stringify(event));\n }\n onLinkActivated(href) {\n this.nativeApi.onLinkActivated(href);\n }\n}\n","//\n// Copyright 2024 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\nimport { FixedDoubleBridge } from \"./bridge/fixed-double-bridge\";\nconst leftIframe = document.getElementById(\"page-left\");\nconst rightIframe = document.getElementById(\"page-right\");\nconst metaViewport = document.querySelector(\"meta[name=viewport]\");\nWindow.prototype.doubleArea = new FixedDoubleBridge(window, leftIframe, rightIframe, metaViewport, window.gestures);\nwindow.initialization.onScriptsLoaded();\n","import { DoubleAreaManager } from \"../fixed/double-area-manager\";\nimport { BridgeGesturesAdapter } from \"./fixed-gestures-bridge\";\nexport class FixedDoubleBridge {\n constructor(window, leftIframe, rightIframe, metaViewport, gestures) {\n const listener = new BridgeGesturesAdapter(gestures);\n this.manager = new DoubleAreaManager(window, leftIframe, rightIframe, metaViewport, listener);\n }\n loadSpread(spread) {\n this.manager.loadSpread(spread);\n }\n setViewport(viewporttWidth, viewportHeight, insetTop, insetRight, insetBottom, insetLeft) {\n const viewport = { width: viewporttWidth, height: viewportHeight };\n const insets = {\n top: insetTop,\n left: insetLeft,\n bottom: insetBottom,\n right: insetRight,\n };\n this.manager.setViewport(viewport, insets);\n }\n setFit(fit) {\n if (fit != \"contain\" && fit != \"width\" && fit != \"height\") {\n throw Error(`Invalid fit value: ${fit}`);\n }\n this.manager.setFit(fit);\n }\n}\n"],"names":["PageManager","constructor","window","iframe","listener","this","margins","top","right","bottom","left","contentWindow","Error","setMessagePort","messagePort","onmessage","message","onMessageFromIframe","show","style","display","hide","setMargins","marginTop","marginLeft","marginBottom","marginRight","loadPage","url","src","setPlaceholder","size","visibility","width","height","event","data","kind","onContentSizeAvailable","onTap","x","y","onLinkActivated","href","onIframeLoaded","ViewportStringBuilder","setInitialScale","scale","initialScale","setMinimumScale","minimumScale","setWidth","setHeight","build","components","push","join","GesturesDetector","document","addEventListener","onClick","defaultPrevented","selection","getSelection","type","nearestElement","target","HTMLElement","nearestInteractiveElement","HTMLAnchorElement","stopPropagation","preventDefault","element","indexOf","nodeName","toLowerCase","hasAttribute","getAttribute","parentElement","DoubleAreaManager","leftIframe","rightIframe","metaViewport","fit","insets","ports","source","leftPage","rightPage","tapEvent","clientX","visualViewport","offsetLeft","clientY","offsetTop","_","leftPageListener","layout","boundingRect","getBoundingClientRect","rightPageListener","loadSpread","spread","setViewport","viewport","setFit","leftMargins","rightMargins","contentWidth","contentHeight","Math","max","contentSize","safeDrawingSize","content","container","widthRatio","heightRatio","min","fitContain","fitWidth","fitHeight","computeScale","BridgeGesturesAdapter","gesturesApi","nativeApi","JSON","stringify","getElementById","querySelector","Window","prototype","doubleArea","gestures","manager","viewporttWidth","viewportHeight","insetTop","insetRight","insetBottom","insetLeft","initialization","onScriptsLoaded"],"sourceRoot":""} \ No newline at end of file diff --git a/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-injectable-script.js b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-injectable-script.js new file mode 100644 index 0000000000..61b11ef4c1 --- /dev/null +++ b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-injectable-script.js @@ -0,0 +1,2 @@ +!function(){"use strict";const e=new MessageChannel;window.parent.postMessage("Init","*",[e.port2]);const t=new class{constructor(e){this.messagePort=e}send(e){this.messagePort.postMessage(e)}}(e.port1),n=function(e){const t=window.document.querySelector("meta[name=viewport]");if(t&&t instanceof HTMLMetaElement)return function(e){const t=/(\w+) *= *([^\s,]+)/g,n=new Map;let s;for(;s=t.exec(e);)null!=s&&n.set(s[1],s[2]);const i=parseFloat(n.get("width")),o=parseFloat(n.get("height"));return i&&o?{width:i,height:o}:void 0}(t.content)}();t.send({kind:"contentSize",size:n});const s=new class{constructor(e){this.messageSender=e}onTap(e){this.messageSender.send({kind:"tap",x:e.clientX,y:e.clientY})}onLinkActivated(e){this.messageSender.send({kind:"linkActivated",href:e})}}(t);new class{constructor(e,t){this.window=e,this.listener=t,document.addEventListener("click",(e=>{this.onClick(e)}),!1)}onClick(e){if(e.defaultPrevented)return;const t=this.window.getSelection();if(t&&"Range"==t.type)return;let n;n=e.target instanceof HTMLElement?this.nearestInteractiveElement(e.target):null,n?n instanceof HTMLAnchorElement&&this.listener.onLinkActivated(n.href):this.listener.onTap(e),e.stopPropagation(),e.preventDefault()}nearestInteractiveElement(e){return null==e?null:-1!=["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(e.nodeName.toLowerCase())||e.hasAttribute("contenteditable")&&"false"!=e.getAttribute("contenteditable").toLowerCase()?e:e.parentElement?this.nearestInteractiveElement(e.parentElement):null}}(window,s)}(); +//# sourceMappingURL=fixed-injectable-script.js.map \ No newline at end of file diff --git a/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-injectable-script.js.map b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-injectable-script.js.map new file mode 100644 index 0000000000..990608b525 --- /dev/null +++ b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-injectable-script.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fixed-injectable-script.js","mappings":"yBAWA,MAAMA,EAAiB,IAAIC,eAC3BC,OAAOC,OAAOC,YAAY,OAAQ,IAAK,CAACJ,EAAeK,QACvD,MAAMC,EAAgB,ICbf,MACH,WAAAC,CAAYC,GACRC,KAAKD,YAAcA,CACvB,CACA,IAAAE,CAAKC,GACDF,KAAKD,YAAYJ,YAAYO,EACjC,GDO0CX,EAAeY,OACvDC,EAeN,SAA0BC,GACtB,MAAMC,EAhB4Bb,OAAOY,SAgBfE,cAAc,uBACxC,GAAKD,GAAcA,aAAoBE,gBAGvC,OEAG,SAA6BC,GAChC,MAAMC,EAAQ,uBACRC,EAAa,IAAIC,IACvB,IAAIC,EACJ,KAAQA,EAAQH,EAAMI,KAAKL,IACV,MAATI,GACAF,EAAWI,IAAIF,EAAM,GAAIA,EAAM,IAGvC,MAAMG,EAAQC,WAAWN,EAAWO,IAAI,UAClCC,EAASF,WAAWN,EAAWO,IAAI,WACzC,OAAIF,GAASG,EACF,CAAEH,QAAOG,eAGhB,CAER,CFjBWC,CAAoBd,EAASe,QACxC,CArBqBC,GACrBzB,EAAcI,KAAK,CAAEsB,KAAM,cAAeC,KAAMpB,IAYhD,MAAMqB,EAAoB,IAX1B,MACI,WAAA3B,CAAYD,GACRG,KAAKH,cAAgBA,CACzB,CACA,KAAA6B,CAAMC,GACF3B,KAAKH,cAAcI,KAAK,CAAEsB,KAAM,MAAOK,EAAGD,EAAME,QAASC,EAAGH,EAAMI,SACtE,CACA,eAAAC,CAAgBC,GACZjC,KAAKH,cAAcI,KAAK,CAAEsB,KAAM,gBAAiBU,KAAMA,GAC3D,GAEoDpC,GACxD,IG5BO,MACH,WAAAC,CAAYL,EAAQyC,GAChBlC,KAAKP,OAASA,EACdO,KAAKkC,SAAWA,EAChB7B,SAAS8B,iBAAiB,SAAUR,IAChC3B,KAAKoC,QAAQT,EAAM,IACpB,EACP,CACA,OAAAS,CAAQT,GACJ,GAAIA,EAAMU,iBACN,OAEJ,MAAMC,EAAYtC,KAAKP,OAAO8C,eAC9B,GAAID,GAA+B,SAAlBA,EAAUE,KAIvB,OAEJ,IAAIC,EAEAA,EADAd,EAAMe,kBAAkBC,YACP3C,KAAK4C,0BAA0BjB,EAAMe,QAGrC,KAEjBD,EACIA,aAA0BI,mBAC1B7C,KAAKkC,SAASF,gBAAgBS,EAAeR,MAIjDjC,KAAKkC,SAASR,MAAMC,GAExBA,EAAMmB,kBACNnB,EAAMoB,gBACV,CAEA,yBAAAH,CAA0BI,GACtB,OAAe,MAAXA,EACO,MAgBqD,GAdxC,CACpB,IACA,QACA,SACA,SACA,UACA,QACA,QACA,SACA,SACA,SACA,WACA,SAEgBC,QAAQD,EAAQE,SAASC,gBAIzCH,EAAQI,aAAa,oBACoC,SAAzDJ,EAAQK,aAAa,mBAAmBF,cAJjCH,EAQPA,EAAQM,cACDtD,KAAK4C,0BAA0BI,EAAQM,eAE3C,IACX,GHzCiB7D,OAAQgC,E","sources":["webpack://readium-js/./src/index-fixed-injectable.ts","webpack://readium-js/./src/fixed/iframe-message.ts","webpack://readium-js/./src/util/viewport.ts","webpack://readium-js/./src/common/gestures.ts"],"sourcesContent":["//\n// Copyright 2024 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n/**\n * Script loaded by fixed layout resources.\n */\nimport { GesturesDetector } from \"./common/gestures\";\nimport { IframeMessageSender } from \"./fixed/iframe-message\";\nimport { parseViewportString } from \"./util/viewport\";\nconst messageChannel = new MessageChannel();\nwindow.parent.postMessage(\"Init\", \"*\", [messageChannel.port2]);\nconst messageSender = new IframeMessageSender(messageChannel.port1);\nconst viewportSize = parseContentSize(window.document);\nmessageSender.send({ kind: \"contentSize\", size: viewportSize });\nclass MessagingGesturesListener {\n constructor(messageSender) {\n this.messageSender = messageSender;\n }\n onTap(event) {\n this.messageSender.send({ kind: \"tap\", x: event.clientX, y: event.clientY });\n }\n onLinkActivated(href) {\n this.messageSender.send({ kind: \"linkActivated\", href: href });\n }\n}\nconst messagingListener = new MessagingGesturesListener(messageSender);\nnew GesturesDetector(window, messagingListener);\nfunction parseContentSize(document) {\n const viewport = document.querySelector(\"meta[name=viewport]\");\n if (!viewport || !(viewport instanceof HTMLMetaElement)) {\n return undefined;\n }\n return parseViewportString(viewport.content);\n}\n","export class IframeMessageSender {\n constructor(messagePort) {\n this.messagePort = messagePort;\n }\n send(message) {\n this.messagePort.postMessage(message);\n }\n}\n","export class ViewportStringBuilder {\n setInitialScale(scale) {\n this.initialScale = scale;\n return this;\n }\n setMinimumScale(scale) {\n this.minimumScale = scale;\n return this;\n }\n setWidth(width) {\n this.width = width;\n return this;\n }\n setHeight(height) {\n this.height = height;\n return this;\n }\n build() {\n const components = [];\n if (this.initialScale) {\n components.push(\"initial-scale=\" + this.initialScale);\n }\n if (this.minimumScale) {\n components.push(\"minimum-scale=\" + this.minimumScale);\n }\n if (this.width) {\n components.push(\"width=\" + this.width);\n }\n if (this.height) {\n components.push(\"height=\" + this.height);\n }\n return components.join(\", \");\n }\n}\nexport function parseViewportString(viewportString) {\n const regex = /(\\w+) *= *([^\\s,]+)/g;\n const properties = new Map();\n let match;\n while ((match = regex.exec(viewportString))) {\n if (match != null) {\n properties.set(match[1], match[2]);\n }\n }\n const width = parseFloat(properties.get(\"width\"));\n const height = parseFloat(properties.get(\"height\"));\n if (width && height) {\n return { width, height };\n }\n else {\n return undefined;\n }\n}\n","export class GesturesDetector {\n constructor(window, listener) {\n this.window = window;\n this.listener = listener;\n document.addEventListener(\"click\", (event) => {\n this.onClick(event);\n }, false);\n }\n onClick(event) {\n if (event.defaultPrevented) {\n return;\n }\n const selection = this.window.getSelection();\n if (selection && selection.type == \"Range\") {\n // There's an on-going selection, the tap will dismiss it so we don't forward it.\n // selection.type might be None (collapsed) or Caret with a collapsed range\n // when there is not true selection.\n return;\n }\n let nearestElement;\n if (event.target instanceof HTMLElement) {\n nearestElement = this.nearestInteractiveElement(event.target);\n }\n else {\n nearestElement = null;\n }\n if (nearestElement) {\n if (nearestElement instanceof HTMLAnchorElement) {\n this.listener.onLinkActivated(nearestElement.href);\n }\n }\n else {\n this.listener.onTap(event);\n }\n event.stopPropagation();\n event.preventDefault();\n }\n // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling\n nearestInteractiveElement(element) {\n if (element == null) {\n return null;\n }\n const interactiveTags = [\n \"a\",\n \"audio\",\n \"button\",\n \"canvas\",\n \"details\",\n \"input\",\n \"label\",\n \"option\",\n \"select\",\n \"submit\",\n \"textarea\",\n \"video\",\n ];\n if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) {\n return element;\n }\n // Checks whether the element is editable by the user.\n if (element.hasAttribute(\"contenteditable\") &&\n element.getAttribute(\"contenteditable\").toLowerCase() != \"false\") {\n return element;\n }\n // Checks parents recursively because the touch might be for example on an inside a .\n if (element.parentElement) {\n return this.nearestInteractiveElement(element.parentElement);\n }\n return null;\n }\n}\n"],"names":["messageChannel","MessageChannel","window","parent","postMessage","port2","messageSender","constructor","messagePort","this","send","message","port1","viewportSize","document","viewport","querySelector","HTMLMetaElement","viewportString","regex","properties","Map","match","exec","set","width","parseFloat","get","height","parseViewportString","content","parseContentSize","kind","size","messagingListener","onTap","event","x","clientX","y","clientY","onLinkActivated","href","listener","addEventListener","onClick","defaultPrevented","selection","getSelection","type","nearestElement","target","HTMLElement","nearestInteractiveElement","HTMLAnchorElement","stopPropagation","preventDefault","element","indexOf","nodeName","toLowerCase","hasAttribute","getAttribute","parentElement"],"sourceRoot":""} \ No newline at end of file diff --git a/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-single-index.html b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-single-index.html new file mode 100644 index 0000000000..4c8982b165 --- /dev/null +++ b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-single-index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + +
+ +
+ + + diff --git a/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-single-script.js b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-single-script.js new file mode 100644 index 0000000000..d5ed6229a4 --- /dev/null +++ b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-single-script.js @@ -0,0 +1,2 @@ +!function(){"use strict";class t{constructor(t,e,i){if(this.margins={top:0,right:0,bottom:0,left:0},!e.contentWindow)throw Error("Iframe argument must have been attached to DOM.");this.listener=i,this.iframe=e}setMessagePort(t){t.onmessage=t=>{this.onMessageFromIframe(t)}}show(){this.iframe.style.display="unset"}hide(){this.iframe.style.display="none"}setMargins(t){this.margins!=t&&(this.iframe.style.marginTop=this.margins.top+"px",this.iframe.style.marginLeft=this.margins.left+"px",this.iframe.style.marginBottom=this.margins.bottom+"px",this.iframe.style.marginRight=this.margins.right+"px")}loadPage(t){this.iframe.src=t}setPlaceholder(t){this.iframe.style.visibility="hidden",this.iframe.style.width=t.width+"px",this.iframe.style.height=t.height+"px",this.size=t}onMessageFromIframe(t){const e=t.data;switch(e.kind){case"contentSize":return this.onContentSizeAvailable(e.size);case"tap":return this.listener.onTap({x:e.x,y:e.y});case"linkActivated":return this.listener.onLinkActivated(e.href)}}onContentSizeAvailable(t){t&&(this.iframe.style.width=t.width+"px",this.iframe.style.height=t.height+"px",this.size=t,this.listener.onIframeLoaded())}}class e{setInitialScale(t){return this.initialScale=t,this}setMinimumScale(t){return this.minimumScale=t,this}setWidth(t){return this.width=t,this}setHeight(t){return this.height=t,this}build(){const t=[];return this.initialScale&&t.push("initial-scale="+this.initialScale),this.minimumScale&&t.push("minimum-scale="+this.minimumScale),this.width&&t.push("width="+this.width),this.height&&t.push("height="+this.height),t.join(", ")}}class i{constructor(t,e){this.window=t,this.listener=e,document.addEventListener("click",(t=>{this.onClick(t)}),!1)}onClick(t){if(t.defaultPrevented)return;const e=this.window.getSelection();if(e&&"Range"==e.type)return;let i;i=t.target instanceof HTMLElement?this.nearestInteractiveElement(t.target):null,i?i instanceof HTMLAnchorElement&&this.listener.onLinkActivated(i.href):this.listener.onTap(t),t.stopPropagation(),t.preventDefault()}nearestInteractiveElement(t){return null==t?null:-1!=["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(t.nodeName.toLowerCase())||t.hasAttribute("contenteditable")&&"false"!=t.getAttribute("contenteditable").toLowerCase()?t:t.parentElement?this.nearestInteractiveElement(t.parentElement):null}}class s{constructor(e,s,n,a){this.fit="contain",this.insets={top:0,right:0,bottom:0,left:0},this.scale=1,e.addEventListener("message",(t=>{t.source===s.contentWindow&&t.ports[0]&&this.page.setMessagePort(t.ports[0])})),new i(e,{onTap:t=>{const e={x:(t.clientX-visualViewport.offsetLeft)*visualViewport.scale,y:(t.clientY-visualViewport.offsetTop)*visualViewport.scale};a.onTap(e)},onLinkActivated:t=>{throw Error("No interactive element in the root document.")}}),this.metaViewport=n;const h={onIframeLoaded:()=>{this.onIframeLoaded()},onTap:t=>{const e=s.getBoundingClientRect(),i={x:(t.x+e.left-visualViewport.offsetLeft)*visualViewport.scale,y:(t.y+e.top-visualViewport.offsetTop)*visualViewport.scale};a.onTap(i)},onLinkActivated:t=>{a.onLinkActivated(t)}};this.page=new t(e,s,h)}setViewport(t,e){this.viewport==t&&this.insets==e||(this.viewport=t,this.insets=e,this.layout())}setFit(t){this.fit!=t&&(this.fit=t,this.layout())}loadResource(t){this.page.hide(),this.page.loadPage(t)}onIframeLoaded(){this.page.size&&this.layout()}layout(){if(!this.page.size||!this.viewport)return;const t={top:this.insets.top,right:this.insets.right,bottom:this.insets.bottom,left:this.insets.left};this.page.setMargins(t);const i={width:this.viewport.width-this.insets.left-this.insets.right,height:this.viewport.height-this.insets.top-this.insets.bottom},s=function(t,e,i){switch(t){case"contain":return function(t,e){const i=e.width/t.width,s=e.height/t.height;return Math.min(i,s)}(e,i);case"width":return function(t,e){return e.width/t.width}(e,i);case"height":return function(t,e){return e.height/t.height}(e,i)}}(this.fit,this.page.size,i);this.metaViewport.content=(new e).setInitialScale(s).setMinimumScale(s).setWidth(this.page.size.width).setHeight(this.page.size.height).build(),this.scale=s,this.page.show()}}class n{constructor(t){this.nativeApi=t}onTap(t){this.nativeApi.onTap(JSON.stringify(t))}onLinkActivated(t){this.nativeApi.onLinkActivated(t)}}const a=document.getElementById("page"),h=document.querySelector("meta[name=viewport]");window.singleArea=new class{constructor(t,e,i,a){const h=new n(a);this.manager=new s(t,e,i,h)}loadResource(t){this.manager.loadResource(t)}setViewport(t,e,i,s,n,a){const h={width:t,height:e},o={top:i,left:a,bottom:n,right:s};this.manager.setViewport(h,o)}setFit(t){if("contain"!=t&&"width"!=t&&"height"!=t)throw Error(`Invalid fit value: ${t}`);this.manager.setFit(t)}}(window,a,h,window.gestures),window.initialization.onScriptsLoaded()}(); +//# sourceMappingURL=fixed-single-script.js.map \ No newline at end of file diff --git a/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-single-script.js.map b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-single-script.js.map new file mode 100644 index 0000000000..a823bae92d --- /dev/null +++ b/readium/navigators/web/src/main/assets/readium/navigators/web/fixed-single-script.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fixed-single-script.js","mappings":"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","sources":["webpack://readium-js/./src/fixed/page-manager.ts","webpack://readium-js/./src/util/viewport.ts","webpack://readium-js/./src/common/gestures.ts","webpack://readium-js/./src/fixed/single-area-manager.ts","webpack://readium-js/./src/util/fit.ts","webpack://readium-js/./src/bridge/fixed-gestures-bridge.ts","webpack://readium-js/./src/index-fixed-single.ts","webpack://readium-js/./src/bridge/fixed-single-bridge.ts"],"sourcesContent":["/** Manages a fixed layout resource embedded in an iframe. */\nexport class PageManager {\n constructor(window, iframe, listener) {\n this.margins = { top: 0, right: 0, bottom: 0, left: 0 };\n if (!iframe.contentWindow) {\n throw Error(\"Iframe argument must have been attached to DOM.\");\n }\n this.listener = listener;\n this.iframe = iframe;\n }\n setMessagePort(messagePort) {\n messagePort.onmessage = (message) => {\n this.onMessageFromIframe(message);\n };\n }\n show() {\n this.iframe.style.display = \"unset\";\n }\n hide() {\n this.iframe.style.display = \"none\";\n }\n /** Sets page margins. */\n setMargins(margins) {\n if (this.margins == margins) {\n return;\n }\n this.iframe.style.marginTop = this.margins.top + \"px\";\n this.iframe.style.marginLeft = this.margins.left + \"px\";\n this.iframe.style.marginBottom = this.margins.bottom + \"px\";\n this.iframe.style.marginRight = this.margins.right + \"px\";\n }\n /** Loads page content. */\n loadPage(url) {\n this.iframe.src = url;\n }\n /** Sets the size of this page without content. */\n setPlaceholder(size) {\n this.iframe.style.visibility = \"hidden\";\n this.iframe.style.width = size.width + \"px\";\n this.iframe.style.height = size.height + \"px\";\n this.size = size;\n }\n onMessageFromIframe(event) {\n const message = event.data;\n switch (message.kind) {\n case \"contentSize\":\n return this.onContentSizeAvailable(message.size);\n case \"tap\":\n return this.listener.onTap({ x: message.x, y: message.y });\n case \"linkActivated\":\n return this.listener.onLinkActivated(message.href);\n }\n }\n onContentSizeAvailable(size) {\n if (!size) {\n //FIXME: handle edge case\n return;\n }\n this.iframe.style.width = size.width + \"px\";\n this.iframe.style.height = size.height + \"px\";\n this.size = size;\n this.listener.onIframeLoaded();\n }\n}\n","export class ViewportStringBuilder {\n setInitialScale(scale) {\n this.initialScale = scale;\n return this;\n }\n setMinimumScale(scale) {\n this.minimumScale = scale;\n return this;\n }\n setWidth(width) {\n this.width = width;\n return this;\n }\n setHeight(height) {\n this.height = height;\n return this;\n }\n build() {\n const components = [];\n if (this.initialScale) {\n components.push(\"initial-scale=\" + this.initialScale);\n }\n if (this.minimumScale) {\n components.push(\"minimum-scale=\" + this.minimumScale);\n }\n if (this.width) {\n components.push(\"width=\" + this.width);\n }\n if (this.height) {\n components.push(\"height=\" + this.height);\n }\n return components.join(\", \");\n }\n}\nexport function parseViewportString(viewportString) {\n const regex = /(\\w+) *= *([^\\s,]+)/g;\n const properties = new Map();\n let match;\n while ((match = regex.exec(viewportString))) {\n if (match != null) {\n properties.set(match[1], match[2]);\n }\n }\n const width = parseFloat(properties.get(\"width\"));\n const height = parseFloat(properties.get(\"height\"));\n if (width && height) {\n return { width, height };\n }\n else {\n return undefined;\n }\n}\n","export class GesturesDetector {\n constructor(window, listener) {\n this.window = window;\n this.listener = listener;\n document.addEventListener(\"click\", (event) => {\n this.onClick(event);\n }, false);\n }\n onClick(event) {\n if (event.defaultPrevented) {\n return;\n }\n const selection = this.window.getSelection();\n if (selection && selection.type == \"Range\") {\n // There's an on-going selection, the tap will dismiss it so we don't forward it.\n // selection.type might be None (collapsed) or Caret with a collapsed range\n // when there is not true selection.\n return;\n }\n let nearestElement;\n if (event.target instanceof HTMLElement) {\n nearestElement = this.nearestInteractiveElement(event.target);\n }\n else {\n nearestElement = null;\n }\n if (nearestElement) {\n if (nearestElement instanceof HTMLAnchorElement) {\n this.listener.onLinkActivated(nearestElement.href);\n }\n }\n else {\n this.listener.onTap(event);\n }\n event.stopPropagation();\n event.preventDefault();\n }\n // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling\n nearestInteractiveElement(element) {\n if (element == null) {\n return null;\n }\n const interactiveTags = [\n \"a\",\n \"audio\",\n \"button\",\n \"canvas\",\n \"details\",\n \"input\",\n \"label\",\n \"option\",\n \"select\",\n \"submit\",\n \"textarea\",\n \"video\",\n ];\n if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) {\n return element;\n }\n // Checks whether the element is editable by the user.\n if (element.hasAttribute(\"contenteditable\") &&\n element.getAttribute(\"contenteditable\").toLowerCase() != \"false\") {\n return element;\n }\n // Checks parents recursively because the touch might be for example on an inside a
.\n if (element.parentElement) {\n return this.nearestInteractiveElement(element.parentElement);\n }\n return null;\n }\n}\n","import { computeScale } from \"../util/fit\";\nimport { PageManager } from \"./page-manager\";\nimport { ViewportStringBuilder } from \"../util/viewport\";\nimport { GesturesDetector } from \"../common/gestures\";\nexport class SingleAreaManager {\n constructor(window, iframe, metaViewport, listener) {\n this.fit = \"contain\" /* Fit.Contain */;\n this.insets = { top: 0, right: 0, bottom: 0, left: 0 };\n this.scale = 1;\n window.addEventListener(\"message\", (event) => {\n if (event.source === iframe.contentWindow && event.ports[0]) {\n this.page.setMessagePort(event.ports[0]);\n }\n });\n const wrapperGesturesListener = {\n onTap: (event) => {\n const tapEvent = {\n x: (event.clientX - visualViewport.offsetLeft) *\n visualViewport.scale,\n y: (event.clientY - visualViewport.offsetTop) * visualViewport.scale,\n };\n listener.onTap(tapEvent);\n },\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLinkActivated: (_) => {\n throw Error(\"No interactive element in the root document.\");\n },\n };\n new GesturesDetector(window, wrapperGesturesListener);\n this.metaViewport = metaViewport;\n const pageListener = {\n onIframeLoaded: () => {\n this.onIframeLoaded();\n },\n onTap: (event) => {\n const boundingRect = iframe.getBoundingClientRect();\n const tapEvent = {\n x: (event.x + boundingRect.left - visualViewport.offsetLeft) *\n visualViewport.scale,\n y: (event.y + boundingRect.top - visualViewport.offsetTop) *\n visualViewport.scale,\n };\n listener.onTap(tapEvent);\n },\n onLinkActivated: (href) => {\n listener.onLinkActivated(href);\n },\n };\n this.page = new PageManager(window, iframe, pageListener);\n }\n setViewport(viewport, insets) {\n if (this.viewport == viewport && this.insets == insets) {\n return;\n }\n this.viewport = viewport;\n this.insets = insets;\n this.layout();\n }\n setFit(fit) {\n if (this.fit == fit) {\n return;\n }\n this.fit = fit;\n this.layout();\n }\n loadResource(url) {\n this.page.hide();\n this.page.loadPage(url);\n }\n onIframeLoaded() {\n if (!this.page.size) {\n // FIXME: raise error\n }\n else {\n this.layout();\n }\n }\n layout() {\n if (!this.page.size || !this.viewport) {\n return;\n }\n const margins = {\n top: this.insets.top,\n right: this.insets.right,\n bottom: this.insets.bottom,\n left: this.insets.left,\n };\n this.page.setMargins(margins);\n const safeDrawingSize = {\n width: this.viewport.width - this.insets.left - this.insets.right,\n height: this.viewport.height - this.insets.top - this.insets.bottom,\n };\n const scale = computeScale(this.fit, this.page.size, safeDrawingSize);\n this.metaViewport.content = new ViewportStringBuilder()\n .setInitialScale(scale)\n .setMinimumScale(scale)\n .setWidth(this.page.size.width)\n .setHeight(this.page.size.height)\n .build();\n this.scale = scale;\n this.page.show();\n }\n}\n","export function computeScale(fit, content, container) {\n switch (fit) {\n case \"contain\" /* Fit.Contain */:\n return fitContain(content, container);\n case \"width\" /* Fit.Width */:\n return fitWidth(content, container);\n case \"height\" /* Fit.Height */:\n return fitHeight(content, container);\n }\n}\nfunction fitContain(content, container) {\n const widthRatio = container.width / content.width;\n const heightRatio = container.height / content.height;\n return Math.min(widthRatio, heightRatio);\n}\nfunction fitWidth(content, container) {\n return container.width / content.width;\n}\nfunction fitHeight(content, container) {\n return container.height / content.height;\n}\n","export class BridgeGesturesAdapter {\n constructor(gesturesApi) {\n this.nativeApi = gesturesApi;\n }\n onTap(event) {\n this.nativeApi.onTap(JSON.stringify(event));\n }\n onLinkActivated(href) {\n this.nativeApi.onLinkActivated(href);\n }\n}\n","//\n// Copyright 2024 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\nimport { FixedSingleBridge } from \"./bridge/fixed-single-bridge\";\nconst iframe = document.getElementById(\"page\");\nconst metaViewport = document.querySelector(\"meta[name=viewport]\");\nwindow.singleArea = new FixedSingleBridge(window, iframe, metaViewport, window.gestures);\nwindow.initialization.onScriptsLoaded();\n","import { SingleAreaManager } from \"../fixed/single-area-manager\";\nimport { BridgeGesturesAdapter } from \"./fixed-gestures-bridge\";\nexport class FixedSingleBridge {\n constructor(window, iframe, metaViewport, gestures) {\n const listener = new BridgeGesturesAdapter(gestures);\n this.manager = new SingleAreaManager(window, iframe, metaViewport, listener);\n }\n loadResource(url) {\n this.manager.loadResource(url);\n }\n setViewport(viewporttWidth, viewportHeight, insetTop, insetRight, insetBottom, insetLeft) {\n const viewport = { width: viewporttWidth, height: viewportHeight };\n const insets = {\n top: insetTop,\n left: insetLeft,\n bottom: insetBottom,\n right: insetRight,\n };\n this.manager.setViewport(viewport, insets);\n }\n setFit(fit) {\n if (fit != \"contain\" && fit != \"width\" && fit != \"height\") {\n throw Error(`Invalid fit value: ${fit}`);\n }\n this.manager.setFit(fit);\n }\n}\n"],"names":["PageManager","constructor","window","iframe","listener","this","margins","top","right","bottom","left","contentWindow","Error","setMessagePort","messagePort","onmessage","message","onMessageFromIframe","show","style","display","hide","setMargins","marginTop","marginLeft","marginBottom","marginRight","loadPage","url","src","setPlaceholder","size","visibility","width","height","event","data","kind","onContentSizeAvailable","onTap","x","y","onLinkActivated","href","onIframeLoaded","ViewportStringBuilder","setInitialScale","scale","initialScale","setMinimumScale","minimumScale","setWidth","setHeight","build","components","push","join","GesturesDetector","document","addEventListener","onClick","defaultPrevented","selection","getSelection","type","nearestElement","target","HTMLElement","nearestInteractiveElement","HTMLAnchorElement","stopPropagation","preventDefault","element","indexOf","nodeName","toLowerCase","hasAttribute","getAttribute","parentElement","SingleAreaManager","metaViewport","fit","insets","source","ports","page","tapEvent","clientX","visualViewport","offsetLeft","clientY","offsetTop","_","pageListener","boundingRect","getBoundingClientRect","setViewport","viewport","layout","setFit","loadResource","safeDrawingSize","content","container","widthRatio","heightRatio","Math","min","fitContain","fitWidth","fitHeight","computeScale","BridgeGesturesAdapter","gesturesApi","nativeApi","JSON","stringify","getElementById","querySelector","singleArea","gestures","manager","viewporttWidth","viewportHeight","insetTop","insetRight","insetBottom","insetLeft","initialization","onScriptsLoaded"],"sourceRoot":""} \ No newline at end of file diff --git a/readium/navigators/web/src/main/assets/readium/navigators/web/prepaginated-double-index.html b/readium/navigators/web/src/main/assets/readium/navigators/web/prepaginated-double-index.html new file mode 100644 index 0000000000..d6041ae251 --- /dev/null +++ b/readium/navigators/web/src/main/assets/readium/navigators/web/prepaginated-double-index.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + +
+ + + +
+ + + + diff --git a/readium/navigators/web/src/main/assets/readium/navigators/web/prepaginated-single-index.html b/readium/navigators/web/src/main/assets/readium/navigators/web/prepaginated-single-index.html new file mode 100644 index 0000000000..dfae3d6ce2 --- /dev/null +++ b/readium/navigators/web/src/main/assets/readium/navigators/web/prepaginated-single-index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + +
+ +
+ + + diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/FixedWebNavigatorFactory.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/FixedWebNavigatorFactory.kt new file mode 100644 index 0000000000..728419553b --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/FixedWebNavigatorFactory.kt @@ -0,0 +1,146 @@ +/* + * 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. + */ + +package org.readium.navigator.web + +import android.app.Application +import java.io.IOException +import org.readium.navigator.web.layout.ReadingOrder +import org.readium.navigator.web.layout.ReadingOrderItem +import org.readium.navigator.web.location.FixedWebGoLocation +import org.readium.navigator.web.location.FixedWebLocatorAdapter +import org.readium.navigator.web.location.HrefLocation +import org.readium.navigator.web.preferences.FixedWebDefaults +import org.readium.navigator.web.preferences.FixedWebPreferences +import org.readium.navigator.web.preferences.FixedWebPreferencesEditor +import org.readium.navigator.web.util.WebViewServer +import org.readium.navigator.web.webapi.FixedDoubleApi +import org.readium.navigator.web.webapi.FixedSingleApi +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.epub.EpubLayout +import org.readium.r2.shared.publication.presentation.page +import org.readium.r2.shared.publication.presentation.presentation +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse + +@ExperimentalReadiumApi +public class FixedWebNavigatorFactory private constructor( + private val application: Application, + private val publication: Publication, + private val defaults: FixedWebDefaults +) { + + public companion object { + + public operator fun invoke( + application: Application, + publication: Publication + ): FixedWebNavigatorFactory? { + if (!publication.conformsTo(Publication.Profile.EPUB) || + publication.metadata.presentation.layout != EpubLayout.FIXED + ) { + return null + } + + if (publication.readingOrder.isEmpty()) { + return null + } + + return FixedWebNavigatorFactory( + application, + publication, + FixedWebDefaults() + ) + } + } + + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + public class Initialization( + cause: org.readium.r2.shared.util.Error + ) : Error("Could not initialize the navigator.", cause) + } + + public suspend fun createRenditionState( + initialLocation: FixedWebGoLocation? = null, + initialPreferences: FixedWebPreferences? = null, + readingOrder: List = publication.readingOrder + ): Try { + val items = readingOrder.map { + ReadingOrderItem( + href = it.url(), + page = it.properties.page + ) + } + + val webViewServer = + WebViewServer( + application = application, + publication = publication, + servedAssets = listOf("readium/.*"), + disableSelectionWhenProtected = false, + onResourceLoadFailed = { _, _ -> } + ) + + val preloads = preloadData() + .getOrElse { return Try.failure(it) } + + val state = + FixedWebRenditionState( + publicationMetadata = publication.metadata, + readingOrder = ReadingOrder(items), + initialPreferences = initialPreferences ?: FixedWebPreferences(), + defaults = defaults, + initialLocation = initialLocation ?: HrefLocation(items[0].href), + webViewServer = webViewServer, + preloadedData = preloads + ) + + return Try.success(state) + } + + private suspend fun preloadData(): Try = + try { + val assetsUrl = WebViewServer.assetUrl("readium/navigators/web")!! + + val prepaginatedSingleContent = FixedSingleApi.getPageContent( + assetManager = application.assets, + assetsUrl = assetsUrl + ) + + val prepaginatedDoubleContent = FixedDoubleApi.getPageContent( + assetManager = application.assets, + assetsUrl = assetsUrl + ) + + val preloadData = FixedWebPreloadedData( + fixedSingleContent = prepaginatedSingleContent, + fixedDoubleContent = prepaginatedDoubleContent + ) + + Try.success(preloadData) + } catch (e: IOException) { + Try.failure(Error.Initialization(ThrowableError(e))) + } + + public fun createPreferencesEditor( + currentPreferences: FixedWebPreferences + ): FixedWebPreferencesEditor = + FixedWebPreferencesEditor( + currentPreferences, + publication.metadata, + defaults + ) + + public fun createLocatorAdapter(): FixedWebLocatorAdapter = + FixedWebLocatorAdapter(publication) +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/FixedWebRendition.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/FixedWebRendition.kt new file mode 100644 index 0000000000..8d4ac0b9aa --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/FixedWebRendition.kt @@ -0,0 +1,169 @@ +package org.readium.navigator.web + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.LayoutDirection +import org.readium.navigator.common.HyperlinkListener +import org.readium.navigator.common.InputListener +import org.readium.navigator.common.LinkContext +import org.readium.navigator.common.NullHyperlinkListener +import org.readium.navigator.common.NullInputListener +import org.readium.navigator.common.TapContext +import org.readium.navigator.common.defaultHyperlinkListener +import org.readium.navigator.common.defaultInputListener +import org.readium.navigator.web.layout.DoubleViewportSpread +import org.readium.navigator.web.layout.ReadingOrder +import org.readium.navigator.web.layout.SingleViewportSpread +import org.readium.navigator.web.location.FixedWebLocation +import org.readium.navigator.web.pager.NavigatorPager +import org.readium.navigator.web.spread.DoubleSpreadState +import org.readium.navigator.web.spread.DoubleViewportSpread +import org.readium.navigator.web.spread.SingleSpreadState +import org.readium.navigator.web.spread.SingleViewportSpread +import org.readium.navigator.web.util.AbsolutePaddingValues +import org.readium.navigator.web.util.DisplayArea +import org.readium.navigator.web.util.WebViewServer +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.RelativeUrl +import org.readium.r2.shared.util.Url + +@ExperimentalReadiumApi +@Composable +public fun FixedWebRendition( + modifier: Modifier = Modifier, + state: FixedWebRenditionState, + windowInsets: WindowInsets = WindowInsets.displayCutout, + backgroundColor: Color = MaterialTheme.colorScheme.background, + inputListener: InputListener = state.navigator + ?.let { defaultInputListener(navigator = it) } + ?: NullInputListener, + hyperlinkListener: HyperlinkListener = + state.navigator + ?.let { defaultHyperlinkListener(navigator = it) } + ?: NullHyperlinkListener +) { + BoxWithConstraints( + modifier = Modifier.fillMaxSize(), + propagateMinConstraints = true + ) { + val viewportSize = DpSize(maxWidth, maxHeight) + + val safeDrawingPadding = windowInsets.asAbsolutePaddingValues() + val displayArea = rememberUpdatedState(DisplayArea(viewportSize, safeDrawingPadding)) + + val readingProgression = + state.layoutDelegate.settings.value.readingProgression + + val reverseLayout = + LocalLayoutDirection.current.toReadingProgression() != readingProgression + + // This is barely needed as location could be computed on the state side without any + // data from the layout pass. I keep it so for demonstration purposes of the way the + // reflowable navigator could fit the architecture as well. + val spreadIndex = state.pagerState.currentPage + val itemIndex = state.layoutDelegate.layout.value.pageIndexForSpread(spreadIndex) + val itemHref = state.readingOrder.items[itemIndex].href + state.updateLocation(FixedWebLocation(itemHref)) + + NavigatorPager( + modifier = modifier, + state = state.pagerState, + beyondViewportPageCount = 2, + key = { index -> state.layoutDelegate.layout.value.pageIndexForSpread(index) }, + reverseLayout = reverseLayout + ) { index -> + when (val spread = state.layoutDelegate.layout.value.spreads[index]) { + is SingleViewportSpread -> { + val spreadState = remember { + SingleSpreadState( + htmlData = state.preloadedData.fixedSingleContent, + publicationBaseUrl = WebViewServer.publicationBaseHref, + webViewClient = state.webViewClient, + spread = spread, + fit = state.layoutDelegate.fit, + displayArea = displayArea + ) + } + + SingleViewportSpread( + onTap = { inputListener.onTap(it, TapContext(viewportSize)) }, + onLinkActivated = { url, context -> + onLinkActivated(url, context, state.readingOrder, hyperlinkListener) + }, + state = spreadState, + backgroundColor = backgroundColor + ) + } + is DoubleViewportSpread -> { + val spreadState = remember { + DoubleSpreadState( + htmlData = state.preloadedData.fixedDoubleContent, + publicationBaseUrl = WebViewServer.publicationBaseHref, + webViewClient = state.webViewClient, + spread = spread, + fit = state.layoutDelegate.fit, + displayArea = displayArea + ) + } + + DoubleViewportSpread( + onTap = { inputListener.onTap(it, TapContext(viewportSize)) }, + onLinkActivated = { url, context -> + onLinkActivated(url, context, state.readingOrder, hyperlinkListener) + }, + state = spreadState, + backgroundColor = backgroundColor + ) + } + } + } + } +} + +@Composable +private fun WindowInsets.asAbsolutePaddingValues(): AbsolutePaddingValues { + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + val top = with(density) { getTop(density).toDp() } + val right = with(density) { getRight(density, layoutDirection).toDp() } + val bottom = with(density) { getBottom(density).toDp() } + val left = with(density) { getLeft(density, layoutDirection).toDp() } + return AbsolutePaddingValues(top = top, right = right, bottom = bottom, left = left) +} + +@OptIn(ExperimentalReadiumApi::class) +private fun LayoutDirection.toReadingProgression(): ReadingProgression = + when (this) { + LayoutDirection.Ltr -> ReadingProgression.LTR + LayoutDirection.Rtl -> ReadingProgression.RTL + } + +@OptIn(ExperimentalReadiumApi::class) +private fun onLinkActivated( + url: Url, + context: LinkContext?, + readingOrder: ReadingOrder, + listener: HyperlinkListener +) { + readingOrder.indexOfHref(url.removeFragment()) + ?.let { listener.onReadingOrderLinkActivated(url, context) } + ?: run { + when (url) { + is RelativeUrl -> listener.onResourceLinkActivated(url, context) + is AbsoluteUrl -> listener.onExternalLinkActivated(url, context) + } + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/FixedWebRenditionState.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/FixedWebRenditionState.kt new file mode 100644 index 0000000000..84684ea1a1 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/FixedWebRenditionState.kt @@ -0,0 +1,210 @@ +package org.readium.navigator.web + +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import org.readium.navigator.common.Configurable +import org.readium.navigator.common.Navigator +import org.readium.navigator.common.Overflow +import org.readium.navigator.common.Overflowable +import org.readium.navigator.common.RenditionState +import org.readium.navigator.web.layout.Layout +import org.readium.navigator.web.layout.LayoutResolver +import org.readium.navigator.web.layout.ReadingOrder +import org.readium.navigator.web.location.FixedWebGoLocation +import org.readium.navigator.web.location.FixedWebLocation +import org.readium.navigator.web.location.HrefLocation +import org.readium.navigator.web.preferences.FixedWebDefaults +import org.readium.navigator.web.preferences.FixedWebPreferences +import org.readium.navigator.web.preferences.FixedWebSettings +import org.readium.navigator.web.preferences.FixedWebSettingsResolver +import org.readium.navigator.web.util.WebViewClient +import org.readium.navigator.web.util.WebViewServer +import org.readium.r2.navigator.SimpleOverflow +import org.readium.r2.navigator.preferences.Axis +import org.readium.r2.navigator.preferences.Fit +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.Metadata + +@ExperimentalReadiumApi +@Stable +public class FixedWebRenditionState internal constructor( + internal val readingOrder: ReadingOrder, + publicationMetadata: Metadata, + defaults: FixedWebDefaults, + initialPreferences: FixedWebPreferences, + initialLocation: FixedWebGoLocation, + internal val webViewServer: WebViewServer, + internal val preloadedData: FixedWebPreloadedData +) : RenditionState { + + private val navigatorState: MutableState = + mutableStateOf(null) + + override val navigator: FixedWebNavigator? get() = + navigatorState.value + + internal val layoutDelegate: LayoutDelegate = + LayoutDelegate( + readingOrder, + publicationMetadata, + defaults, + initialPreferences + ) + + internal val webViewClient: WebViewClient = + WebViewClient(webViewServer) + + internal val pagerState: PagerState = run { + val initialPage = when (initialLocation) { + is HrefLocation -> layoutDelegate.layout.value.spreadIndexForPage(initialLocation.href) + } + + PagerState( + currentPage = layoutDelegate.layout.value.spreadIndexForPage(initialPage), + pageCount = { layoutDelegate.layout.value.spreads.size } + ) + } + + private lateinit var navigationDelegate: NavigationDelegate + + internal fun updateLocation(location: FixedWebLocation) { + initNavigatorIfNeeded(location) + navigationDelegate.updateLocation(location) + } + + private fun initNavigatorIfNeeded(location: FixedWebLocation) { + if (navigator != null) { + return + } + + navigationDelegate = + NavigationDelegate( + readingOrder, + pagerState, + layoutDelegate.layout, + layoutDelegate.settings, + location + ) + navigatorState.value = + FixedWebNavigator( + navigationDelegate, + layoutDelegate + ) + } +} + +@ExperimentalReadiumApi +@Stable +public class FixedWebNavigator internal constructor( + private val navigationDelegate: NavigationDelegate, + layoutDelegate: LayoutDelegate +) : Navigator by navigationDelegate, + Overflowable by navigationDelegate, + Configurable by layoutDelegate + +internal data class FixedWebPreloadedData( + val fixedSingleContent: String, + val fixedDoubleContent: String +) + +@OptIn(ExperimentalReadiumApi::class) +internal class LayoutDelegate( + readingOrder: ReadingOrder, + publicationMetadata: Metadata, + defaults: FixedWebDefaults, + initialPreferences: FixedWebPreferences +) : Configurable { + + private val settingsResolver: FixedWebSettingsResolver = + FixedWebSettingsResolver(publicationMetadata, defaults) + + private val layoutResolver = + LayoutResolver(readingOrder) + + override val preferences: MutableState = + mutableStateOf(initialPreferences) + + override val settings: State = + derivedStateOf { settingsResolver.settings(preferences.value) } + + val layout: State = + derivedStateOf { + val spreads = layoutResolver.layout(settings.value) + Layout(settings.value.readingProgression, spreads) + } + + val fit: State = + derivedStateOf { settings.value.fit } +} + +@OptIn(ExperimentalReadiumApi::class, InternalReadiumApi::class) +internal class NavigationDelegate( + private val readingOrder: ReadingOrder, + private val pagerState: PagerState, + private val layout: State, + private val settings: State, + initialLocation: FixedWebLocation +) : Navigator, Overflowable { + + private val locationMutable: MutableState = + mutableStateOf(initialLocation) + + internal fun updateLocation(location: FixedWebLocation) { + locationMutable.value = location + } + + override val location: State = + locationMutable + + override suspend fun goTo(link: Link) { + val href = link.url().removeFragment() + val location = HrefLocation(href) + goTo(location) + } + + override suspend fun goTo(location: FixedWebGoLocation) { + when (location) { + is HrefLocation -> { + val pageIndex = checkNotNull(readingOrder.indexOfHref(location.href)) + pagerState.scrollToPage(layout.value.spreadIndexForPage(pageIndex)) + } + } + } + + override suspend fun goTo(location: FixedWebLocation) { + return goTo(HrefLocation(location.href)) + } + + override val overflow: State = + derivedStateOf { + SimpleOverflow( + settings.value.readingProgression, + false, + Axis.HORIZONTAL + ) + } + + override val canMoveForward: Boolean + get() = pagerState.currentPage < layout.value.spreads.size - 1 + + override val canMoveBackward: Boolean + get() = pagerState.currentPage > 0 + + override suspend fun moveForward() { + if (canMoveForward) { + pagerState.scrollToPage(pagerState.currentPage + 1) + } + } + + override suspend fun moveBackward() { + if (canMoveBackward) { + pagerState.scrollToPage(pagerState.currentPage - 1) + } + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/AndroidScrollable.android.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/AndroidScrollable.android.kt new file mode 100644 index 0000000000..93c856adb0 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/AndroidScrollable.android.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.readium.navigator.web.gestures + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.node.CompositionLocalConsumerModifierNode +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFold + +internal fun CompositionLocalConsumerModifierNode.platformScrollConfig(): ScrollConfig = + AndroidConfig + +private object AndroidConfig : ScrollConfig { + override fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset { + // 64 dp value is taken from ViewConfiguration.java, replace with better solution + return event.changes.fastFold(Offset.Zero) { acc, c -> acc + c.scrollDelta } * -64.dp.toPx() + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/DragGestureDetector.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/DragGestureDetector.kt new file mode 100644 index 0000000000..e1eef2f8d1 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/DragGestureDetector.kt @@ -0,0 +1,382 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.readium.navigator.web.gestures + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.AwaitPointerEventScope +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerId +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed +import androidx.compose.ui.platform.ViewConfiguration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFirstOrNull +import kotlin.math.absoluteValue +import kotlin.math.sign + +/** + * A Gesture detector that waits for pointer down and touch slop in the direction specified by + * [orientationLock] and then calls [onDrag] for each drag event. + * It follows the touch slop detection of [awaitTouchSlopOrCancellation] but will consume the + * position change automatically once the touch slop has been crossed, the amount of drag over + * the touch slop is reported as the first drag event [onDrag] after the slop is crossed. + * If [shouldAwaitTouchSlop] returns true the touch slop recognition phase will be ignored + * and the drag gesture will be recognized immediately.The first [onDrag] in this case will report + * an [Offset.Zero]. + * + * [onDragStart] is called when the touch slop has been passed and includes an [Offset] representing + * the last known pointer position relative to the containing element as well as the initial + * down event that triggered this gesture detection cycle. The [Offset] can be outside + * the actual bounds of the element itself meaning the numbers can be negative or larger than the + * element bounds if the touch target is smaller than the + * [ViewConfiguration.minimumTouchTargetSize]. + * + * [onDragEnd] is called after all pointers are up with the event change of the up event + * and [onDragCancel] is called if another gesture has consumed pointer input, + * canceling this gesture. + * + * @param onDragStart A lambda to be called when the drag gesture starts, it contains information + * about the last known [PointerInputChange] relative to the containing element and the post slop + * delta. + * @param onDragEnd A lambda to be called when the gesture ends. It contains information about the + * up [PointerInputChange] that finished the gesture. + * @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or + * when it was consumed. + * @param shouldAwaitTouchSlop Indicates if touch slop detection should be skipped. + * @param orientationLock Optionally locks detection to this orientation, this means, when this is + * provided, touch slop detection and drag event detection will be conditioned to the given + * orientation axis. [onDrag] will still dispatch events on with information in both axis, but + * if orientation lock is provided, only events that happen on the given orientation will be + * considered. If no value is provided (i.e. null) touch slop and drag detection will happen on + * an "any" orientation basis, that is, touch slop will be detected if crossed in either direction + * and drag events will be dispatched if present in either direction. + * @param onDrag A lambda to be called for each delta event in the gesture. It contains information + * about the [PointerInputChange] and the movement offset. + * + * Example Usage: + * @sample androidx.compose.foundation.samples.DetectDragGesturesSample + * + * @see detectVerticalDragGestures + * @see detectHorizontalDragGestures + * @see detectDragGesturesAfterLongPress to detect gestures after long press + */ +internal suspend fun PointerInputScope.detectDragGestures( + onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit, + onDragEnd: (change: PointerInputChange) -> Unit, + onDragCancel: () -> Unit, + shouldAwaitTouchSlop: () -> Boolean, + orientationLock: Orientation?, + onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit +) { + awaitEachGesture { + val initialDown = + awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial) + val awaitTouchSlop = shouldAwaitTouchSlop() + + if (!awaitTouchSlop) { + initialDown.consume() + } + val down = awaitFirstDown(requireUnconsumed = false) + var drag: PointerInputChange? + var overSlop = Offset.Zero + var initialDelta = Offset.Zero + + if (awaitTouchSlop) { + do { + drag = awaitPointerSlopOrCancellation( + down.id, + down.type, + orientation = orientationLock + ) { change, over -> + change.consume() + overSlop = over + } + } while (drag != null && !drag.isConsumed) + initialDelta = overSlop + } else { + drag = initialDown + } + + if (drag != null) { + onDragStart.invoke(drag, initialDelta) + onDrag(drag, overSlop) + val upEvent = drag( + pointerId = drag.id, + onDrag = { + onDrag(it, it.positionChange()) + it.consume() + }, + orientation = orientationLock, + motionConsumed = { + it.isConsumed + } + ) + if (upEvent == null) { + onDragCancel() + } else { + onDragEnd(upEvent) + } + } + } +} + +/** + * Continues to read drag events until all pointers are up or the drag event is canceled. + * The initial pointer to use for driving the drag is [pointerId]. [onDrag] is called + * whenever the pointer moves. The up event is returned at the end of the drag gesture. + * + * @param pointerId The pointer where that is driving the gesture. + * @param onDrag Callback for every new drag event. + * @param motionConsumed If the PointerInputChange should be considered as consumed. + * + * @return The last pointer input event change when gesture ended with all pointers up + * and null when the gesture was canceled. + */ +internal suspend inline fun AwaitPointerEventScope.drag( + pointerId: PointerId, + onDrag: (PointerInputChange) -> Unit, + orientation: Orientation?, + motionConsumed: (PointerInputChange) -> Boolean +): PointerInputChange? { + if (currentEvent.isPointerUp(pointerId)) { + return null // The pointer has already been lifted, so the gesture is canceled + } + var pointer = pointerId + while (true) { + val change = awaitDragOrUp(pointer) { + val positionChange = it.positionChangeIgnoreConsumed() + val motionChange = if (orientation == null) { + positionChange.getDistance() + } else { + if (orientation == Orientation.Vertical) positionChange.y else positionChange.x + } + motionChange != 0.0f + } ?: return null + + if (motionConsumed(change)) { + return null + } + + if (change.changedToUpIgnoreConsumed()) { + return change + } + + onDrag(change) + pointer = change.id + } +} + +/** + * Waits for a single drag in one axis, final pointer up, or all pointers are up. + * When [pointerId] has lifted, another pointer that is down is chosen to be the finger + * governing the drag. When the final pointer is lifted, that [PointerInputChange] is + * returned. When a drag is detected, that [PointerInputChange] is returned. A drag is + * only detected when [hasDragged] returns `true`. + * + * `null` is returned if there was an error in the pointer input stream and the pointer + * that was down was dropped before the 'up' was received. + */ +private suspend inline fun AwaitPointerEventScope.awaitDragOrUp( + pointerId: PointerId, + hasDragged: (PointerInputChange) -> Boolean +): PointerInputChange? { + var pointer = pointerId + while (true) { + val event = awaitPointerEvent() + val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null + if (dragEvent.changedToUpIgnoreConsumed()) { + val otherDown = event.changes.fastFirstOrNull { it.pressed } + if (otherDown == null) { + // This is the last "up" + return dragEvent + } else { + pointer = otherDown.id + } + } else if (hasDragged(dragEvent)) { + return dragEvent + } + } +} + +/** + * Waits for drag motion and uses [orientation] to detect the direction of touch slop detection. + * It passes [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer from + * those that are down will be chosen to + * lead the gesture, and if none are down, `null` is returned. If [pointerId] is not down when + * [awaitPointerSlopOrCancellation] is called, then `null` is returned. + * + * When pointer slop is detected, [onPointerSlopReached] is called with the change and the distance + * beyond the pointer slop. If [onPointerSlopReached] does not consume the + * position change, pointer slop will not have been considered detected and the detection will + * continue or, if it is consumed, the [PointerInputChange] that was consumed will be returned. + * + * This works with [awaitTouchSlopOrCancellation] for the other axis to ensure that only horizontal + * or vertical dragging is done, but not both. It also works for dragging in two ways when using + * [awaitTouchSlopOrCancellation] + * + * @return The [PointerInputChange] of the event that was consumed in [onPointerSlopReached] or + * `null` if all pointers are raised or the position change was consumed by another gesture + * detector. + */ +private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation( + pointerId: PointerId, + pointerType: PointerType, + orientation: Orientation?, + onPointerSlopReached: (PointerInputChange, Offset) -> Unit +): PointerInputChange? { + if (currentEvent.isPointerUp(pointerId)) { + return null // The pointer has already been lifted, so the gesture is canceled + } + val touchSlop = viewConfiguration.pointerSlop(pointerType) + var pointer: PointerId = pointerId + val touchSlopDetector = TouchSlopDetector(orientation) + while (true) { + val event = awaitPointerEvent() + val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null + if (dragEvent.isConsumed) { + return null + } else if (dragEvent.changedToUpIgnoreConsumed()) { + val otherDown = event.changes.fastFirstOrNull { it.pressed } + if (otherDown == null) { + // This is the last "up" + return null + } else { + pointer = otherDown.id + } + } else { + val postSlopOffset = touchSlopDetector.addPointerInputChange(dragEvent, touchSlop) + if (postSlopOffset != null) { + onPointerSlopReached( + dragEvent, + postSlopOffset + ) + if (dragEvent.isConsumed) { + return dragEvent + } else { + touchSlopDetector.reset() + } + } else { + // verify that nothing else consumed the drag event + awaitPointerEvent(PointerEventPass.Final) + if (dragEvent.isConsumed) { + return null + } + } + } + } +} + +/** + * Detects if touch slop has been crossed after adding a series of [PointerInputChange]. + * For every new [PointerInputChange] one should add it to this detector using + * [addPointerInputChange]. If the position change causes the touch slop to be crossed, + * [addPointerInputChange] will return true. + */ +private class TouchSlopDetector(val orientation: Orientation? = null) { + + fun Offset.mainAxis() = if (orientation == Orientation.Horizontal) x else y + fun Offset.crossAxis() = if (orientation == Orientation.Horizontal) y else x + + /** + * The accumulation of drag deltas in this detector. + */ + private var totalPositionChange: Offset = Offset.Zero + + /** + * Adds [dragEvent] to this detector. If the accumulated position changes crosses the touch + * slop provided by [touchSlop], this method will return the post slop offset, that is the + * total accumulated delta change minus the touch slop value, otherwise this should return null. + */ + fun addPointerInputChange( + dragEvent: PointerInputChange, + touchSlop: Float + ): Offset? { + val currentPosition = dragEvent.position + val previousPosition = dragEvent.previousPosition + val positionChange = currentPosition - previousPosition + totalPositionChange += positionChange + + val inDirection = if (orientation == null) { + totalPositionChange.getDistance() + } else { + totalPositionChange.mainAxis().absoluteValue + } + + val hasCrossedSlop = inDirection >= touchSlop + + return if (hasCrossedSlop) { + calculatePostSlopOffset(touchSlop) + } else { + null + } + } + + /** + * Resets the accumulator associated with this detector. + */ + fun reset() { + totalPositionChange = Offset.Zero + } + + private fun calculatePostSlopOffset(touchSlop: Float): Offset { + return if (orientation == null) { + val touchSlopOffset = + totalPositionChange / totalPositionChange.getDistance() * touchSlop + // update postSlopOffset + totalPositionChange - touchSlopOffset + } else { + val finalMainAxisChange = totalPositionChange.mainAxis() - + (sign(totalPositionChange.mainAxis()) * touchSlop) + val finalCrossAxisChange = totalPositionChange.crossAxis() + if (orientation == Orientation.Horizontal) { + Offset(finalMainAxisChange, finalCrossAxisChange) + } else { + Offset(finalCrossAxisChange, finalMainAxisChange) + } + } + } +} + +private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean = + changes.fastFirstOrNull { it.id == pointerId }?.pressed != true + +// This value was determined using experiments and common sense. +// We can't use zero slop, because some hypothetical desktop/mobile devices can send +// pointer events with a very high precision (but I haven't encountered any that send +// events with less than 1px precision) +private val mouseSlop = 0.125.dp +private val defaultTouchSlop = 18.dp // The default touch slop on Android devices +private val mouseToTouchSlopRatio = mouseSlop / defaultTouchSlop + +// TODO(demin): consider this as part of ViewConfiguration class after we make *PointerSlop* +// functions public (see the comment at the top of the file). +// After it will be a public API, we should get rid of `touchSlop / 144` and return absolute +// value 0.125.dp.toPx(). It is not possible right now, because we can't access density. +internal fun ViewConfiguration.pointerSlop(pointerType: PointerType): Float { + return when (pointerType) { + PointerType.Mouse -> touchSlop * mouseToTouchSlopRatio + else -> touchSlop + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Draggable.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Draggable.kt new file mode 100644 index 0000000000..2d45c84e3a --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Draggable.kt @@ -0,0 +1,317 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.readium.navigator.web.gestures + +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.MutatorMutex +import androidx.compose.foundation.gestures.DragScope +import androidx.compose.foundation.gestures.DraggableState +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode +import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.input.pointer.util.addPointerInputChange +import androidx.compose.ui.node.CompositionLocalConsumerModifierNode +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.PointerInputModifierNode +import androidx.compose.ui.node.currentValueOf +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.Velocity +import kotlin.coroutines.cancellation.CancellationException +import kotlin.math.sign +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.readium.navigator.web.gestures.DragEvent.DragCancelled +import org.readium.navigator.web.gestures.DragEvent.DragDelta +import org.readium.navigator.web.gestures.DragEvent.DragStarted +import org.readium.navigator.web.gestures.DragEvent.DragStopped + +/** + * A node that performs drag gesture recognition and event propagation. + */ +internal abstract class DragGestureNode( + canDrag: (PointerInputChange) -> Boolean, + enabled: Boolean, + interactionSource: MutableInteractionSource?, + private var orientationLock: Orientation? +) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode { + + protected var canDrag = canDrag + private set + protected var enabled = enabled + private set + protected var interactionSource = interactionSource + private set + + // Use wrapper lambdas here to make sure that if these properties are updated while we suspend, + // we point to the new reference when we invoke them. startDragImmediately is a lambda since we + // need the most recent value passed to it from Scrollable. + private val _canDrag: (PointerInputChange) -> Boolean = { this.canDrag(it) } + private var channel: Channel? = null + private var dragInteraction: DragInteraction.Start? = null + private var isListeningForEvents = false + + /** + * Responsible for the dragging behavior between the start and the end of the drag. It + * continually invokes `forEachDelta` to process incoming events. In return, `forEachDelta` + * calls `dragBy` method to process each individual delta. + */ + abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) + + /** + * Passes the action needed when a drag starts. This gives the ability to pass the desired + * behavior from other nodes implementing AbstractDraggableNode + */ + abstract fun onDragStarted(startedPosition: Offset) + + /** + * Passes the action needed when a drag stops. This gives the ability to pass the desired + * behavior from other nodes implementing AbstractDraggableNode + */ + abstract fun onDragStopped(velocity: Velocity) + + /** + * If touch slop recognition should be skipped. If this is true, this node will start + * recognizing drag events immediately without waiting for touch slop. + */ + abstract fun startDragImmediately(): Boolean + + private fun startListeningForEvents() { + isListeningForEvents = true + + /** + * To preserve the original behavior we had (before the Modifier.Node migration) we need to + * scope the DragStopped and DragCancel methods to the node's coroutine scope instead of using + * the one provided by the pointer input modifier, this is to ensure that even when the pointer + * input scope is reset we will continue any coroutine scope scope that we started from these + * methods while the pointer input scope was active. + */ + coroutineScope.launch { + while (isActive) { + var event = channel?.receive() + if (event !is DragStarted) continue + processDragStart(event) + try { + drag { processDelta -> + while (event !is DragStopped && event !is DragCancelled) { + (event as? DragDelta)?.let(processDelta) + event = channel?.receive() + } + } + if (event is DragStopped) { + processDragStop(event as DragStopped) + } else if (event is DragCancelled) { + processDragCancel() + } + } catch (c: CancellationException) { + processDragCancel() + } + } + } + } + + private var pointerInputNode: SuspendingPointerInputModifierNode? = null + + override fun onDetach() { + isListeningForEvents = false + disposeInteractionSource() + } + + override fun onPointerEvent( + pointerEvent: PointerEvent, + pass: PointerEventPass, + bounds: IntSize + ) { + if (enabled && pointerInputNode == null) { + pointerInputNode = delegate(initializePointerInputNode()) + } + pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds) + } + + private fun initializePointerInputNode(): SuspendingPointerInputModifierNode { + return SuspendingPointerInputModifierNode { + // re-create tracker when pointer input block restarts. This lazily creates the tracker + // only when it is need. + val velocityTracker = VelocityTracker() + val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit = + { startEvent, initialDelta -> + if (canDrag.invoke(startEvent)) { + if (!isListeningForEvents) { + if (channel == null) { + channel = Channel(capacity = Channel.UNLIMITED) + } + startListeningForEvents() + } + val overSlopOffset = initialDelta + val xSign = sign(startEvent.position.x) + val ySign = sign(startEvent.position.y) + val adjustedStart = startEvent.position - + Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign) + + channel?.trySend(DragStarted(adjustedStart)) + } + } + + val onDragEnd: (change: PointerInputChange) -> Unit = { upEvent -> + velocityTracker.addPointerInputChange(upEvent) + val maximumVelocity = currentValueOf(LocalViewConfiguration) + .maximumFlingVelocity + val velocity = velocityTracker.calculateVelocity( + Velocity(maximumVelocity, maximumVelocity) + ) + velocityTracker.resetTracking() + channel?.trySend(DragStopped(velocity)) + } + + val onDragCancel: () -> Unit = { + channel?.trySend(DragCancelled) + } + + val shouldAwaitTouchSlop: () -> Boolean = { + !startDragImmediately() + } + + val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit = + { change, delta -> + velocityTracker.addPointerInputChange(change) + channel?.trySend(DragDelta(delta)) + } + + coroutineScope { + try { + detectDragGestures( + orientationLock = orientationLock, + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + shouldAwaitTouchSlop = shouldAwaitTouchSlop, + onDrag = onDrag + ) + } catch (cancellation: CancellationException) { + channel?.trySend(DragCancelled) + if (!isActive) throw cancellation + } + } + } + } + + override fun onCancelPointerInput() { + pointerInputNode?.onCancelPointerInput() + } + + private suspend fun processDragStart(event: DragStarted) { + dragInteraction?.let { oldInteraction -> + interactionSource?.emit(DragInteraction.Cancel(oldInteraction)) + } + val interaction = DragInteraction.Start() + interactionSource?.emit(interaction) + dragInteraction = interaction + onDragStarted(event.startPoint) + } + + private suspend fun processDragStop(event: DragStopped) { + dragInteraction?.let { interaction -> + interactionSource?.emit(DragInteraction.Stop(interaction)) + dragInteraction = null + } + onDragStopped(event.velocity) + } + + private suspend fun processDragCancel() { + dragInteraction?.let { interaction -> + interactionSource?.emit(DragInteraction.Cancel(interaction)) + dragInteraction = null + } + onDragStopped(Velocity.Zero) + } + + fun disposeInteractionSource() { + dragInteraction?.let { interaction -> + interactionSource?.tryEmit(DragInteraction.Cancel(interaction)) + dragInteraction = null + } + } + + fun update( + canDrag: (PointerInputChange) -> Boolean = this.canDrag, + enabled: Boolean = this.enabled, + interactionSource: MutableInteractionSource? = this.interactionSource, + orientationLock: Orientation? = this.orientationLock, + shouldResetPointerInputHandling: Boolean = false + ) { + var resetPointerInputHandling = shouldResetPointerInputHandling + + this.canDrag = canDrag + if (this.enabled != enabled) { + this.enabled = enabled + if (!enabled) { + disposeInteractionSource() + pointerInputNode?.let { undelegate(it) } + pointerInputNode = null + } + resetPointerInputHandling = true + } + if (this.interactionSource != interactionSource) { + disposeInteractionSource() + this.interactionSource = interactionSource + } + + if (this.orientationLock != orientationLock) { + this.orientationLock = orientationLock + resetPointerInputHandling = true + } + + if (resetPointerInputHandling) { + pointerInputNode?.resetPointerInputHandler() + } + } +} + +private class DefaultDraggableState(val onDelta: (Float) -> Unit) : DraggableState { + + private val dragScope: DragScope = object : DragScope { + override fun dragBy(pixels: Float): Unit = onDelta(pixels) + } + + private val scrollMutex = MutatorMutex() + + override suspend fun drag( + dragPriority: MutatePriority, + block: suspend DragScope.() -> Unit + ): Unit = coroutineScope { + scrollMutex.mutateWith(dragScope, dragPriority, block) + } + + override fun dispatchRawDelta(delta: Float) { + return onDelta(delta) + } +} + +internal sealed class DragEvent { + class DragStarted(val startPoint: Offset) : DragEvent() + class DragStopped(val velocity: Velocity) : DragEvent() + object DragCancelled : DragEvent() + class DragDelta(val delta: Offset) : DragEvent() +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Fling2DBehavior.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Fling2DBehavior.kt new file mode 100644 index 0000000000..af95d26eb1 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Fling2DBehavior.kt @@ -0,0 +1,32 @@ +package org.readium.navigator.web.gestures + +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.runtime.Stable +import androidx.compose.ui.unit.Velocity + +/** + * Interface to specify fling behavior. + * + * When drag has ended with velocity in [scrollable], [performFling] is invoked to perform fling + * animation and update state via [ScrollScope.scrollBy] + */ +@Stable +internal interface Fling2DBehavior { + /** + * Perform settling via fling animation with given velocity and suspend until fling has + * finished. + * + * This functions is called with [ScrollScope] to drive the state change of the + * [androidx.compose.foundation.gestures.ScrollableState] via [ScrollScope.scrollBy]. + * + * This function must return correct velocity left after it is finished flinging in order to + * guarantee proper nested scroll support. + * + * @param initialVelocity velocity available for fling in the orientation specified in + * [androidx.compose.foundation.gestures.scrollable] that invoked this method. + * + * @return remaining velocity after fling operation has ended + */ + suspend fun Scroll2DScope.performFling(initialVelocity: Velocity): Velocity +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Scrollable2D.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Scrollable2D.kt new file mode 100644 index 0000000000..29ee52dc36 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Scrollable2D.kt @@ -0,0 +1,830 @@ +package org.readium.navigator.web.gestures + +import androidx.compose.animation.core.AnimationState +import androidx.compose.animation.core.AnimationVector2D +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.TwoWayConverter +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.animateDecay +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.animation.splineBasedDecay +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.OverscrollEffect +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.LocalBringIntoViewSpec +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.MotionDurationScale +import androidx.compose.ui.focus.FocusProperties +import androidx.compose.ui.focus.FocusPropertiesModifierNode +import androidx.compose.ui.focus.FocusTargetModifierNode +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.node.CompositionLocalConsumerModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.ObserverModifierNode +import androidx.compose.ui.node.SemanticsModifierNode +import androidx.compose.ui.node.TraversableNode +import androidx.compose.ui.node.currentValueOf +import androidx.compose.ui.node.invalidateSemantics +import androidx.compose.ui.node.observeReads +import androidx.compose.ui.node.requireDensity +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.scrollBy +import androidx.compose.ui.semantics.scrollByOffset +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.util.fastAll +import androidx.compose.ui.util.fastForEach +import kotlin.coroutines.cancellation.CancellationException +import kotlin.math.abs +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Configure touch scrolling and flinging for the UI element in a single [Orientation]. + * + * Users should update their state themselves using default [ScrollableState] and its + * `consumeScrollDelta` callback or by implementing [ScrollableState] interface manually and reflect + * their own state in UI when using this component. + * + * If you don't need to have fling or nested scroll support, but want to make component simply + * draggable, consider using [draggable]. + * + * @sample androidx.compose.foundation.samples.ScrollableSample + * + * @param state [ScrollableState] state of the scrollable. Defines how scroll events will be + * interpreted by the user land logic and contains useful information about on-going events. + * @param orientation orientation of the scrolling + * @param enabled whether or not scrolling in enabled + * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will + * behave like bottom to top and left to right will behave like right to left. + * @param flingBehavior logic describing fling behavior when drag has finished with velocity. If + * `null`, default from [ScrollableDefaults.flingBehavior] will be used. + * @param interactionSource [MutableInteractionSource] that will be used to emit + * drag events when this scrollable is being dragged. + */ +@Stable +@OptIn(ExperimentalFoundationApi::class) +internal fun Modifier.scrollable2D( + state: Scrollable2DState, + enabled: Boolean = true, + reverseDirection: Boolean = false, + flingBehavior: Fling2DBehavior? = null, + interactionSource: MutableInteractionSource? = null +): Modifier = scrollable2D( + state = state, + enabled = enabled, + reverseDirection = reverseDirection, + flingBehavior = flingBehavior, + interactionSource = interactionSource, + overscrollEffect = null +) + +/** + * Configure touch scrolling and flinging for the UI element in a single [Orientation]. + * + * Users should update their state themselves using default [ScrollableState] and its + * `consumeScrollDelta` callback or by implementing [ScrollableState] interface manually and reflect + * their own state in UI when using this component. + * + * If you don't need to have fling or nested scroll support, but want to make component simply + * draggable, consider using [draggable]. + * + * This overload provides the access to [OverscrollEffect] that defines the behaviour of the + * over scrolling logic. Consider using [ScrollableDefaults.overscrollEffect] for the platform + * look-and-feel. + * + * @sample androidx.compose.foundation.samples.ScrollableSample + * + * @param state [ScrollableState] state of the scrollable. Defines how scroll events will be + * interpreted by the user land logic and contains useful information about on-going events. + * @param orientation orientation of the scrolling + * @param overscrollEffect effect to which the deltas will be fed when the scrollable have + * some scrolling delta left. Pass `null` for no overscroll. If you pass an effect you should + * also apply [androidx.compose.foundation.overscroll] modifier. + * @param enabled whether or not scrolling in enabled + * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will + * behave like bottom to top and left to right will behave like right to left. + * @param flingBehavior logic describing fling behavior when drag has finished with velocity. If + * `null`, default from [ScrollableDefaults.flingBehavior] will be used. + * @param interactionSource [MutableInteractionSource] that will be used to emit + * drag events when this scrollable is being dragged. + * @param bringIntoViewSpec The configuration that this scrollable should use to perform + * scrolling when scroll requests are received from the focus system. If null is provided the + * system will use the behavior provided by [LocalBringIntoViewSpec] which by default has a + * platform dependent implementation. + * + * Note: This API is experimental as it brings support for some experimental features: + * [overscrollEffect] and [bringIntoViewSpec]. + */ +@Stable +@ExperimentalFoundationApi +internal fun Modifier.scrollable2D( + state: Scrollable2DState, + overscrollEffect: OverscrollEffect?, + enabled: Boolean = true, + reverseDirection: Boolean = false, + flingBehavior: Fling2DBehavior? = null, + interactionSource: MutableInteractionSource? = null +) = this then Scrollable2DElement( + state, + overscrollEffect, + enabled, + reverseDirection, + flingBehavior, + interactionSource +) + +@OptIn(ExperimentalFoundationApi::class) +private class Scrollable2DElement( + val state: Scrollable2DState, + val overscrollEffect: OverscrollEffect?, + val enabled: Boolean, + val reverseDirection: Boolean, + val flingBehavior: Fling2DBehavior?, + val interactionSource: MutableInteractionSource? +) : ModifierNodeElement() { + override fun create(): Scrollable2DNode { + return Scrollable2DNode( + state, + overscrollEffect, + flingBehavior, + enabled, + reverseDirection, + interactionSource + ) + } + + override fun update(node: Scrollable2DNode) { + node.update( + state, + overscrollEffect, + enabled, + reverseDirection, + flingBehavior, + interactionSource + ) + } + + override fun hashCode(): Int { + var result = state.hashCode() + result = 31 * result + overscrollEffect.hashCode() + result = 31 * result + enabled.hashCode() + result = 31 * result + reverseDirection.hashCode() + result = 31 * result + flingBehavior.hashCode() + result = 31 * result + interactionSource.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + + if (other !is Scrollable2DElement) return false + + if (state != other.state) return false + if (overscrollEffect != other.overscrollEffect) return false + if (enabled != other.enabled) return false + if (reverseDirection != other.reverseDirection) return false + if (flingBehavior != other.flingBehavior) return false + if (interactionSource != other.interactionSource) return false + + return true + } + + override fun InspectorInfo.inspectableProperties() { + name = "scrollable" + properties["state"] = state + properties["overscrollEffect"] = overscrollEffect + properties["enabled"] = enabled + properties["reverseDirection"] = reverseDirection + properties["flingBehavior"] = flingBehavior + properties["interactionSource"] = interactionSource + } +} + +@OptIn(ExperimentalFoundationApi::class) +private class Scrollable2DNode( + state: Scrollable2DState, + private var overscrollEffect: OverscrollEffect?, + private var flingBehavior: Fling2DBehavior?, + enabled: Boolean, + reverseDirection: Boolean, + interactionSource: MutableInteractionSource? +) : DragGestureNode( + canDrag = CanDragCalculation, + enabled = enabled, + interactionSource = interactionSource, + orientationLock = null +), + ObserverModifierNode, + CompositionLocalConsumerModifierNode, + FocusPropertiesModifierNode, + SemanticsModifierNode { + + override val shouldAutoInvalidate: Boolean = false + + private val nestedScrollDispatcher = NestedScrollDispatcher() + + private val scrollableContainerNode = + delegate(ScrollableContainerNode(enabled)) + + // Place holder fling behavior, we'll initialize it when the density is available. + private val defaultFlingBehavior = DefaultFling2DBehavior(splineBasedDecay(UnityDensity)) + + private val scrollingLogic = ScrollingLogic( + scrollableState = state, + overscrollEffect = overscrollEffect, + reverseDirection = reverseDirection, + flingBehavior = flingBehavior ?: defaultFlingBehavior, + nestedScrollDispatcher = nestedScrollDispatcher + ) + + private val nestedScrollConnection = + ScrollableNestedScrollConnection(enabled = enabled, scrollingLogic = scrollingLogic) + + // Need to wait until onAttach to read the scroll config. Currently this is static, so we + // don't need to worry about observation / updating this over time. + private var scrollConfig: ScrollConfig? = null + private var scrollByAction: ((x: Float, y: Float) -> Boolean)? = null + private var scrollByOffsetAction: (suspend (Offset) -> Offset)? = null + + init { + /** + * Nested scrolling + */ + delegate(nestedScrollModifierNode(nestedScrollConnection, nestedScrollDispatcher)) + + /** + * Focus scrolling + */ + delegate(FocusTargetModifierNode()) + } + + override suspend fun drag( + forEachDelta: suspend ((dragDelta: DragEvent.DragDelta) -> Unit) -> Unit + ) { + with(scrollingLogic) { + scroll(scrollPriority = MutatePriority.UserInput) { + forEachDelta { + scrollByWithOverscroll( + it.delta, + source = NestedScrollSource.UserInput + ) + } + } + } + } + + override fun onDragStarted(startedPosition: Offset) {} + + override fun onDragStopped(velocity: Velocity) { + nestedScrollDispatcher.coroutineScope.launch { + scrollingLogic.onDragStopped(velocity) + } + } + + override fun startDragImmediately(): Boolean { + return scrollingLogic.shouldScrollImmediately() + } + + fun update( + state: Scrollable2DState, + overscrollEffect: OverscrollEffect?, + enabled: Boolean, + reverseDirection: Boolean, + flingBehavior: Fling2DBehavior?, + interactionSource: MutableInteractionSource? + ) { + var shouldInvalidateSemantics = false + if (this.enabled != enabled) { // enabled changed + nestedScrollConnection.enabled = enabled + scrollableContainerNode.update(enabled) + shouldInvalidateSemantics = true + } + // a new fling behavior was set, change the resolved one. + val resolvedFlingBehavior = flingBehavior ?: defaultFlingBehavior + + val resetPointerInputHandling = scrollingLogic.update( + scrollableState = state, + overscrollEffect = overscrollEffect, + reverseDirection = reverseDirection, + flingBehavior = resolvedFlingBehavior, + nestedScrollDispatcher = nestedScrollDispatcher + ) + + this.overscrollEffect = overscrollEffect + this.flingBehavior = flingBehavior + + // update DragGestureNode + update( + canDrag = CanDragCalculation, + enabled = enabled, + interactionSource = interactionSource, + orientationLock = null, + shouldResetPointerInputHandling = resetPointerInputHandling + ) + + if (shouldInvalidateSemantics) { + clearScrollSemanticsActions() + invalidateSemantics() + } + } + + override fun onAttach() { + updateDefaultFlingBehavior() + scrollConfig = platformScrollConfig() + } + + override fun onObservedReadsChanged() { + // if density changes, update the default fling behavior. + updateDefaultFlingBehavior() + } + + private fun updateDefaultFlingBehavior() { + // monitor change in Density + observeReads { + val density = currentValueOf(LocalDensity) + defaultFlingBehavior.flingDecay = splineBasedDecay(density) + } + } + + override fun applyFocusProperties(focusProperties: FocusProperties) { + focusProperties.canFocus = false + } + + override fun onPointerEvent( + pointerEvent: PointerEvent, + pass: PointerEventPass, + bounds: IntSize + ) { + super.onPointerEvent(pointerEvent, pass, bounds) + if (pass == PointerEventPass.Main && pointerEvent.type == PointerEventType.Scroll) { + processMouseWheelEvent(pointerEvent, bounds) + } + } + + override fun SemanticsPropertyReceiver.applySemantics() { + if (enabled && (scrollByAction == null || scrollByOffsetAction == null)) { + setScrollSemanticsActions() + } + + scrollByAction?.let { + scrollBy(action = it) + } + + scrollByOffsetAction?.let { + scrollByOffset(action = it) + } + } + + private fun setScrollSemanticsActions() { + scrollByAction = { x, y -> + coroutineScope.launch { + scrollingLogic.semanticsScrollBy(Offset(x, y)) + } + true + } + + scrollByOffsetAction = { offset -> scrollingLogic.semanticsScrollBy(offset) } + } + + private fun clearScrollSemanticsActions() { + scrollByAction = null + scrollByOffsetAction = null + } + + /** + * Mouse wheel + */ + private fun processMouseWheelEvent(event: PointerEvent, size: IntSize) { + if (event.changes.fastAll { !it.isConsumed }) { + with(scrollConfig!!) { + val scrollAmount = requireDensity().calculateMouseWheelScroll(event, size) + // A coroutine is launched for every individual scroll event in the + // larger scroll gesture. If we see degradation in the future (that is, + // a fast scroll gesture on a slow device causes UI jank [not seen up to + // this point), we can switch to a more efficient solution where we + // lazily launch one coroutine (with the first event) and use a Channel + // to communicate the scroll amount to the UI thread. + coroutineScope.launch { + scrollingLogic.scroll(scrollPriority = MutatePriority.UserInput) { + scrollBy( + offset = scrollAmount, + source = NestedScrollSource.UserInput + ) + } + } + event.changes.fastForEach { it.consume() } + } + } + } +} + +/** + * Contains the default values used by [scrollable] + */ +internal object Scrollable2DDefaults { + + /** + * Create and remember default [FlingBehavior] that will represent natural fling curve. + */ + @Composable + fun flingBehavior(): Fling2DBehavior { + val flingSpec = rememberSplineBasedDecay() + return remember(flingSpec) { + DefaultFling2DBehavior(flingSpec) + } + } + + /** + * Used to determine the value of `reverseDirection` parameter of [Modifier.scrollable] + * in scrollable layouts. + * + * @param layoutDirection current layout direction (e.g. from [LocalLayoutDirection]) + * @param orientation orientation of scroll + * @param reverseScrolling whether scrolling direction should be reversed + * + * @return `true` if scroll direction should be reversed, `false` otherwise. + */ + fun reverseDirection( + layoutDirection: LayoutDirection, + orientation: Orientation, + reverseScrolling: Boolean + ): Boolean { + // A finger moves with the content, not with the viewport. Therefore, + // always reverse once to have "natural" gesture that goes reversed to layout + var reverseDirection = !reverseScrolling + // But if rtl and horizontal, things move the other way around + val isRtl = layoutDirection == LayoutDirection.Rtl + if (isRtl && orientation != Orientation.Vertical) { + reverseDirection = !reverseDirection + } + return reverseDirection + } +} + +internal interface ScrollConfig { + fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset +} + +private val CanDragCalculation: (PointerInputChange) -> Boolean = + { change -> change.type != PointerType.Mouse } + +/** + * Holds all scrolling related logic: controls nested scrolling, flinging, overscroll and delta + * dispatching. + */ +@OptIn(ExperimentalFoundationApi::class) +internal class ScrollingLogic( + private var scrollableState: Scrollable2DState, + private var overscrollEffect: OverscrollEffect?, + private var flingBehavior: Fling2DBehavior, + private var reverseDirection: Boolean, + private var nestedScrollDispatcher: NestedScrollDispatcher +) { + fun Offset.reverseIfNeeded(): Offset = if (reverseDirection) this * -1f else this + + fun Velocity.reverseIfNeeded(): Velocity = if (reverseDirection) this * -1f else this + + private var latestScrollSource = NestedScrollSource.UserInput + private var outerStateScope = NoOpScrollScope + + private val nestedScrollScope = object : NestedScrollScope { + override fun scrollBy(offset: Offset, source: NestedScrollSource): Offset { + return with(outerStateScope) { + performScroll(offset, source) + } + } + + override fun scrollByWithOverscroll(offset: Offset, source: NestedScrollSource): Offset { + latestScrollSource = source + val overscroll = overscrollEffect + return if (overscroll != null && shouldDispatchOverscroll) { + overscroll.applyToScroll(offset, latestScrollSource, performScrollForOverscroll) + } else { + with(outerStateScope) { + performScroll(offset, source) + } + } + } + } + + private val performScrollForOverscroll: (Offset) -> Offset = { delta -> + with(outerStateScope) { + performScroll(delta, latestScrollSource) + } + } + + private fun Scroll2DScope.performScroll(delta: Offset, source: NestedScrollSource): Offset { + val consumedByPreScroll = + nestedScrollDispatcher.dispatchPreScroll(delta, source) + + val scrollAvailableAfterPreScroll = delta - consumedByPreScroll + + val deltaForSelfScroll = + scrollAvailableAfterPreScroll.reverseIfNeeded() + + // Consume on a single axis. + val consumedBySelfScroll = + scrollBy(deltaForSelfScroll).reverseIfNeeded() + + val deltaAvailableAfterScroll = scrollAvailableAfterPreScroll - consumedBySelfScroll + val consumedByPostScroll = nestedScrollDispatcher.dispatchPostScroll( + consumedBySelfScroll, + deltaAvailableAfterScroll, + source + ) + + return consumedByPreScroll + consumedBySelfScroll + consumedByPostScroll + } + + private val shouldDispatchOverscroll + get() = scrollableState.canScrollTop || scrollableState.canScrollBottom || + scrollableState.canScrollRight || scrollableState.canScrollLeft + + fun performRawScroll(scroll: Offset): Offset { + return if (scrollableState.isScrollInProgress) { + Offset.Zero + } else { + scrollableState.dispatchRawDelta(scroll.reverseIfNeeded()) + .reverseIfNeeded() + } + } + + suspend fun onDragStopped(availableVelocity: Velocity) { + val performFling: suspend (Velocity) -> Velocity = { velocity -> + val preConsumedByParent = nestedScrollDispatcher + .dispatchPreFling(velocity) + val available = velocity - preConsumedByParent + + val velocityLeft = doFlingAnimation(available) + + val consumedPost = + nestedScrollDispatcher.dispatchPostFling( + (available - velocityLeft), + velocityLeft + ) + val totalLeft = velocityLeft - consumedPost + velocity - totalLeft + } + + val overscroll = overscrollEffect + if (overscroll != null && shouldDispatchOverscroll) { + overscroll.applyToFling(availableVelocity, performFling) + } else { + performFling(availableVelocity) + } + } + + suspend fun doFlingAnimation(available: Velocity): Velocity { + var result: Velocity = available + + // Unlike the scrollable modifier, we bypass nested scroll while performing fling + // so that nested scroll is more predictable : ancestors do not get called while + // this is performing fling, only preFling beforehand and postFling afterwards. + scrollableState.scroll(scrollPriority = MutatePriority.Default) { + val scrollScope = this + val reverseScope = object : Scroll2DScope { + override fun scrollBy(pixels: Offset): Offset { + return scrollScope.scrollBy( + pixels = pixels.reverseIfNeeded() + ).reverseIfNeeded() + } + } + + with(reverseScope) { + with(flingBehavior) { + result = performFling(available.reverseIfNeeded()).reverseIfNeeded() + } + } + } + return result + } + + fun shouldScrollImmediately(): Boolean { + return scrollableState.isScrollInProgress || + overscrollEffect?.isInProgress ?: false + } + + /** + * Opens a scrolling session with nested scrolling and overscroll support. + */ + suspend fun scroll( + scrollPriority: MutatePriority = MutatePriority.Default, + block: suspend NestedScrollScope.() -> Unit + ) { + scrollableState.scroll(scrollPriority) { + outerStateScope = this + block.invoke(nestedScrollScope) + } + } + + /** + * @return true if the pointer input should be reset + */ + fun update( + scrollableState: Scrollable2DState, + overscrollEffect: OverscrollEffect?, + reverseDirection: Boolean, + flingBehavior: Fling2DBehavior, + nestedScrollDispatcher: NestedScrollDispatcher + ): Boolean { + var resetPointerInputHandling = false + if (this.scrollableState != scrollableState) { + this.scrollableState = scrollableState + resetPointerInputHandling = true + } + this.overscrollEffect = overscrollEffect + if (this.reverseDirection != reverseDirection) { + this.reverseDirection = reverseDirection + resetPointerInputHandling = true + } + this.flingBehavior = flingBehavior + this.nestedScrollDispatcher = nestedScrollDispatcher + return resetPointerInputHandling + } +} + +private val NoOpScrollScope: Scroll2DScope = object : Scroll2DScope { + override fun scrollBy(pixels: Offset): Offset = pixels +} + +private class ScrollableNestedScrollConnection( + val scrollingLogic: ScrollingLogic, + var enabled: Boolean +) : NestedScrollConnection { + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset = if (enabled) { + scrollingLogic.performRawScroll(available) + } else { + Offset.Zero + } + + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity + ): Velocity { + return if (enabled) { + val velocityLeft = scrollingLogic.doFlingAnimation(available) + available - velocityLeft + } else { + Velocity.Zero + } + } +} + +internal class DefaultFling2DBehavior( + var flingDecay: DecayAnimationSpec, + private val motionDurationScale: MotionDurationScale = DefaultScrollMotionDurationScale +) : Fling2DBehavior { + + // For Testing + var lastAnimationCycleCount = 0 + + override suspend fun Scroll2DScope.performFling(initialVelocity: Velocity): Velocity { + lastAnimationCycleCount = 0 + // come up with the better threshold, but we need it since spline curve gives us NaNs + return withContext(motionDurationScale) { + if (abs(initialVelocity.x) > 1f || abs(initialVelocity.y) > 1f) { + var velocityLeft = Offset(initialVelocity.x, initialVelocity.y) + var lastValue = Offset.Zero + var hasStarted = false + val animationState = AnimationState( + typeConverter = Offset.VectorConverter, + initialValue = Offset.Zero, + initialVelocityVector = AnimationVector2D(initialVelocity.x, initialVelocity.y) + ) + try { + animationState.animateDecay(flingDecay) { + val delta = value - lastValue + val consumed = scrollBy(delta) + lastValue = value + velocityLeft = this.velocity + // avoid rounding errors and stop if anything is unconsumed on both axes + val unconsumedX = abs(delta.x) <= 0.5f || abs(delta.x - consumed.x) > 0.5f + val unconsumedY = abs(delta.y) <= 0.5f || abs(delta.y - consumed.y) > 0.5f + if (hasStarted && unconsumedX && unconsumedY) { + this.cancelAnimation() + } + lastAnimationCycleCount++ + hasStarted = true + } + } catch (exception: CancellationException) { + velocityLeft = animationState.velocity + } + Velocity(velocityLeft.x, velocityLeft.y) + } else { + initialVelocity + } + } + } +} + +private const val DefaultScrollMotionDurationScaleFactor = 1f +internal val DefaultScrollMotionDurationScale = object : MotionDurationScale { + override val scaleFactor: Float + get() = DefaultScrollMotionDurationScaleFactor +} + +/** + * (b/311181532): This could not be flattened so we moved it to TraversableNode, but ideally + * ScrollabeNode should be the one to be travesable. + */ +internal class ScrollableContainerNode(enabled: Boolean) : + Modifier.Node(), + TraversableNode { + override val traverseKey: Any = TraverseKey + + var enabled: Boolean = enabled + private set + + companion object TraverseKey + + fun update(enabled: Boolean) { + this.enabled = enabled + } +} + +internal val UnityDensity = object : Density { + override val density: Float + get() = 1f + override val fontScale: Float + get() = 1f +} + +/** + * A scroll scope for nested scrolling and overscroll support. + */ +internal interface NestedScrollScope { + fun scrollBy( + offset: Offset, + source: NestedScrollSource + ): Offset + + fun scrollByWithOverscroll( + offset: Offset, + source: NestedScrollSource + ): Offset +} + +/** + * Scroll deltas originating from the semantics system. Should be dispatched as an animation + * driven event. + */ +private suspend fun ScrollingLogic.semanticsScrollBy(offset: Offset): Offset { + var previousValue = Offset.Zero + scroll(scrollPriority = MutatePriority.Default) { + animate(Offset.VectorConverter, Offset.Zero, offset) { currentValue, _ -> + val delta = currentValue - previousValue + val consumed = + scrollBy( + offset = delta.reverseIfNeeded(), + source = NestedScrollSource.UserInput + ).reverseIfNeeded() + previousValue += consumed + } + } + return previousValue +} + +/** + * A type converter that converts a [Velocity] to a [AnimationVector2D], and vice versa. + */ +internal val Velocity.Companion.VectorConverter: TwoWayConverter + get() = VelocityToVector + +/** + * A type converter that converts a [Velocity] to a [AnimationVector2D], and vice versa. + */ +private val VelocityToVector: TwoWayConverter = + TwoWayConverter( + convertToVector = { AnimationVector2D(it.x, it.y) }, + convertFromVector = { Velocity(it.v1, it.v2) } + ) diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Scrollable2DState.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Scrollable2DState.kt new file mode 100644 index 0000000000..4ab82fbd73 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/gestures/Scrollable2DState.kt @@ -0,0 +1,169 @@ +package org.readium.navigator.web.gestures + +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.MutatorMutex +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.geometry.Offset +import kotlinx.coroutines.coroutineScope + +/** + * An object representing something that can be scrolled. This interface is implemented by states + * of scrollable containers such as [androidx.compose.foundation.lazy.LazyListState] or + * [androidx.compose.foundation.ScrollState] in order to provide low-level scrolling control via + * [scroll], as well as allowing for higher-level scrolling functions like + * [animateScrollBy] to be implemented as extension + * functions on [ScrollableState]. + * + * Subclasses may also have their own methods that are specific to their interaction paradigm, such + * as [androidx.compose.foundation.lazy.LazyListState.scrollToItem]. + * + * @see androidx.compose.foundation.gestures.animateScrollBy + * @see androidx.compose.foundation.gestures.scrollable + */ +internal interface Scrollable2DState { + /** + * Call this function to take control of scrolling and gain the ability to send scroll events + * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be + * performed within a [scroll] block (even if they don't call any other methods on this + * object) in order to guarantee that mutual exclusion is enforced. + * + * If [scroll] is called from elsewhere with the [scrollPriority] higher or equal to ongoing + * scroll, ongoing scroll will be canceled. + */ + suspend fun scroll( + scrollPriority: MutatePriority = MutatePriority.Default, + block: suspend Scroll2DScope.() -> Unit + ) + + /** + * Dispatch scroll delta in pixels avoiding all scroll related mechanisms. + * + * **NOTE:** unlike [scroll], dispatching any delta with this method won't trigger nested + * scroll, won't stop ongoing scroll/drag animation and will bypass scrolling of any priority. + * This method will also ignore `reverseDirection` and other parameters set in scrollable. + * + * This method is used internally for nested scrolling dispatch and other low level + * operations, allowing implementers of [ScrollableState] influence the consumption as suits + * them. Manually dispatching delta via this method will likely result in a bad user experience, + * you must prefer [scroll] method over this one. + * + * @param delta amount of scroll dispatched in the nested scroll process + * + * @return the amount of delta consumed + */ + fun dispatchRawDelta(delta: Offset): Offset + + /** + * Whether this [ScrollableState] is currently scrolling by gesture, fling or programmatically or + * not. + */ + val isScrollInProgress: Boolean + + val canScrollBottom: Boolean + get() = true + + val canScrollTop: Boolean + get() = true + + val canScrollLeft: Boolean + get() = true + + val canScrollRight: Boolean + get() = true +} + +/** + * Default implementation of [ScrollableState] interface that contains necessary information about the + * ongoing fling and provides smooth scrolling capabilities. + * + * This is the simplest way to set up a [scrollable] modifier. When constructing this + * [ScrollableState], you must provide a [consumeScrollDelta] lambda, which will be invoked whenever + * scroll happens (by gesture input, by smooth scrolling, by flinging or nested scroll) with the + * delta in pixels. The amount of scrolling delta consumed must be returned from this lambda to + * ensure proper nested scrolling behaviour. + * + * @param consumeScrollDelta callback invoked when drag/fling/smooth scrolling occurs. The + * callback receives the delta in pixels. Callers should update their state in this lambda and + * return the amount of delta consumed + */ +internal fun Scrollable2DState(consumeScrollDelta: (Offset) -> Offset): Scrollable2DState { + return DefaultScrollable2DState(consumeScrollDelta) +} + +/** + * Create and remember the default implementation of [ScrollableState] interface that contains + * necessary information about the ongoing fling and provides smooth scrolling capabilities. + * + * This is the simplest way to set up a [scrollable] modifier. When constructing this + * [ScrollableState], you must provide a [consumeScrollDelta] lambda, which will be invoked whenever + * scroll happens (by gesture input, by smooth scrolling, by flinging or nested scroll) with the + * delta in pixels. The amount of scrolling delta consumed must be returned from this lambda to + * ensure proper nested scrolling behaviour. + * + * @param consumeScrollDelta callback invoked when drag/fling/smooth scrolling occurs. The + * callback receives the delta in pixels. Callers should update their state in this lambda and + * return the amount of delta consumed + */ +@Composable +internal fun rememberScrollable2DState(consumeScrollDelta: (Offset) -> Offset): Scrollable2DState { + val lambdaState = rememberUpdatedState(consumeScrollDelta) + return remember { Scrollable2DState { lambdaState.value.invoke(it) } } +} + +/** + * Scope used for suspending scroll blocks + */ +internal interface Scroll2DScope { + /** + * Attempts to scroll forward by [pixels] px. + * + * @return the amount of the requested scroll that was consumed (that is, how far it scrolled) + */ + fun scrollBy(pixels: Offset): Offset +} + +internal class DefaultScrollable2DState(val onDelta: (Offset) -> Offset) : Scrollable2DState { + + private val scrollScope: Scroll2DScope = object : Scroll2DScope { + override fun scrollBy(pixels: Offset): Offset { + val coercedPixels = Offset( + x = if (pixels.x.isNaN()) 0f else pixels.x, + y = if (pixels.y.isNaN()) 0f else pixels.y + ) + if (coercedPixels == Offset.Zero) return Offset.Zero + val delta = onDelta(coercedPixels) + return delta + } + } + + private val scrollMutex = MutatorMutex() + + private val isScrollingState = mutableStateOf(false) + + override suspend fun scroll( + scrollPriority: MutatePriority, + block: suspend Scroll2DScope.() -> Unit + ): Unit = coroutineScope { + scrollMutex.mutateWith(scrollScope, scrollPriority) { + isScrollingState.value = true + try { + block() + } finally { + isScrollingState.value = false + } + } + } + + override fun dispatchRawDelta(delta: Offset): Offset { + return onDelta(delta) + } + + override val isScrollInProgress: Boolean + get() = isScrollingState.value +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/layout/Layout.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/layout/Layout.kt new file mode 100644 index 0000000000..f2f7a31370 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/layout/Layout.kt @@ -0,0 +1,98 @@ +@file:OptIn(ExperimentalReadiumApi::class) + +package org.readium.navigator.web.layout + +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Url + +internal class Layout( + val readingProgression: ReadingProgression, + val spreads: List +) { + + fun spreadIndexForPage(href: Url): Int = spreads + .indexOfFirst { href in it.pages.map { page -> page.href } } + .also { check(it != -1) } + + fun spreadIndexForPage(pageIndex: Int): Int = spreads + .indexOfFirst { pageIndex in it.pages.map { page -> page.index } } + .also { check(it != -1) } + + fun pageIndexForSpread(spreadIndex: Int) = + when (val spread = spreads[spreadIndex]) { + is SingleViewportSpread -> + spread.page.index + is LeftOnlySpread -> + spread.page.index + is RightOnlySpread -> + spread.page.index + is DoubleSpread -> + when (readingProgression) { + ReadingProgression.LTR -> spread.leftPage.index + ReadingProgression.RTL -> spread.rightPage.index + } + } +} + +internal data class Page( + val index: Int, + val href: Url +) + +internal sealed interface Spread { + + val pages: List + + fun contains(href: Url): Boolean = + href in pages.map { it.href } +} + +internal data class SingleViewportSpread( + val page: Page +) : Spread { + + override val pages: List get() = listOfNotNull(page) +} + +internal sealed class DoubleViewportSpread( + override val pages: List +) : Spread { + abstract val leftPage: Page? + + abstract val rightPage: Page? + + companion object { + + operator fun invoke(leftPage: Page?, rightPage: Page?): Spread = + when { + leftPage != null && rightPage != null -> DoubleSpread(leftPage, rightPage) + leftPage != null -> LeftOnlySpread(leftPage) + rightPage != null -> RightOnlySpread(rightPage) + else -> throw IllegalArgumentException("Attempt to create an empty spread.") + } + } +} + +internal data class LeftOnlySpread( + val page: Page +) : DoubleViewportSpread(listOf(page)) { + + override val leftPage: Page = page + + override val rightPage: Page? = null +} + +internal data class RightOnlySpread( + val page: Page +) : DoubleViewportSpread(listOf(page)) { + + override val leftPage: Page? = null + + override val rightPage: Page = page +} + +internal data class DoubleSpread( + override val leftPage: Page, + override val rightPage: Page +) : DoubleViewportSpread(listOf(leftPage, rightPage)) diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/layout/LayoutResolver.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/layout/LayoutResolver.kt new file mode 100644 index 0000000000..3e10e37f9a --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/layout/LayoutResolver.kt @@ -0,0 +1,96 @@ +package org.readium.navigator.web.layout + +import org.readium.navigator.web.preferences.FixedWebSettings +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.presentation.Presentation + +@OptIn(ExperimentalReadiumApi::class) +internal class LayoutResolver( + private val readingOrder: ReadingOrder +) { + + fun layout(settings: FixedWebSettings): List = + if (settings.spreads) { + when (settings.readingProgression) { + ReadingProgression.LTR -> layoutSpreadsLtr() + ReadingProgression.RTL -> layoutSpreadsRtl() + } + } else { + readingOrder.items.mapIndexed { index, item -> + SingleViewportSpread( + Page(index, item.href) + ) + } + } + + private fun layoutSpreadsLtr(): List = + buildList { + var pending: Page? = null + + for ((index, item) in readingOrder.items.withIndex()) { + val page = Page(index, item.href) + + when (item.page) { + Presentation.Page.LEFT -> { + pending?.let { add(LeftOnlySpread(it)) } + pending = page + } + Presentation.Page.RIGHT -> { + add(DoubleViewportSpread(pending, page)) + pending = null + } + Presentation.Page.CENTER -> { + pending?.let { add(LeftOnlySpread(it)) } + pending = null + add(SingleViewportSpread(page)) + } + null -> { + if (pending == null) { + pending = page + } else { + add(DoubleSpread(pending, page)) + pending = null + } + } + } + } + + pending?.let { add(LeftOnlySpread(it)) } + } + + private fun layoutSpreadsRtl(): List = + buildList { + var pending: Page? = null + + for ((index, item) in readingOrder.items.withIndex()) { + val page = Page(index, item.href) + + when (item.page) { + Presentation.Page.LEFT -> { + add(DoubleViewportSpread(page, pending)) + pending = null + } + Presentation.Page.RIGHT -> { + pending?.let { add(RightOnlySpread(it)) } + pending = page + } + Presentation.Page.CENTER -> { + pending?.let { add(RightOnlySpread(it)) } + pending = null + add(SingleViewportSpread(page)) + } + null -> { + if (pending == null) { + pending = page + } else { + add(DoubleSpread(page, pending)) + pending = null + } + } + } + } + + pending?.let { add(RightOnlySpread(it)) } + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/layout/ReadingOrder.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/layout/ReadingOrder.kt new file mode 100644 index 0000000000..b10b8ceb91 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/layout/ReadingOrder.kt @@ -0,0 +1,23 @@ +package org.readium.navigator.web.layout + +import org.readium.r2.shared.DelicateReadiumApi +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.presentation.Presentation +import org.readium.r2.shared.util.Url + +@ExperimentalReadiumApi +internal data class ReadingOrder( + val items: List +) { + + @OptIn(DelicateReadiumApi::class) + fun indexOfHref(href: Url): Int? = items + .indexOfFirst { it.href == href } + .takeUnless { it == -1 } +} + +@ExperimentalReadiumApi +internal data class ReadingOrderItem( + val href: Url, + val page: Presentation.Page? +) diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/location/LocationTypes.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/location/LocationTypes.kt new file mode 100644 index 0000000000..f0472273df --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/location/LocationTypes.kt @@ -0,0 +1,69 @@ +package org.readium.navigator.web.location + +import org.readium.navigator.common.GoLocation +import org.readium.navigator.common.Location +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Url + +@ExperimentalReadiumApi +@JvmInline +public value class CssSelector( + public val value: String +) + +@ExperimentalReadiumApi +@JvmInline +public value class Progression( + public val value: Double +) + +@ExperimentalReadiumApi +@JvmInline +public value class TextFragment( + public val value: String +) + +@ExperimentalReadiumApi +public sealed interface FixedWebGoLocation : GoLocation + +@ExperimentalReadiumApi +public sealed interface ReflowableWebGoLocation : GoLocation + +@ExperimentalReadiumApi +public data class ProgressionLocation( + val href: Url, + val progression: Double +) : ReflowableWebGoLocation + +@ExperimentalReadiumApi +public data class TextLocation( + val href: Url, + val cssSelector: String?, + val textBefore: String?, + val textAfter: String? +) : ReflowableWebGoLocation + +@ExperimentalReadiumApi +public data class PositionLocation( + val position: Int +) : ReflowableWebGoLocation + +@ExperimentalReadiumApi +public data class HrefLocation( + val href: Url +) : ReflowableWebGoLocation, FixedWebGoLocation + +@ExperimentalReadiumApi +public data class ReflowableWebLocation( + override val href: Url, + val progression: Double, + val cssSelector: String?, + val textBefore: String?, + val textAfter: String?, + val position: Int? +) : Location + +@ExperimentalReadiumApi +public data class FixedWebLocation( + override val href: Url +) : Location diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/location/LocatorAdapters.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/location/LocatorAdapters.kt new file mode 100644 index 0000000000..8a2447e9d8 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/location/LocatorAdapters.kt @@ -0,0 +1,78 @@ +package org.readium.navigator.web.location + +import org.readium.navigator.common.LocatorAdapter +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.html.cssSelector +import org.readium.r2.shared.publication.indexOfFirstWithHref + +@ExperimentalReadiumApi +public class FixedWebLocatorAdapter( + private val publication: Publication +) : LocatorAdapter { + public fun FixedWebGoLocation.toLocator(): Locator = + when (this) { + is HrefLocation -> publication.locatorFromLink(Link(href))!! + } + + public override fun Locator.toGoLocation(): FixedWebGoLocation = + HrefLocation(href) + + override fun FixedWebLocation.toLocator(): Locator { + val position = publication.readingOrder.indexOfFirstWithHref(href) + return publication.locatorFromLink(Link(href))!! + .copyWithLocations(position = position) + } +} + +@ExperimentalReadiumApi +public class ReflowableWebLocatorAdapter( + private val publication: Publication, + private val allowProduceHrefLocation: Boolean = false +) : LocatorAdapter { + public override fun ReflowableWebLocation.toLocator(): Locator = + publication.locatorFromLink(Link(href))!! + .copy( + text = Locator.Text( + after = textAfter, + before = textBefore + ) + ) + .copyWithLocations( + progression = progression, + position = position, + otherLocations = buildMap { cssSelector?.let { put("cssSelector", cssSelector) } } + ) + + public override fun Locator.toGoLocation(): ReflowableWebGoLocation { + return when { + text.highlight != null || text.before != null || text.after != null -> { + TextLocation( + href = href, + textBefore = text.before, + textAfter = text.highlight?.let { it + text.after } ?: text.after, + cssSelector = locations.cssSelector + ) + } + locations.progression != null -> { + ProgressionLocation( + href = href, + progression = locations.progression!! + ) + } + locations.position != null -> { + PositionLocation( + position = locations.position!! + ) + } + else -> + if (allowProduceHrefLocation) { + HrefLocation(href = href) + } else { + throw IllegalArgumentException("No supported location found in locator.") + } + } + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/LoggingNestedScrollConnection.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/LoggingNestedScrollConnection.kt new file mode 100644 index 0000000000..8fbe924f14 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/LoggingNestedScrollConnection.kt @@ -0,0 +1,36 @@ +package org.readium.navigator.web.pager + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Velocity +import timber.log.Timber + +internal class LoggingNestedScrollConnection( + private val delegateNestedScrollConnection: NestedScrollConnection +) : NestedScrollConnection { + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + Timber.d("onPostFling consumed $consumed, available $available") + return delegateNestedScrollConnection.onPostFling(consumed, available) + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + Timber.d("onPostScroll consumed, $consumed, available $available") + return delegateNestedScrollConnection.onPostScroll(consumed, available, source) + } + + override suspend fun onPreFling(available: Velocity): Velocity { + Timber.d("onPreFling available $available") + return delegateNestedScrollConnection.onPreFling(available) + } + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + Timber.d("onPreScroll available $available") + return delegateNestedScrollConnection.onPreScroll(available, source) + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/LoggingTargetedFlingBehavior.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/LoggingTargetedFlingBehavior.kt new file mode 100644 index 0000000000..43b138fad3 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/LoggingTargetedFlingBehavior.kt @@ -0,0 +1,20 @@ +package org.readium.navigator.web.pager + +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.TargetedFlingBehavior +import timber.log.Timber + +internal class LoggingTargetedFlingBehavior( + private val delegate: TargetedFlingBehavior +) : TargetedFlingBehavior { + + override suspend fun ScrollScope.performFling( + initialVelocity: Float, + onRemainingDistanceUpdated: (Float) -> Unit + ): Float { + Timber.d("performFling $initialVelocity") + return with(delegate) { + performFling(initialVelocity, onRemainingDistanceUpdated) + } + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/NavigatorPager.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/NavigatorPager.kt new file mode 100644 index 0000000000..97fb06ddb2 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/NavigatorPager.kt @@ -0,0 +1,45 @@ +package org.readium.navigator.web.pager + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.PagerScope +import androidx.compose.foundation.pager.PagerSnapDistance +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +internal fun NavigatorPager( + modifier: Modifier = Modifier, + state: PagerState, + reverseLayout: Boolean, + beyondViewportPageCount: Int = 2, + key: ((index: Int) -> Any)? = null, + pageContent: @Composable PagerScope.(Int) -> Unit + +) { + val flingBehavior = PagerDefaults.flingBehavior( + state = state, + pagerSnapDistance = PagerSnapDistance.atMost(0) + ) + + HorizontalPager( + modifier = modifier, + // Pages must intercept all scroll gestures so the pager moves + // only through the PagerNestedScrollConnection. + userScrollEnabled = false, + state = state, + beyondViewportPageCount = beyondViewportPageCount, + reverseLayout = reverseLayout, + flingBehavior = flingBehavior, + key = key, + pageNestedScrollConnection = + PagerNestedScrollConnection( + state, + flingBehavior, + Orientation.Horizontal + ), + pageContent = pageContent + ) +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/PagerNestedScrollConnection.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/PagerNestedScrollConnection.kt new file mode 100644 index 0000000000..808aa141f6 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/pager/PagerNestedScrollConnection.kt @@ -0,0 +1,107 @@ +package org.readium.navigator.web.pager + +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.TargetedFlingBehavior +import androidx.compose.foundation.pager.PagerState +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Velocity +import kotlin.math.abs + +internal class PagerNestedScrollConnection( + private val state: PagerState, + private val flingBehavior: TargetedFlingBehavior, + private val orientation: Orientation +) : NestedScrollConnection { + + private var spreadConsumedVertically = false + + private val PagerState.firstVisibleOffset get() = + layoutInfo.visiblePagesInfo.first().offset + + private val PagerState.lastVisibleOffset get() = + layoutInfo.visiblePagesInfo.last().offset + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + if (state.layoutInfo.visiblePagesInfo.size <= 1) { + return Offset.Zero + } + + // rounding and drag only + if (source != NestedScrollSource.UserInput || abs(state.currentPageOffsetFraction) < 1e-6) { + return Offset.Zero + } + + val delta = if (orientation == Orientation.Horizontal) available.x else available.y + + val minBound = -(state.layoutInfo.pageSize + state.firstVisibleOffset).toFloat() + + val maxBound = (state.layoutInfo.pageSize - state.lastVisibleOffset).toFloat() + + val coerced = delta.coerceIn(minBound, maxBound) + // dispatch and return reversed as usual + val consumed = -state.dispatchRawDelta(-coerced) + + check(state.layoutInfo.visiblePagesInfo.size == 1 || abs(consumed - available.x) < 1) + return Offset( + x = if (orientation == Orientation.Horizontal) consumed else available.x, + y = if (orientation == Orientation.Vertical) consumed else available.y + ) + } + + override suspend fun onPreFling(available: Velocity): Velocity { + if (state.layoutInfo.visiblePagesInfo.size > 1) { + state.scroll(scrollPriority = MutatePriority.Default) { + with(flingBehavior) { + performFling(-available.x) + } + } + + return available + } + + return Velocity.Zero + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + if (abs(consumed.y) > 0) { + spreadConsumedVertically = true + } + + if (spreadConsumedVertically) { + return Offset.Zero + } + val consumedX = -state.dispatchRawDelta(-available.x) + return Offset(consumedX, 0f) + } + + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity + ): Velocity { + if (spreadConsumedVertically) { + spreadConsumedVertically = false + return Velocity.Zero + } + + var remaining = available.x + + state.scroll(scrollPriority = MutatePriority.Default) { + with(flingBehavior) { + remaining = -performFling(-available.x) + } + } + + if ((available.x - remaining).isNaN()) { + return available + } + + return Velocity(available.x - remaining, available.y) + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebDefaults.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebDefaults.kt new file mode 100644 index 0000000000..da76c40ecf --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebDefaults.kt @@ -0,0 +1,12 @@ +package org.readium.navigator.web.preferences + +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi + +@ExperimentalReadiumApi +public data class FixedWebDefaults( + val fit: Fit? = null, + val readingProgression: ReadingProgression? = null, + val spreads: Boolean? = null +) diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebPreferences.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebPreferences.kt new file mode 100644 index 0000000000..23ccd68189 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebPreferences.kt @@ -0,0 +1,27 @@ +package org.readium.navigator.web.preferences + +import kotlinx.serialization.Serializable +import org.readium.r2.navigator.preferences.Configurable +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi + +@Serializable +@ExperimentalReadiumApi +public data class FixedWebPreferences( + val fit: Fit? = null, + val readingProgression: ReadingProgression? = null, + val spreads: Boolean? = null +) : Configurable.Preferences { + + init { + require(fit in listOf(null, Fit.CONTAIN, Fit.WIDTH, Fit.HEIGHT)) + } + + override operator fun plus(other: FixedWebPreferences): FixedWebPreferences = + FixedWebPreferences( + fit = other.fit ?: fit, + readingProgression = other.readingProgression ?: readingProgression, + spreads = other.spreads ?: spreads + ) +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebPreferencesEditor.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebPreferencesEditor.kt new file mode 100644 index 0000000000..27f2fd4801 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebPreferencesEditor.kt @@ -0,0 +1,88 @@ +/* + * 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. + */ + +package org.readium.navigator.web.preferences + +import org.readium.r2.navigator.preferences.EnumPreference +import org.readium.r2.navigator.preferences.EnumPreferenceDelegate +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.navigator.preferences.Preference +import org.readium.r2.navigator.preferences.PreferenceDelegate +import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.publication.Metadata + +/** + * Interactive editor of [FixedWebPreferences]. + * + * This can be used as a view model for a user preferences screen. + * + * @see FixedWebPreferences + */ +@ExperimentalReadiumApi +@OptIn(InternalReadiumApi::class) +public class FixedWebPreferencesEditor internal constructor( + initialPreferences: FixedWebPreferences, + publicationMetadata: Metadata, + defaults: FixedWebDefaults +) : PreferencesEditor { + + private data class State( + val preferences: FixedWebPreferences, + val settings: FixedWebSettings + ) + + private val settingsResolver: FixedWebSettingsResolver = + FixedWebSettingsResolver(publicationMetadata, defaults) + + private var state: State = + initialPreferences.toState() + + override val preferences: FixedWebPreferences + get() = state.preferences + + override fun clear() { + updateValues { FixedWebPreferences() } + } + + public val fit: EnumPreference = + EnumPreferenceDelegate( + getValue = { preferences.fit }, + getEffectiveValue = { state.settings.fit }, + getIsEffective = { true }, + updateValue = { value -> updateValues { it.copy(fit = value) } }, + supportedValues = listOf(Fit.CONTAIN, Fit.WIDTH, Fit.HEIGHT) + ) + + public val readingProgression: EnumPreference = + EnumPreferenceDelegate( + getValue = { preferences.readingProgression }, + getEffectiveValue = { state.settings.readingProgression }, + getIsEffective = { true }, + updateValue = { value -> updateValues { it.copy(readingProgression = value) } }, + supportedValues = listOf(ReadingProgression.LTR, ReadingProgression.RTL) + ) + + public val spreads: Preference = + PreferenceDelegate( + getValue = { preferences.spreads }, + getEffectiveValue = { state.settings.spreads }, + getIsEffective = { true }, + updateValue = { value -> updateValues { it.copy(spreads = value) } } + ) + + private fun updateValues( + updater: (FixedWebPreferences) -> FixedWebPreferences + ) { + val newPreferences = updater(preferences) + state = newPreferences.toState() + } + + private fun FixedWebPreferences.toState() = + State(preferences = this, settings = settingsResolver.settings(this)) +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebPreferencesFilters.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebPreferencesFilters.kt new file mode 100644 index 0000000000..c867892649 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebPreferencesFilters.kt @@ -0,0 +1,38 @@ +/* + * 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. + */ + +package org.readium.navigator.web.preferences + +import org.readium.r2.navigator.preferences.PreferencesFilter +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * Suggested filter to keep only shared [FixedWebPreferences]. + */ +@ExperimentalReadiumApi +public object FixedWebSharedPreferencesFilter : PreferencesFilter { + + override fun filter(preferences: FixedWebPreferences): FixedWebPreferences = + preferences.copy( + fit = null, + readingProgression = null, + spreads = null + ) +} + +/** + * Suggested filter to keep only publication-specific [FixedWebPreferences]. + */ +@ExperimentalReadiumApi +public object FixedWebPublicationPreferencesFilter : PreferencesFilter { + + override fun filter(preferences: FixedWebPreferences): FixedWebPreferences = + FixedWebPreferences( + fit = preferences.fit, + readingProgression = preferences.readingProgression, + spreads = preferences.spreads + ) +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebSettings.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebSettings.kt new file mode 100644 index 0000000000..01f11534bc --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebSettings.kt @@ -0,0 +1,13 @@ +package org.readium.navigator.web.preferences + +import org.readium.r2.navigator.preferences.Configurable +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi + +@ExperimentalReadiumApi +public data class FixedWebSettings( + val fit: Fit, + val readingProgression: ReadingProgression, + val spreads: Boolean +) : Configurable.Settings diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebSettingsResolver.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebSettingsResolver.kt new file mode 100644 index 0000000000..1d5c5223d7 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/preferences/FixedWebSettingsResolver.kt @@ -0,0 +1,35 @@ +package org.readium.navigator.web.preferences + +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Metadata +import org.readium.r2.shared.publication.ReadingProgression as PublicationReadingProgression + +@ExperimentalReadiumApi +internal class FixedWebSettingsResolver( + private val metadata: Metadata, + private val defaults: FixedWebDefaults +) { + + fun settings(preferences: FixedWebPreferences): FixedWebSettings { + val readingProgression: ReadingProgression = + preferences.readingProgression + ?: when (metadata.readingProgression) { + PublicationReadingProgression.LTR -> ReadingProgression.LTR + PublicationReadingProgression.RTL -> ReadingProgression.RTL + else -> null + } ?: defaults.readingProgression + ?: ReadingProgression.LTR + + val fit = preferences.fit ?: defaults.fit ?: Fit.CONTAIN + + val spreads = preferences.spreads ?: defaults.spreads ?: false + + return FixedWebSettings( + fit = fit, + readingProgression = readingProgression, + spreads = spreads + ) + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/DoubleViewportSpread.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/DoubleViewportSpread.kt new file mode 100644 index 0000000000..bbf4adf591 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/DoubleViewportSpread.kt @@ -0,0 +1,102 @@ +@file:OptIn(ExperimentalReadiumApi::class) + +package org.readium.navigator.web.spread + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.readium.navigator.common.LinkContext +import org.readium.navigator.common.TapEvent +import org.readium.navigator.web.layout.DoubleViewportSpread +import org.readium.navigator.web.util.DisplayArea +import org.readium.navigator.web.util.WebViewClient +import org.readium.navigator.web.webapi.FixedDoubleApi +import org.readium.navigator.web.webview.rememberWebViewStateWithHTMLData +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Url + +@Composable +internal fun DoubleViewportSpread( + onTap: (TapEvent) -> Unit, + onLinkActivated: (Url, LinkContext?) -> Unit, + state: DoubleSpreadState, + backgroundColor: Color +) { + Box( + modifier = Modifier.fillMaxSize(), + propagateMinConstraints = true + ) { + val webViewState = rememberWebViewStateWithHTMLData( + data = state.htmlData, + baseUrl = state.publicationBaseUrl.toString() + ) + + val scriptsLoaded = remember(webViewState.webView) { + mutableStateOf(false) + } + + val layoutApi = remember(webViewState.webView, scriptsLoaded.value) { + webViewState.webView + .takeIf { scriptsLoaded.value } + ?.let { FixedDoubleApi(it) } + } + + layoutApi?.let { api -> + LaunchedEffect(api) { + snapshotFlow { + state.fit.value + }.onEach { + api.setFit(state.fit.value) + }.launchIn(this) + + snapshotFlow { + state.displayArea.value + }.onEach { + api.setDisplayArea(it) + }.launchIn(this) + + api.loadSpread(state.spread) + } + } + + SpreadWebView( + state = webViewState, + client = state.webViewClient, + onTap = onTap, + onLinkActivated = { url, context -> + onLinkActivated( + state.publicationBaseUrl.relativize(url), + context + ) + }, + backgroundColor = backgroundColor, + onScriptsLoaded = { scriptsLoaded.value = true } + ) + } +} + +internal class DoubleSpreadState( + val htmlData: String, + val publicationBaseUrl: AbsoluteUrl, + val webViewClient: WebViewClient, + val spread: DoubleViewportSpread, + val fit: State, + val displayArea: State +) { + val left: AbsoluteUrl? = + spread.leftPage?.let { publicationBaseUrl.resolve(it.href) } + + val right: AbsoluteUrl? = + spread.rightPage?.let { publicationBaseUrl.resolve(it.href) } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/SingleViewportSpread.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/SingleViewportSpread.kt new file mode 100644 index 0000000000..a45807feaa --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/SingleViewportSpread.kt @@ -0,0 +1,99 @@ +@file:OptIn(ExperimentalReadiumApi::class) + +package org.readium.navigator.web.spread + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.readium.navigator.common.LinkContext +import org.readium.navigator.common.TapEvent +import org.readium.navigator.web.layout.SingleViewportSpread +import org.readium.navigator.web.util.DisplayArea +import org.readium.navigator.web.util.WebViewClient +import org.readium.navigator.web.webapi.FixedSingleApi +import org.readium.navigator.web.webview.rememberWebViewStateWithHTMLData +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Url + +@Composable +internal fun SingleViewportSpread( + onTap: (TapEvent) -> Unit, + onLinkActivated: (Url, LinkContext?) -> Unit, + state: SingleSpreadState, + backgroundColor: Color +) { + Box( + modifier = Modifier.fillMaxSize(), + propagateMinConstraints = true + ) { + val webViewState = rememberWebViewStateWithHTMLData( + data = state.htmlData, + baseUrl = state.publicationBaseUrl.toString() + ) + + val scriptsLoaded = remember(webViewState.webView) { + mutableStateOf(false) + } + + val layoutApi = remember(webViewState.webView, scriptsLoaded.value) { + webViewState.webView + .takeIf { scriptsLoaded.value } + ?.let { FixedSingleApi(it) } + } + + layoutApi?.let { api -> + LaunchedEffect(api) { + snapshotFlow { + state.fit.value + }.onEach { + api.setFit(it) + }.launchIn(this) + + snapshotFlow { + state.displayArea.value + }.onEach { + api.setDisplayArea(it) + }.launchIn(this) + + api.loadSpread(state.spread) + } + } + + SpreadWebView( + state = webViewState, + client = state.webViewClient, + onTap = onTap, + onLinkActivated = { url, context -> + onLinkActivated( + state.publicationBaseUrl.relativize(url), + context + ) + }, + backgroundColor = backgroundColor, + onScriptsLoaded = { scriptsLoaded.value = true } + ) + } +} + +internal class SingleSpreadState( + val htmlData: String, + val publicationBaseUrl: AbsoluteUrl, + val webViewClient: WebViewClient, + val spread: SingleViewportSpread, + val fit: State, + val displayArea: State +) { + val url: AbsoluteUrl = + publicationBaseUrl.resolve(spread.page.href) +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/SpreadNestedScrollConnection.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/SpreadNestedScrollConnection.kt new file mode 100644 index 0000000000..908e526cdb --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/SpreadNestedScrollConnection.kt @@ -0,0 +1,52 @@ +package org.readium.navigator.web.spread + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Velocity +import kotlin.math.abs +import org.readium.navigator.web.webview.RelaxedWebView +import org.readium.navigator.web.webview.WebViewScrollable2DState + +internal class SpreadNestedScrollConnection( + private val webviewState: WebViewScrollable2DState +) : NestedScrollConnection { + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val webViewNow = webviewState.webView ?: return Offset.Zero + + // For some reason, scrollX can vary by 1 or 2 pixels without any call to scrollTo. + val webViewCannotScrollHorizontally = + (webViewNow.scrollX < 3 && available.x > 0) || + ((webViewNow.maxScrollX - webViewNow.scrollX) < 3 && available.x < 0) + + if (webViewCannotScrollHorizontally) { + snapWebview(webViewNow) + } + + val isGestureHorizontal = + (abs(available.y) / abs(available.x)) < 0.58 // tan(Pi/6) + + return if (webViewCannotScrollHorizontally && isGestureHorizontal) { + // If the gesture is mostly horizontal and the spread has nothing to consume horizontally, + // we consume everything vertically. + Offset(0f, available.y) + } else { + Offset.Zero + } + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + val webViewNow = webviewState.webView ?: return Velocity.Zero + snapWebview(webViewNow) + return Velocity.Zero + } + + private fun snapWebview(webview: RelaxedWebView) { + if ((webview.maxScrollX - webview.scrollX) < 15) { + webview.scrollTo(webview.maxScrollX, webview.scrollY) + } else if (webview.scrollX in (0 until 15)) { + webview.scrollTo(0, webview.scrollY) + } + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/SpreadWebView.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/SpreadWebView.kt new file mode 100644 index 0000000000..ae1b078a97 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/spread/SpreadWebView.kt @@ -0,0 +1,83 @@ +package org.readium.navigator.web.spread + +import android.annotation.SuppressLint +import android.view.View +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.DpOffset +import org.readium.navigator.common.LinkContext +import org.readium.navigator.common.TapEvent +import org.readium.navigator.web.util.WebViewClient +import org.readium.navigator.web.webapi.GesturesApi +import org.readium.navigator.web.webapi.GesturesListener +import org.readium.navigator.web.webapi.InitializationApi +import org.readium.navigator.web.webview.WebView +import org.readium.navigator.web.webview.WebViewScrollable2DState +import org.readium.navigator.web.webview.WebViewState +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.AbsoluteUrl + +@OptIn(ExperimentalReadiumApi::class) +@SuppressLint("SetJavaScriptEnabled") +@Composable +internal fun SpreadWebView( + state: WebViewState, + client: WebViewClient, + onScriptsLoaded: () -> Unit, + onTap: (TapEvent) -> Unit, + onLinkActivated: (AbsoluteUrl, LinkContext?) -> Unit, + backgroundColor: Color +) { + val scrollableState = remember { WebViewScrollable2DState() } + + val spreadNestedScrollConnection = SpreadNestedScrollConnection(scrollableState) + + val initializationApi = remember(onScriptsLoaded) { + InitializationApi(onScriptsLoaded) + } + + val gesturesApi = remember(onTap) { + val listener = object : GesturesListener { + override fun onTap(offset: DpOffset) { + onTap(TapEvent(offset)) + } + + override fun onLinkActivated(href: AbsoluteUrl) { + onLinkActivated(href, null) + } + } + GesturesApi(listener) + } + + LaunchedEffect(state.webView) { + state.webView?.let { initializationApi.registerOnWebView(it) } + state.webView?.let { gesturesApi.registerOnWebView(it) } + } + + WebView( + modifier = Modifier + .fillMaxSize() + .nestedScroll(spreadNestedScrollConnection), + state = state, + client = client, + scrollableState = scrollableState, + onCreated = { webview -> + webview.settings.javaScriptEnabled = true + webview.settings.setSupportZoom(true) + webview.settings.builtInZoomControls = true + webview.settings.displayZoomControls = false + webview.settings.loadWithOverviewMode = true + webview.settings.useWideViewPort = true + webview.isVerticalScrollBarEnabled = false + webview.isHorizontalScrollBarEnabled = false + webview.setBackgroundColor(backgroundColor.toArgb()) + webview.setLayerType(View.LAYER_TYPE_HARDWARE, null) + } + ) +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/util/DisplayArea.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/util/DisplayArea.kt new file mode 100644 index 0000000000..18443b558e --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/util/DisplayArea.kt @@ -0,0 +1,16 @@ +package org.readium.navigator.web.util + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize + +internal data class DisplayArea( + val viewportSize: DpSize, + val safeDrawingPadding: AbsolutePaddingValues +) + +internal data class AbsolutePaddingValues( + val top: Dp, + val right: Dp, + val bottom: Dp, + val left: Dp +) diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/util/HtmlInjector.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/util/HtmlInjector.kt new file mode 100644 index 0000000000..94a1951c4f --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/util/HtmlInjector.kt @@ -0,0 +1,71 @@ +/* + * 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. + */ + +package org.readium.navigator.web.util + +import org.readium.navigator.web.webapi.GesturesApi +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.services.isProtected +import org.readium.r2.shared.util.AbsoluteUrl +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 timber.log.Timber + +/** + * Injects scripts in the HTML [Resource] receiver. + * + * @param baseHref Base URL where and scripts are served. + */ +internal fun Resource.injectHtml( + publication: Publication, + mediaType: MediaType, + baseHref: AbsoluteUrl, + disableSelectionWhenProtected: Boolean +): Resource = + TransformingResource(this) { bytes -> + if (!mediaType.isHtml) { + return@TransformingResource Try.success(bytes) + } + + var content = bytes.toString(mediaType.charset ?: Charsets.UTF_8).trim() + val injectables = mutableListOf() + + injectables.add( + script(baseHref.resolve(GesturesApi.path)) + ) + + // Disable the text selection if the publication is protected. + // FIXME: This is a hack until proper LCP copy is implemented, see https://github.com/readium/kotlin-toolkit/issues/221 + if (disableSelectionWhenProtected && publication.isProtected) { + injectables.add( + """ + + """ + ) + } + + val headEndIndex = content.indexOf("", 0, true) + if (headEndIndex == -1) { + Timber.e(" closing tag not found in resource with href: $sourceUrl") + } else { + content = StringBuilder(content) + .insert(headEndIndex, "\n" + injectables.joinToString("\n") + "\n") + .toString() + } + + Try.success(content.toByteArray()) + } + +private fun script(src: Url): String = + """""" diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/util/WebViewClient.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/util/WebViewClient.kt new file mode 100644 index 0000000000..7d2568110b --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/util/WebViewClient.kt @@ -0,0 +1,17 @@ +package org.readium.navigator.web.util + +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView + +internal class WebViewClient( + private val webViewServer: WebViewServer +) : android.webkit.WebViewClient() { + + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest + ): WebResourceResponse? { + return webViewServer.shouldInterceptRequest(request) + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/util/WebViewServer.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/util/WebViewServer.kt new file mode 100644 index 0000000000..7633ff50db --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/util/WebViewServer.kt @@ -0,0 +1,170 @@ +/* + * 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. + */ + +@file:OptIn(InternalReadiumApi::class) + +package org.readium.navigator.web.util + +import android.app.Application +import android.os.PatternMatcher +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import androidx.webkit.WebViewAssetLoader +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.publication.Href +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.asInputStream +import org.readium.r2.shared.util.http.HttpHeaders +import org.readium.r2.shared.util.http.HttpRange +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.StringResource +import org.readium.r2.shared.util.resource.fallback + +/** + * Serves the publication resources and application assets in the EPUB navigator web views. + */ +internal class WebViewServer( + private val application: Application, + private val publication: Publication, + servedAssets: List, + private val disableSelectionWhenProtected: Boolean, + private val onResourceLoadFailed: (Url, ReadError) -> Unit +) { + companion object { + val publicationBaseHref = AbsoluteUrl("https://readium/publication/")!! + val assetsBaseHref = AbsoluteUrl("https://readium/assets/")!! + + fun assetUrl(path: String): AbsoluteUrl? = + Url.fromDecodedPath(path)?.let { assetsBaseHref.resolve(it) } + } + + /** + * Serves the requests of the navigator web views. + * + * https://readium/publication/ serves the publication resources through its fetcher. + * https://readium/assets/ serves the application assets. + */ + fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? { + if (request.url.host != "readium") return null + val path = request.url.path ?: return null + + return when { + path.startsWith("/publication/") -> { + val href = Url.fromDecodedPath(path.removePrefix("/publication/")) + ?: return null + + servePublicationResource( + href = href, + range = HttpHeaders(request.requestHeaders).range + ) + } + path.startsWith("/assets/") && isServedAsset(path.removePrefix("/assets/")) -> { + assetsLoader.shouldInterceptRequest(request.url) + } + else -> null + } + } + + /** + * Returns a new [Resource] to serve the given [href] in the publication. + * + * If the [Resource] is an HTML document, injects the required JavaScript and CSS files. + */ + private fun servePublicationResource(href: Url, range: HttpRange?): WebResourceResponse { + val link = publication.linkWithHref(href) + // Query parameters must be kept as they might be relevant for the fetcher. + ?.copy(href = Href(href)) + ?: Link(href = href) + + // Drop anchor because it is meant to be interpreted by the client. + val urlWithoutAnchor = href.removeFragment() + + var resource = publication + .get(urlWithoutAnchor) + ?.fallback { + onResourceLoadFailed(urlWithoutAnchor, it) + errorResource() + } ?: run { + val error = ReadError.Decoding( + "Resource not found at $urlWithoutAnchor in publication." + ) + onResourceLoadFailed(urlWithoutAnchor, error) + errorResource() + } + + link.mediaType + ?.takeIf { it.isHtml } + ?.let { + resource = resource.injectHtml( + publication, + mediaType = it, + baseHref = assetsBaseHref, + disableSelectionWhenProtected = disableSelectionWhenProtected + ) + } + + val headers = mutableMapOf( + "Accept-Ranges" to "bytes" + ) + + if (range == null) { + return WebResourceResponse( + link.mediaType?.toString(), + null, + 200, + "OK", + headers, + resource.asInputStream() + ) + } else { // Byte range request + val stream = resource.asInputStream() + val length = stream.available() + val longRange = range.toLongRange(length.toLong()) + headers["Content-Range"] = "bytes ${longRange.first}-${longRange.last}/$length" + // Content-Length will automatically be filled by the WebView using the Content-Range header. + // headers["Content-Length"] = (longRange.last - longRange.first + 1).toString() + // Weirdly, the WebView will call itself stream.skip to skip to the requested range. + return WebResourceResponse( + link.mediaType?.toString(), + null, + 206, + "Partial Content", + headers, + stream + ) + } + } + private fun errorResource(): Resource = + StringResource { + withContext(Dispatchers.IO) { + Try.success( + application.assets + .open("readium/error.xhtml") + .bufferedReader() + .use { it.readText() } + ) + } + } + + private fun isServedAsset(path: String): Boolean = + servedAssetPatterns.any { it.match(path) } + + private val servedAssetPatterns: List = + servedAssets.map { PatternMatcher(it, PatternMatcher.PATTERN_SIMPLE_GLOB) } + + private val assetsLoader = + WebViewAssetLoader.Builder() + .setDomain("readium") + .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(application)) + .build() +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/FixedDoubleApi.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/FixedDoubleApi.kt new file mode 100644 index 0000000000..493f74b776 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/FixedDoubleApi.kt @@ -0,0 +1,55 @@ +package org.readium.navigator.web.webapi + +import android.content.res.AssetManager +import android.webkit.WebView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.navigator.web.layout.DoubleViewportSpread +import org.readium.navigator.web.util.DisplayArea +import org.readium.navigator.web.util.WebViewServer +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.AbsoluteUrl + +@OptIn(ExperimentalReadiumApi::class) +internal class FixedDoubleApi( + private val webView: WebView +) { + + companion object { + + suspend fun getPageContent(assetManager: AssetManager, assetsUrl: AbsoluteUrl): String = + withContext(Dispatchers.IO) { + assetManager.open("readium/navigators/web/fixed-double-index.html") + .bufferedReader() + .use { it.readText() } + .replace("{{ASSETS_URL}}", assetsUrl.toString()) + } + } + + fun loadSpread(spread: DoubleViewportSpread) { + val leftUrl = spread.leftPage?.let { WebViewServer.publicationBaseHref.resolve(it.href) } + val rightUrl = spread.rightPage?.let { WebViewServer.publicationBaseHref.resolve(it.href) } + val argument = buildList { + leftUrl?.let { add("left: `$it`") } + rightUrl?.let { add("right: `$it`") } + }.joinToString(separator = ", ", prefix = "{ ", postfix = " }") + webView.evaluateJavascript("doubleArea.loadSpread($argument);") {} + } + + fun setDisplayArea(displayArea: DisplayArea) { + val width = displayArea.viewportSize.width.value + val height = displayArea.viewportSize.height.value + val top = displayArea.safeDrawingPadding.top.value + val right = displayArea.safeDrawingPadding.right.value + val bottom = displayArea.safeDrawingPadding.bottom.value + val left = displayArea.safeDrawingPadding.left.value + val script = "doubleArea.setViewport($width, $height, $top, $right, $bottom, $left);" + webView.evaluateJavascript(script) {} + } + + fun setFit(fit: Fit) { + val script = "doubleArea.setFit(`${fit.value}`);" + webView.evaluateJavascript(script) {} + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/FixedSingleApi.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/FixedSingleApi.kt new file mode 100644 index 0000000000..0d5dcf0297 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/FixedSingleApi.kt @@ -0,0 +1,51 @@ +package org.readium.navigator.web.webapi + +import android.content.res.AssetManager +import android.webkit.WebView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.navigator.web.layout.SingleViewportSpread +import org.readium.navigator.web.util.DisplayArea +import org.readium.navigator.web.util.WebViewServer +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.AbsoluteUrl + +@OptIn(ExperimentalReadiumApi::class) +internal class FixedSingleApi( + private val webView: WebView +) { + + companion object { + + suspend fun getPageContent(assetManager: AssetManager, assetsUrl: AbsoluteUrl): String = + withContext(Dispatchers.IO) { + assetManager.open("readium/navigators/web/fixed-single-index.html") + .bufferedReader() + .use { it.readText() } + .replace("{{ASSETS_URL}}", assetsUrl.toString()) + } + } + + fun loadSpread(spread: SingleViewportSpread) { + val resourceUrl = WebViewServer.publicationBaseHref.resolve(spread.page.href) + val script = "singleArea.loadResource(`$resourceUrl`);" + webView.evaluateJavascript(script) {} + } + + fun setDisplayArea(displayArea: DisplayArea) { + val width = displayArea.viewportSize.width.value + val height = displayArea.viewportSize.height.value + val top = displayArea.safeDrawingPadding.top.value + val right = displayArea.safeDrawingPadding.right.value + val bottom = displayArea.safeDrawingPadding.bottom.value + val left = displayArea.safeDrawingPadding.left.value + val script = "singleArea.setViewport($width, $height, $top, $right, $bottom, $left);" + webView.evaluateJavascript(script) {} + } + + fun setFit(fit: Fit) { + val script = "singleArea.setFit(`${fit.value}`);" + webView.evaluateJavascript(script) {} + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/GesturesApi.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/GesturesApi.kt new file mode 100644 index 0000000000..29b8cfec02 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/GesturesApi.kt @@ -0,0 +1,50 @@ +package org.readium.navigator.web.webapi + +import android.webkit.WebView +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.RelativeUrl +import timber.log.Timber + +internal interface GesturesListener { + + fun onTap(offset: DpOffset) + fun onLinkActivated(href: AbsoluteUrl) +} + +internal class GesturesApi( + private val listener: GesturesListener +) { + + companion object { + + val path: RelativeUrl = + RelativeUrl("readium/navigators/web/fixed-injectable-script.js")!! + } + + fun registerOnWebView(webView: WebView) { + webView.addJavascriptInterface(this, "gestures") + } + + @android.webkit.JavascriptInterface + fun onTap(eventJson: String) { + Timber.d("onTap start $eventJson") + val tapEvent = Json.decodeFromString(eventJson) + listener.onTap(DpOffset(tapEvent.x.dp, tapEvent.y.dp)) + } + + @android.webkit.JavascriptInterface + fun onLinkActivated(href: String) { + val url = AbsoluteUrl(href) ?: return + listener.onLinkActivated(url) + } +} + +@Serializable +private data class JsonTapEvent( + val x: Float, + val y: Float +) diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/InitializationApi.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/InitializationApi.kt new file mode 100644 index 0000000000..417bab5d8e --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/webapi/InitializationApi.kt @@ -0,0 +1,18 @@ +package org.readium.navigator.web.webapi + +import android.webkit.JavascriptInterface +import android.webkit.WebView + +internal class InitializationApi( + private val onScriptsLoadedDelegate: () -> Unit +) { + + fun registerOnWebView(webView: WebView) { + webView.addJavascriptInterface(this, "initialization") + } + + @JavascriptInterface + fun onScriptsLoaded() { + onScriptsLoadedDelegate.invoke() + } +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/webview/ComposableWebView.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/webview/ComposableWebView.kt new file mode 100644 index 0000000000..40d279a1ab --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/webview/ComposableWebView.kt @@ -0,0 +1,337 @@ +package org.readium.navigator.web.webview + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup.LayoutParams +import android.webkit.WebChromeClient +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.FrameLayout +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.ViewCompat +import org.readium.navigator.web.gestures.Fling2DBehavior +import org.readium.navigator.web.gestures.scrollable2D +import org.readium.navigator.web.webview.LoadingState.Finished +import org.readium.navigator.web.webview.LoadingState.Loading +import timber.log.Timber + +/** + * A wrapper around the Android View WebView to provide a basic WebView composable. + * + * If you require more customisation you are most likely better rolling your own and using this + * wrapper as an example. + * + * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it + * is incorrectly sizing, use the layoutParams composable function instead. + * + * @param state The webview state holder where the Uri to load is defined. + * @param modifier A compose modifier + * @param onCreated Called when the WebView is first created, this can be used to set additional + * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be + * subsequently overwritten after this lambda is called. + * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved + * if you need to save and restore state in this WebView. + * @param client Provides access to WebViewClient via subclassing + * @param chromeClient Provides access to WebChromeClient via subclassing + * @param factory An optional WebView factory for using a custom subclass of WebView + */ +@Composable +internal fun WebView( + state: WebViewState, + modifier: Modifier = Modifier, + scrollableState: WebViewScrollable2DState = remember { WebViewScrollable2DState() }, + flingBehavior: Fling2DBehavior? = null, + onCreated: (WebView) -> Unit = {}, + onDispose: (WebView) -> Unit = {}, + client: WebViewClient = remember { WebViewClient() }, + chromeClient: WebChromeClient = remember { WebChromeClient() }, + factory: ((Context) -> RelaxedWebView)? = null +) { + BoxWithConstraints( + modifier = modifier, + propagateMinConstraints = true + ) { + // WebView changes it's layout strategy based on + // it's layoutParams. We convert from Compose Modifier to + // layout params here. + val width = + if (constraints.hasFixedWidth) { + LayoutParams.MATCH_PARENT + } else { + LayoutParams.WRAP_CONTENT + } + val height = + if (constraints.hasFixedHeight) { + LayoutParams.MATCH_PARENT + } else { + LayoutParams.WRAP_CONTENT + } + + val layoutParams = FrameLayout.LayoutParams( + width, + height + ) + + Timber.d("constraints ${constraints.maxWidth} ${constraints.maxHeight}") + + LazyRow( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + userScrollEnabled = false, + modifier = Modifier.fillMaxSize() + ) { + item { + WebView( + state, + scrollableState, + flingBehavior, + layoutParams, + Modifier.fillParentMaxSize(), + onCreated, + onDispose, + client, + chromeClient, + factory + ) + } + } + } +} + +/** + * A wrapper around the Android View WebView to provide a basic WebView composable. + * + * If you require more customisation you are most likely better rolling your own and using this + * wrapper as an example. + * + * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it + * is incorrectly sizing, use the layoutParams composable function instead. + * + * @param state The webview state holder where the Uri to load is defined. + * @param layoutParams A FrameLayout.LayoutParams object to custom size the underlying WebView. + * @param modifier A compose modifier + * @param onCreated Called when the WebView is first created, this can be used to set additional + * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be + * subsequently overwritten after this lambda is called. + * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved + * if you need to save and restore state in this WebView. + * @param client Provides access to WebViewClient via subclassing + * @param chromeClient Provides access to WebChromeClient via subclassing + * @param factory An optional WebView factory for using a custom subclass of WebView + */ +@Composable +internal fun WebView( + state: WebViewState, + scrollableState: WebViewScrollable2DState, + flingBehavior: Fling2DBehavior?, + layoutParams: FrameLayout.LayoutParams, + modifier: Modifier = Modifier, + onCreated: (WebView) -> Unit = {}, + onDispose: (WebView) -> Unit = {}, + client: WebViewClient = remember { WebViewClient() }, + chromeClient: WebChromeClient = remember { WebChromeClient() }, + factory: ((Context) -> RelaxedWebView)? = null +) { + val webView = state.webView + + webView?.let { wv -> + LaunchedEffect(wv, state) { + snapshotFlow { state.content }.collect { content -> + when (content) { + is WebContent.Url -> { + wv.loadUrl(content.url, content.additionalHttpHeaders) + } + + is WebContent.Data -> { + wv.loadDataWithBaseURL( + content.baseUrl, + content.data, + content.mimeType, + content.encoding, + content.historyUrl + ) + } + } + } + } + } + + AndroidView( + factory = { context -> + (factory?.let { it(context) } ?: RelaxedWebView(context)).apply { + onCreated(this) + ViewCompat.setNestedScrollingEnabled(this, true) + + this.layoutParams = layoutParams + + state.viewState?.let { + this.restoreState(it) + } + + webChromeClient = chromeClient + webViewClient = client + scrollableState.webView = this + }.also { state.webView = it } + }, + modifier = modifier + .scrollable2D(scrollableState, flingBehavior = flingBehavior), + onRelease = { + onDispose(it) + scrollableState.webView = null + state.webView = null + } + ) +} + +internal sealed class WebContent { + internal data class Url( + val url: String, + val additionalHttpHeaders: Map = emptyMap() + ) : WebContent() + + internal data class Data( + val data: String, + val baseUrl: String? = null, + val encoding: String = "utf-8", + val mimeType: String? = null, + val historyUrl: String? = null + ) : WebContent() +} + +/** + * Sealed class for constraining possible loading states. + * See [Loading] and [Finished]. + */ +internal sealed class LoadingState { + /** + * Describes a WebView that has not yet loaded for the first time. + */ + internal data object Initializing : LoadingState() + + /** + * Describes a webview between `onPageStarted` and `onPageFinished` events. + */ + internal data object Loading : LoadingState() + + /** + * Describes a webview that has finished loading content. + */ + internal data object Finished : LoadingState() +} + +/** + * A state holder to hold the state for the WebView. In most cases this will be remembered + * using the rememberWebViewState(uri) function. + */ +@Stable +internal class WebViewState(webContent: WebContent) { + /** + * The content being loaded by the WebView + */ + var content: WebContent by mutableStateOf(webContent) + + /** + * A list for errors captured in the last load. Reset when a new page is loaded. + * Errors could be from any resource (iframe, image, etc.), not just for the main page. + * For more fine grained control use the OnError callback of the WebView. + */ + internal val errorsForCurrentRequest: SnapshotStateList = mutableStateListOf() + + /** + * The saved view state from when the view was destroyed last. To restore state, + * use the navigator and only call loadUrl if the bundle is null. + * See WebViewSaveStateSample. + */ + internal var viewState: Bundle? = null + + // We need access to this in the state saver. An internal DisposableEffect or AndroidView + // onDestroy is called after the state saver and so can't be used. + internal var webView by mutableStateOf(null) +} + +/** + * A wrapper class to hold errors from the WebView. + */ +@Immutable +internal data class WebViewError( + /** + * The request the error came from. + */ + val request: WebResourceRequest?, + /** + * The error that was reported. + */ + val error: WebResourceError +) + +/** + * Creates a WebView state that is remembered across Compositions. + * + * @param url The url to load in the WebView + * @param additionalHttpHeaders Optional, additional HTTP headers that are passed to [WebView.loadUrl]. + * Note that these headers are used for all subsequent requests of the WebView. + */ +@Composable +internal fun rememberWebViewState( + url: String, + additionalHttpHeaders: Map = emptyMap() +): WebViewState = + // Rather than using .apply {} here we will recreate the state, this prevents + // a recomposition loop when the webview updates the url itself. + remember { + WebViewState( + WebContent.Url( + url = url, + additionalHttpHeaders = additionalHttpHeaders + ) + ) + }.apply { + this.content = WebContent.Url( + url = url, + additionalHttpHeaders = additionalHttpHeaders + ) + } + +/** + * Creates a WebView state that is remembered across Compositions. + * + * @param data The uri to load in the WebView + */ +@Composable +internal fun rememberWebViewStateWithHTMLData( + data: String, + baseUrl: String? = null, + encoding: String = "utf-8", + mimeType: String? = null, + historyUrl: String? = null +): WebViewState = + remember { + WebViewState(WebContent.Data(data, baseUrl, encoding, mimeType, historyUrl)) + }.apply { + this.content = WebContent.Data( + data, + baseUrl, + encoding, + mimeType, + historyUrl + ) + } diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/webview/RelaxedWebView.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/webview/RelaxedWebView.kt new file mode 100644 index 0000000000..ce6b900bf3 --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/webview/RelaxedWebView.kt @@ -0,0 +1,25 @@ +package org.readium.navigator.web.webview + +import android.content.Context +import android.webkit.WebView + +internal class RelaxedWebView(context: Context) : WebView(context) { + + val maxScrollX: Int get() = + horizontalScrollRange - horizontalScrollExtent + + val maxScrollY: Int get() = + verticalScrollRange - verticalScrollExtent + + val verticalScrollRange: Int get() = + computeVerticalScrollRange() + + val horizontalScrollRange: Int get() = + computeHorizontalScrollRange() + + val verticalScrollExtent: Int get() = + computeVerticalScrollExtent() + + val horizontalScrollExtent: Int get() = + computeHorizontalScrollExtent() +} diff --git a/readium/navigators/web/src/main/java/org/readium/navigator/web/webview/WebViewScrollable2DState.kt b/readium/navigators/web/src/main/java/org/readium/navigator/web/webview/WebViewScrollable2DState.kt new file mode 100644 index 0000000000..1ba506941a --- /dev/null +++ b/readium/navigators/web/src/main/java/org/readium/navigator/web/webview/WebViewScrollable2DState.kt @@ -0,0 +1,58 @@ +package org.readium.navigator.web.webview + +import androidx.compose.ui.geometry.Offset +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.sign +import org.readium.navigator.web.gestures.DefaultScrollable2DState +import org.readium.navigator.web.gestures.Scrollable2DState + +internal class WebViewScrollable2DState private constructor( + private val webViewDeltaDispatcher: WebViewDeltaDispatcher +) : Scrollable2DState by DefaultScrollable2DState(webViewDeltaDispatcher::onDelta) { + + constructor() : this(WebViewDeltaDispatcher()) + + var webView: RelaxedWebView? + get() = + webViewDeltaDispatcher.webView + set(value) { + webViewDeltaDispatcher.webView = value + } +} + +private class WebViewDeltaDispatcher { + + var webView: RelaxedWebView? = null + + fun onDelta(delta: Offset): Offset { + val webViewNow = webView ?: return Offset.Zero + + val currentX = webViewNow.scrollX + val currentY = webViewNow.scrollY + val maxX = webViewNow.maxScrollX + val maxY = webViewNow.maxScrollY + + // Consume slightly more than delta si we have to because + // we don't want the pager to consume any rounding error + val newX = (currentX - sign(delta.x) * ceil(abs(delta.x))).toInt().coerceIn(0, maxX) + val newY = (currentY - sign(delta.y) * ceil(abs(delta.y))).toInt().coerceIn(0, maxY) + webViewNow.scrollTo(newX, newY) + + // Fake that we never consume more than delta + val consumedX = (currentX - webViewNow.scrollX).toFloat().coerceAbsAtMost(abs(delta.x)) + val consumedY = (currentY - webViewNow.scrollY).toFloat().coerceAbsAtMost(abs(delta.y)) + val consumed = Offset(consumedX, consumedY) + + return consumed + } + + private fun Float.coerceAbsAtMost(maxValue: Float): Float { + require(maxValue >= 0) + return if (this > 0) { + this.coerceAtMost(maxValue) + } else { + this.coerceAtLeast(-maxValue) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 3d18b8fd12..62ea973616 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,6 +41,18 @@ include(":readium:navigator") project(":readium:navigator") .name = "readium-navigator" +include(":readium:navigators:common") +project(":readium:navigators:common") + .name = "readium-navigator-common" + +include(":readium:navigators:web") +project(":readium:navigators:web") + .name = "readium-navigator-web" + +include(":readium:navigators:pdf") +project(":readium:navigators:pdf") + .name = "readium-navigator-pdf" + include(":readium:navigators:media:common") project(":readium:navigators:media:common") .name = "readium-navigator-media-common" @@ -74,3 +86,4 @@ project(":readium:streamer") .name = "readium-streamer" include("test-app") +include(":readium:navigators:demo")