├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── r2-navigator-kotlin.iml ├── r2-navigator ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── _scripts │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── .prettierrc.json │ │ ├── README.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── src │ │ │ ├── decorator.js │ │ │ ├── gestures.js │ │ │ ├── highlight.js │ │ │ ├── index-fixed.js │ │ │ ├── index-reflowable.js │ │ │ ├── index.js │ │ │ ├── rect.js │ │ │ ├── selection.js │ │ │ ├── utils.js │ │ │ └── vendor │ │ │ │ └── hypothesis │ │ │ │ ├── README.md │ │ │ │ └── anchoring │ │ │ │ ├── html.js │ │ │ │ ├── match-quote.js │ │ │ │ ├── pdf.js │ │ │ │ ├── placeholder.js │ │ │ │ ├── test │ │ │ │ ├── fake-pdf-viewer-application.js │ │ │ │ ├── html-anchoring-fixture.html │ │ │ │ ├── html-baselines │ │ │ │ │ ├── index.js │ │ │ │ │ ├── minimal.html │ │ │ │ │ ├── minimal.json │ │ │ │ │ ├── wikipedia-regression-testing.html │ │ │ │ │ └── wikipedia-regression-testing.json │ │ │ │ ├── html-test.js │ │ │ │ ├── match-quote-test.js │ │ │ │ ├── pdf-test.js │ │ │ │ ├── placeholder-test.js │ │ │ │ ├── text-range-test.js │ │ │ │ ├── types-test.js │ │ │ │ └── xpath-test.js │ │ │ │ ├── text-range.js │ │ │ │ ├── types.js │ │ │ │ └── xpath.js │ │ └── webpack.config.js │ └── readium │ │ ├── divina │ │ ├── divinaPlayer.html │ │ ├── divinaPlayer.js │ │ └── divinaTouchHandling.js │ │ ├── fonts │ │ └── OpenDyslexic-Regular.otf │ │ ├── readium-css │ │ ├── ReadMe.md │ │ ├── ReadiumCSS-after.css │ │ ├── ReadiumCSS-before.css │ │ ├── ReadiumCSS-default.css │ │ ├── ReadiumCSS-ebpaj_fonts_patch.css │ │ ├── cjk-horizontal │ │ │ ├── ReadiumCSS-after.css │ │ │ ├── ReadiumCSS-before.css │ │ │ └── ReadiumCSS-default.css │ │ ├── cjk-vertical │ │ │ ├── ReadiumCSS-after.css │ │ │ ├── ReadiumCSS-before.css │ │ │ └── ReadiumCSS-default.css │ │ ├── fonts │ │ │ ├── AccessibleDfA.otf │ │ │ ├── LICENSE-AccessibleDfa │ │ │ ├── LICENSE-IaWriterDuospace.md │ │ │ └── iAWriterDuospace-Regular.ttf │ │ └── rtl │ │ │ ├── ReadiumCSS-after.css │ │ │ ├── ReadiumCSS-before.css │ │ │ └── ReadiumCSS-default.css │ │ └── scripts │ │ ├── readium-fixed.js │ │ └── readium-reflowable.js │ ├── java │ └── org │ │ └── readium │ │ └── r2 │ │ └── navigator │ │ ├── DecorableNavigator.kt │ │ ├── Experimental.kt │ │ ├── GlobalVars.kt │ │ ├── IR2Activity.kt │ │ ├── Navigator.kt │ │ ├── NavigatorFragmentFactory.kt │ │ ├── R2BasicWebView.kt │ │ ├── R2WebView.kt │ │ ├── SelectableNavigator.kt │ │ ├── audio │ │ └── PublicationDataSource.kt │ │ ├── audiobook │ │ ├── R2AudiobookActivity.kt │ │ └── R2MediaPlayer.kt │ │ ├── cbz │ │ └── R2CbzActivity.kt │ │ ├── divina │ │ └── R2DiViNaActivity.kt │ │ ├── epub │ │ ├── EpubNavigatorFragment.kt │ │ ├── EpubNavigatorViewModel.kt │ │ ├── IR2Highlightable.kt │ │ ├── IR2Selectable.kt │ │ ├── R2EpubActivity.kt │ │ ├── extensions │ │ │ └── Decoration.kt │ │ └── fxl │ │ │ ├── R2FXLLayout.kt │ │ │ ├── R2FXLOnDoubleTapListener.kt │ │ │ ├── R2FXLScroller.kt │ │ │ └── R2FXLUtils.kt │ │ ├── extensions │ │ ├── ControlFlow.kt │ │ ├── Duration.kt │ │ ├── Extensions.kt │ │ ├── JSON.kt │ │ ├── Link.kt │ │ ├── Locator.kt │ │ ├── Publication.kt │ │ └── String.kt │ │ ├── html │ │ └── HtmlDecorationTemplate.kt │ │ ├── image │ │ └── ImageNavigatorFragment.kt │ │ ├── media │ │ ├── ExoMediaPlayer.kt │ │ ├── MediaPlayback.kt │ │ ├── MediaPlayer.kt │ │ ├── MediaService.kt │ │ ├── MediaSessionNavigator.kt │ │ ├── PendingMedia.kt │ │ └── extensions │ │ │ ├── MediaControllerCompat.kt │ │ │ ├── MediaMetadataCompat.kt │ │ │ ├── MediaSessionCompat.kt │ │ │ └── PlaybackStateCompat.kt │ │ ├── pager │ │ ├── R2CbzPageFragment.kt │ │ ├── R2EpubPageFragment.kt │ │ ├── R2FXLPageFragment.kt │ │ ├── R2FragmentPagerAdapter.kt │ │ ├── R2PagerAdapter.kt │ │ ├── R2RTLViewPager.java │ │ └── R2ViewPager.kt │ │ ├── pdf │ │ ├── PdfNavigatorFragment.kt │ │ └── R2PdfActivity.kt │ │ └── util │ │ ├── BaseActionModeCallback.kt │ │ ├── FragmentFactory.kt │ │ └── ViewModelFactory.kt │ └── res │ ├── drawable │ ├── baseline_forward_10_white_24.png │ ├── baseline_replay_10_white_24.png │ ├── ic_pause_white_24dp.png │ ├── ic_play_arrow_white_24dp.png │ ├── ic_skip_next_white_24dp.png │ ├── ic_skip_previous_white_24dp.png │ ├── r2_media_notification_fastforward.xml │ └── r2_media_notification_rewind.xml │ ├── layout │ ├── activity_r2_audiobook.xml │ ├── activity_r2_divina.xml │ ├── activity_r2_epub.xml │ ├── activity_r2_image.xml │ ├── activity_r2_viewpager.xml │ ├── fragment_fxllayout_double.xml │ ├── fragment_fxllayout_single.xml │ ├── item_spinner_font.xml │ ├── popup_footnote.xml │ ├── r2_pdf_activity.xml │ ├── viewpager_fragment_cbz.xml │ └── viewpager_fragment_epub.xml │ ├── values-land │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug Report 11 | 12 | 15 | 16 | ### What happened? 17 | 18 | 23 | 24 | ### Expected behavior 25 | 26 | 27 | 28 | ### How to reproduce? 29 | 30 | 40 | 41 | ### Environment 42 | 43 | 44 | 45 | #### Readium versions 46 | 47 | 48 | 49 | * `r2-shared-kotlin`: 50 | * `r2-streamer-kotlin`: 51 | * `r2-navigator-kotlin`: 52 | * `r2-opds-kotlin`: 53 | * `r2-lcp-kotlin`: 54 | 55 | #### Development environment 56 | 57 | * OS: 58 | * IDE: 59 | 60 | #### Testing device 61 | 62 | * Android version: 63 | * Model: 64 | * Is it an emulator? Yes or No 65 | 66 | ### Additional context 67 | 68 | * Are you willing to fix the problem and contribute a pull request? Yes or No 69 | 70 | 77 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | build: 11 | name: Build and test 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Set up JDK 11 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '11' 21 | distribution: 'adopt' 22 | - name: Build 23 | run: ./gradlew clean build -x test 24 | - name: Test 25 | run: ./gradlew test --continue 26 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: macos-latest 13 | env: 14 | scripts: ${{ 'r2-navigator/src/main/assets/_scripts' }} 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Install dependencies 20 | run: npm install --prefix "$scripts" 21 | - name: Lint JavaScript 22 | run: yarn --cwd "$scripts" run lint 23 | - name: Check JavaScript formatting 24 | run: yarn --cwd "$scripts" run checkformat 25 | - name: Check if bundled scripts are up-to-date 26 | run: | 27 | make scripts 28 | git diff --exit-code --name-only r2-navigator/src/main/assets/readium/scripts 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | r2-navigator/build 10 | local.properties -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Readium 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SCRIPTS_PATH := r2-navigator/src/main/assets/_scripts 2 | 3 | help: 4 | @echo "Usage: make \n\n\ 5 | scripts\tBundle EPUB scripts with Webpack\n\ 6 | lint-scripts\tCheck quality of EPUB scripts\n\ 7 | " 8 | 9 | scripts: 10 | yarn --cwd "$(SCRIPTS_PATH)" run format 11 | yarn --cwd "$(SCRIPTS_PATH)" run bundle 12 | 13 | 14 | lint-scripts: 15 | yarn --cwd "$(SCRIPTS_PATH)" run lint 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **:warning: ᴛʜɪs ʀᴇᴘᴏsɪᴛᴏʀʏ ɪs ᴅᴇᴘʀᴇᴄᴀᴛᴇᴅ :warning:** 2 | > 3 | > We moved all the `r2-*-kotlin` modules to a single repository: [`kotlin-toolkit`](https://github.com/readium/kotlin-toolkit). 4 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.5.31' 5 | 6 | repositories { 7 | google() 8 | jcenter() 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:7.0.2' 12 | 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | maven { url 'https://jitpack.io' } 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | org.gradle.parallel=true 18 | org.gradle.configureondemand=true 19 | android.useAndroidX=true 20 | android.enableJetifier=true 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | -------------------------------------------------------------------------------- /r2-navigator-kotlin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /r2-navigator/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | plugins { 8 | id 'com.android.library' 9 | id 'kotlin-android' 10 | id 'kotlin-parcelize' 11 | id 'maven-publish' 12 | } 13 | 14 | group='com.github.readium' 15 | 16 | android { 17 | // FIXME: This doesn't pass the lint because some resources don't start with r2_ yet. We need to rename all resources for the next major version. 18 | // resourcePrefix "r2_" 19 | 20 | compileSdkVersion 31 21 | 22 | defaultConfig { 23 | minSdkVersion 21 24 | targetSdkVersion 31 25 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_8 29 | targetCompatibility JavaVersion.VERSION_1_8 30 | } 31 | kotlinOptions { 32 | jvmTarget = "1.8" 33 | freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" 34 | } 35 | buildTypes { 36 | release { 37 | minifyEnabled false 38 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 39 | } 40 | } 41 | buildFeatures { 42 | viewBinding true 43 | } 44 | } 45 | 46 | afterEvaluate { 47 | publishing { 48 | publications { 49 | release(MavenPublication) { 50 | from components.release 51 | } 52 | } 53 | } 54 | } 55 | 56 | dependencies { 57 | implementation fileTree(include: ['*.jar'], dir: 'libs') 58 | 59 | if (findProject(':r2-shared')) { 60 | implementation project(':r2-shared') 61 | } else { 62 | implementation "com.github.readium:r2-shared-kotlin:2.1.0" 63 | } 64 | 65 | implementation 'androidx.activity:activity-ktx:1.3.1' 66 | implementation 'androidx.appcompat:appcompat:1.3.1' 67 | implementation "androidx.browser:browser:1.3.0" 68 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0' 69 | implementation 'androidx.core:core-ktx:1.6.0' 70 | implementation 'androidx.fragment:fragment-ktx:1.3.6' 71 | implementation "androidx.legacy:legacy-support-core-ui:1.0.0" 72 | implementation "androidx.legacy:legacy-support-v4:1.0.0" 73 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" 74 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" 75 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" 76 | implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1" 77 | implementation "androidx.recyclerview:recyclerview:1.2.1" 78 | implementation "androidx.media:media:1.4.2" 79 | implementation "androidx.viewpager2:viewpager2:1.0.0" 80 | implementation "androidx.webkit:webkit:1.4.0" 81 | // Needed to avoid a crash with API 31, see https://stackoverflow.com/a/69152986/1474476 82 | implementation 'androidx.work:work-runtime-ktx:2.7.0-beta01' 83 | implementation "com.duolingo.open:rtl-viewpager:1.0.3" 84 | api "com.github.barteksc:android-pdf-viewer:2.8.2" 85 | // ChrisBane/PhotoView ( for the Zoom handling ) 86 | implementation "com.github.chrisbanes:PhotoView:2.3.0" 87 | // ExoPlayer is used by the Audio Navigator. 88 | api "com.google.android.exoplayer:exoplayer-core:2.15.0" 89 | api "com.google.android.exoplayer:exoplayer-ui:2.15.0" 90 | api "com.google.android.exoplayer:extension-mediasession:2.15.0" 91 | api "com.google.android.exoplayer:extension-workmanager:2.15.0" 92 | implementation 'com.google.android.material:material:1.4.0' 93 | implementation "com.jakewharton.timber:timber:4.7.1" 94 | implementation "com.shopgun.android:utils:1.0.9" 95 | implementation "joda-time:joda-time:2.10.10" 96 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0" 97 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0" 98 | // AM NOTE: needs to stay this version for now (June 24,2020) 99 | //noinspection GradleDependency 100 | implementation 'org.jsoup:jsoup:1.13.1' 101 | 102 | testImplementation "junit:junit:4.13.2" 103 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" 104 | 105 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 106 | androidTestImplementation 'androidx.test:runner:1.4.0' 107 | } 108 | -------------------------------------------------------------------------------- /r2-navigator/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/aferditamuriqi/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /r2-navigator/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 17 | 18 | 19 | 20 | 23 | 26 | 29 | 32 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "globals": { 7 | "Android": "readonly", 8 | "readium": "writable" 9 | }, 10 | "extends": "eslint:recommended", 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "ignorePatterns": ["src/vendor"], 16 | "rules": { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/.prettierignore: -------------------------------------------------------------------------------- 1 | src/vendor 2 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/.prettierrc.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/r2-navigator/src/main/assets/_scripts/.prettierrc.json -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/README.md: -------------------------------------------------------------------------------- 1 | # Readium JS (Kotlin) 2 | 3 | A set of JavaScript files used by the Kotlin EPUB navigator. 4 | 5 | This folder starts with an underscore to prevent Gradle from embedding it as an asset. 6 | 7 | ## Scripts 8 | 9 | Run `npm install`, then use one of the following: 10 | 11 | * `yarn run bundle` Rebuild the assets after any changes in the `src/` folder. 12 | * `yarn run lint` Check code quality. 13 | * `yarn run checkformat` Check if there's any formatting issues. 14 | * `yarn run format` Automatically format JavaScript sources. 15 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "readium-js", 3 | "version": "0.1.0", 4 | "description": "A set of scripts for the EPUB navigator", 5 | "private": true, 6 | "scripts": { 7 | "bundle": "webpack", 8 | "lint": "eslint src", 9 | "checkformat": "prettier --check '**/*.js'", 10 | "format": "prettier --list-different --write '**/*.js'" 11 | }, 12 | "author": "Readium Foundation", 13 | "license": "BSD-3-Clause", 14 | "devDependencies": { 15 | "eslint": "^7.29.0", 16 | "prettier": "2.3.1", 17 | "webpack": "^5.40.0", 18 | "webpack-cli": "^4.7.2" 19 | }, 20 | "dependencies": { 21 | "approx-string-match": "^1.1.0", 22 | "hash.js": "^1.1.7", 23 | "string.prototype.matchall": "^4.0.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/gestures.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | import { handleDecorationClickEvent } from "./decorator"; 8 | 9 | window.addEventListener("DOMContentLoaded", function () { 10 | document.addEventListener("click", onClick, false); 11 | }); 12 | 13 | function onClick(event) { 14 | if (!window.getSelection().isCollapsed) { 15 | // There's an on-going selection, the tap will dismiss it so we don't forward it. 16 | return; 17 | } 18 | 19 | var pixelRatio = window.devicePixelRatio; 20 | let clickEvent = { 21 | defaultPrevented: event.defaultPrevented, 22 | x: event.clientX * pixelRatio, 23 | y: event.clientY * pixelRatio, 24 | targetElement: event.target.outerHTML, 25 | interactiveElement: nearestInteractiveElement(event.target), 26 | }; 27 | 28 | if (handleDecorationClickEvent(event, clickEvent)) { 29 | return; 30 | } 31 | 32 | // Send the tap data over the JS bridge even if it's been handled within the web view, so that 33 | // it can be preserved and used by the toolkit if needed. 34 | var shouldPreventDefault = Android.onTap(JSON.stringify(clickEvent)); 35 | 36 | if (shouldPreventDefault) { 37 | event.stopPropagation(); 38 | event.preventDefault(); 39 | } 40 | } 41 | 42 | // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling 43 | function nearestInteractiveElement(element) { 44 | var interactiveTags = [ 45 | "a", 46 | "audio", 47 | "button", 48 | "canvas", 49 | "details", 50 | "input", 51 | "label", 52 | "option", 53 | "select", 54 | "submit", 55 | "textarea", 56 | "video", 57 | ]; 58 | if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) { 59 | return element.outerHTML; 60 | } 61 | 62 | // Checks whether the element is editable by the user. 63 | if ( 64 | element.hasAttribute("contenteditable") && 65 | element.getAttribute("contenteditable").toLowerCase() != "false" 66 | ) { 67 | return element.outerHTML; 68 | } 69 | 70 | // Checks parents recursively because the touch might be for example on an inside a . 71 | if (element.parentElement) { 72 | return nearestInteractiveElement(element.parentElement); 73 | } 74 | 75 | return null; 76 | } 77 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/index-fixed.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2021 Readium Foundation. All rights reserved. 3 | // Use of this source code is governed by the BSD-style license 4 | // available in the top-level LICENSE file of the project. 5 | // 6 | 7 | // Script used for fixed layouts resources. 8 | 9 | import "./index"; 10 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/index-reflowable.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2021 Readium Foundation. All rights reserved. 3 | // Use of this source code is governed by the BSD-style license 4 | // available in the top-level LICENSE file of the project. 5 | // 6 | 7 | // Script used for reflowable resources. 8 | 9 | import "./index"; 10 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2021 Readium Foundation. All rights reserved. 3 | // Use of this source code is governed by the BSD-style license 4 | // available in the top-level LICENSE file of the project. 5 | // 6 | 7 | // Base script used by both reflowable and fixed layout resources. 8 | 9 | import "./gestures"; 10 | import { 11 | removeProperty, 12 | scrollLeft, 13 | scrollRight, 14 | scrollToEnd, 15 | scrollToId, 16 | scrollToPosition, 17 | scrollToStart, 18 | scrollToText, 19 | setProperty, 20 | } from "./utils"; 21 | import { 22 | createAnnotation, 23 | createHighlight, 24 | destroyHighlight, 25 | getCurrentSelectionInfo, 26 | getSelectionRect, 27 | rectangleForHighlightWithID, 28 | setScrollMode, 29 | } from "./highlight"; 30 | import { getCurrentSelection } from "./selection"; 31 | import { getDecorations, registerTemplates } from "./decorator"; 32 | 33 | // Public API used by the navigator. 34 | window.readium = { 35 | // utils 36 | scrollToId: scrollToId, 37 | scrollToPosition: scrollToPosition, 38 | scrollToText: scrollToText, 39 | scrollLeft: scrollLeft, 40 | scrollRight: scrollRight, 41 | scrollToStart: scrollToStart, 42 | scrollToEnd: scrollToEnd, 43 | setProperty: setProperty, 44 | removeProperty: removeProperty, 45 | 46 | // selection 47 | getCurrentSelection: getCurrentSelection, 48 | 49 | // decoration 50 | registerDecorationTemplates: registerTemplates, 51 | getDecorations: getDecorations, 52 | }; 53 | 54 | // Legacy highlights API. 55 | window.createAnnotation = createAnnotation; 56 | window.createHighlight = createHighlight; 57 | window.destroyHighlight = destroyHighlight; 58 | window.getCurrentSelectionInfo = getCurrentSelectionInfo; 59 | window.getSelectionRect = getSelectionRect; 60 | window.rectangleForHighlightWithID = rectangleForHighlightWithID; 61 | window.setScrollMode = setScrollMode; 62 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/selection.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2021 Readium Foundation. All rights reserved. 3 | // Use of this source code is governed by the BSD-style license 4 | // available in the top-level LICENSE file of the project. 5 | // 6 | 7 | import { log as logNative, logError } from "./utils"; 8 | import { toNativeRect } from "./rect"; 9 | import { TextRange } from "./vendor/hypothesis/anchoring/text-range"; 10 | 11 | // Polyfill for Android API 26 12 | import matchAll from "string.prototype.matchall"; 13 | matchAll.shim(); 14 | 15 | const debug = true; 16 | 17 | export function getCurrentSelection() { 18 | const text = getCurrentSelectionText(); 19 | if (!text) { 20 | return null; 21 | } 22 | const rect = getSelectionRect(); 23 | return { text, rect }; 24 | } 25 | 26 | function getSelectionRect() { 27 | try { 28 | let sel = window.getSelection(); 29 | if (!sel) { 30 | return; 31 | } 32 | let range = sel.getRangeAt(0); 33 | 34 | return toNativeRect(range.getBoundingClientRect()); 35 | } catch (e) { 36 | logError(e); 37 | return null; 38 | } 39 | } 40 | 41 | function getCurrentSelectionText() { 42 | const selection = window.getSelection(); 43 | if (!selection) { 44 | return undefined; 45 | } 46 | if (selection.isCollapsed) { 47 | return undefined; 48 | } 49 | const highlight = selection.toString(); 50 | const cleanHighlight = highlight 51 | .trim() 52 | .replace(/\n/g, " ") 53 | .replace(/\s\s+/g, " "); 54 | if (cleanHighlight.length === 0) { 55 | return undefined; 56 | } 57 | if (!selection.anchorNode || !selection.focusNode) { 58 | return undefined; 59 | } 60 | const range = 61 | selection.rangeCount === 1 62 | ? selection.getRangeAt(0) 63 | : createOrderedRange( 64 | selection.anchorNode, 65 | selection.anchorOffset, 66 | selection.focusNode, 67 | selection.focusOffset 68 | ); 69 | if (!range || range.collapsed) { 70 | log("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!"); 71 | return undefined; 72 | } 73 | 74 | const text = document.body.textContent; 75 | const textRange = TextRange.fromRange(range).relativeTo(document.body); 76 | const start = textRange.start.offset; 77 | const end = textRange.end.offset; 78 | 79 | const snippetLength = 200; 80 | 81 | // Compute the text before the highlight, ignoring the first "word", which might be cut. 82 | let before = text.slice(Math.max(0, start - snippetLength), start); 83 | let firstWordStart = before.search(/\P{L}\p{L}/gu); 84 | if (firstWordStart !== -1) { 85 | before = before.slice(firstWordStart + 1); 86 | } 87 | 88 | // Compute the text after the highlight, ignoring the last "word", which might be cut. 89 | let after = text.slice(end, Math.min(text.length, end + snippetLength)); 90 | let lastWordEnd = Array.from(after.matchAll(/\p{L}\P{L}/gu)).pop(); 91 | if (lastWordEnd !== undefined && lastWordEnd.index > 1) { 92 | after = after.slice(0, lastWordEnd.index + 1); 93 | } 94 | 95 | return { highlight, before, after }; 96 | } 97 | 98 | function createOrderedRange(startNode, startOffset, endNode, endOffset) { 99 | const range = new Range(); 100 | range.setStart(startNode, startOffset); 101 | range.setEnd(endNode, endOffset); 102 | if (!range.collapsed) { 103 | return range; 104 | } 105 | log(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?"); 106 | const rangeReverse = new Range(); 107 | rangeReverse.setStart(endNode, endOffset); 108 | rangeReverse.setEnd(startNode, startOffset); 109 | if (!rangeReverse.collapsed) { 110 | log(">>> createOrderedRange RANGE REVERSE OK."); 111 | return range; 112 | } 113 | log(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!"); 114 | return undefined; 115 | } 116 | 117 | export function convertRangeInfo(document, rangeInfo) { 118 | const startElement = document.querySelector( 119 | rangeInfo.startContainerElementCssSelector 120 | ); 121 | if (!startElement) { 122 | log("^^^ convertRangeInfo NO START ELEMENT CSS SELECTOR?!"); 123 | return undefined; 124 | } 125 | let startContainer = startElement; 126 | if (rangeInfo.startContainerChildTextNodeIndex >= 0) { 127 | if ( 128 | rangeInfo.startContainerChildTextNodeIndex >= 129 | startElement.childNodes.length 130 | ) { 131 | log( 132 | "^^^ convertRangeInfo rangeInfo.startContainerChildTextNodeIndex >= startElement.childNodes.length?!" 133 | ); 134 | return undefined; 135 | } 136 | startContainer = 137 | startElement.childNodes[rangeInfo.startContainerChildTextNodeIndex]; 138 | if (startContainer.nodeType !== Node.TEXT_NODE) { 139 | log("^^^ convertRangeInfo startContainer.nodeType !== Node.TEXT_NODE?!"); 140 | return undefined; 141 | } 142 | } 143 | const endElement = document.querySelector( 144 | rangeInfo.endContainerElementCssSelector 145 | ); 146 | if (!endElement) { 147 | log("^^^ convertRangeInfo NO END ELEMENT CSS SELECTOR?!"); 148 | return undefined; 149 | } 150 | let endContainer = endElement; 151 | if (rangeInfo.endContainerChildTextNodeIndex >= 0) { 152 | if ( 153 | rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length 154 | ) { 155 | log( 156 | "^^^ convertRangeInfo rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length?!" 157 | ); 158 | return undefined; 159 | } 160 | endContainer = 161 | endElement.childNodes[rangeInfo.endContainerChildTextNodeIndex]; 162 | if (endContainer.nodeType !== Node.TEXT_NODE) { 163 | log("^^^ convertRangeInfo endContainer.nodeType !== Node.TEXT_NODE?!"); 164 | return undefined; 165 | } 166 | } 167 | return createOrderedRange( 168 | startContainer, 169 | rangeInfo.startOffset, 170 | endContainer, 171 | rangeInfo.endOffset 172 | ); 173 | } 174 | 175 | export function location2RangeInfo(location) { 176 | const locations = location.locations; 177 | const domRange = locations.domRange; 178 | const start = domRange.start; 179 | const end = domRange.end; 180 | 181 | return { 182 | endContainerChildTextNodeIndex: end.textNodeIndex, 183 | endContainerElementCssSelector: end.cssSelector, 184 | endOffset: end.offset, 185 | startContainerChildTextNodeIndex: start.textNodeIndex, 186 | startContainerElementCssSelector: start.cssSelector, 187 | startOffset: start.offset, 188 | }; 189 | } 190 | 191 | function log() { 192 | if (debug) { 193 | logNative.apply(null, arguments); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/README.md: -------------------------------------------------------------------------------- 1 | The code in this module has been directly copied from https://github.com/hypothesis/client, released under the 2-Clause BSD license. 2 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/html.js: -------------------------------------------------------------------------------- 1 | import { RangeAnchor, TextPositionAnchor, TextQuoteAnchor } from './types'; 2 | 3 | /** 4 | * @typedef {import('../../types/api').Selector} Selector 5 | */ 6 | 7 | /** 8 | * @param {RangeAnchor|TextPositionAnchor|TextQuoteAnchor} anchor 9 | * @param {Object} [options] 10 | * @param {number} [options.hint] 11 | */ 12 | async function querySelector(anchor, options = {}) { 13 | return anchor.toRange(options); 14 | } 15 | 16 | /** 17 | * Anchor a set of selectors. 18 | * 19 | * This function converts a set of selectors into a document range. 20 | * It encapsulates the core anchoring algorithm, using the selectors alone or 21 | * in combination to establish the best anchor within the document. 22 | * 23 | * @param {Element} root - The root element of the anchoring context. 24 | * @param {Selector[]} selectors - The selectors to try. 25 | * @param {Object} [options] 26 | * @param {number} [options.hint] 27 | */ 28 | export function anchor(root, selectors, options = {}) { 29 | let position = null; 30 | let quote = null; 31 | let range = null; 32 | 33 | // Collect all the selectors 34 | for (let selector of selectors) { 35 | switch (selector.type) { 36 | case 'TextPositionSelector': 37 | position = selector; 38 | options.hint = position.start; // TextQuoteAnchor hint 39 | break; 40 | case 'TextQuoteSelector': 41 | quote = selector; 42 | break; 43 | case 'RangeSelector': 44 | range = selector; 45 | break; 46 | } 47 | } 48 | 49 | /** 50 | * Assert the quote matches the stored quote, if applicable 51 | * @param {Range} range 52 | */ 53 | const maybeAssertQuote = range => { 54 | if (quote?.exact && range.toString() !== quote.exact) { 55 | throw new Error('quote mismatch'); 56 | } else { 57 | return range; 58 | } 59 | }; 60 | 61 | // From a default of failure, we build up catch clauses to try selectors in 62 | // order, from simple to complex. 63 | /** @type {Promise} */ 64 | let promise = Promise.reject('unable to anchor'); 65 | 66 | if (range) { 67 | promise = promise.catch(() => { 68 | let anchor = RangeAnchor.fromSelector(root, range); 69 | return querySelector(anchor, options).then(maybeAssertQuote); 70 | }); 71 | } 72 | 73 | if (position) { 74 | promise = promise.catch(() => { 75 | let anchor = TextPositionAnchor.fromSelector(root, position); 76 | return querySelector(anchor, options).then(maybeAssertQuote); 77 | }); 78 | } 79 | 80 | if (quote) { 81 | promise = promise.catch(() => { 82 | let anchor = TextQuoteAnchor.fromSelector(root, quote); 83 | return querySelector(anchor, options); 84 | }); 85 | } 86 | 87 | return promise; 88 | } 89 | 90 | /** 91 | * @param {Element} root 92 | * @param {Range} range 93 | */ 94 | export function describe(root, range) { 95 | const types = [RangeAnchor, TextPositionAnchor, TextQuoteAnchor]; 96 | const result = []; 97 | for (let type of types) { 98 | try { 99 | const anchor = type.fromRange(root, range); 100 | result.push(anchor.toSelector()); 101 | } catch (error) { 102 | continue; 103 | } 104 | } 105 | return result; 106 | } 107 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/match-quote.js: -------------------------------------------------------------------------------- 1 | import approxSearch from 'approx-string-match'; 2 | 3 | /** 4 | * @typedef {import('approx-string-match').Match} StringMatch 5 | */ 6 | 7 | /** 8 | * @typedef Match 9 | * @prop {number} start - Start offset of match in text 10 | * @prop {number} end - End offset of match in text 11 | * @prop {number} score - 12 | * Score for the match between 0 and 1.0, where 1.0 indicates a perfect match 13 | * for the quote and context. 14 | */ 15 | 16 | /** 17 | * Find the best approximate matches for `str` in `text` allowing up to `maxErrors` errors. 18 | * 19 | * @param {string} text 20 | * @param {string} str 21 | * @param {number} maxErrors 22 | * @return {StringMatch[]} 23 | */ 24 | function search(text, str, maxErrors) { 25 | // Do a fast search for exact matches. The `approx-string-match` library 26 | // doesn't currently incorporate this optimization itself. 27 | let matchPos = 0; 28 | let exactMatches = []; 29 | while (matchPos !== -1) { 30 | matchPos = text.indexOf(str, matchPos); 31 | if (matchPos !== -1) { 32 | exactMatches.push({ 33 | start: matchPos, 34 | end: matchPos + str.length, 35 | errors: 0, 36 | }); 37 | matchPos += 1; 38 | } 39 | } 40 | if (exactMatches.length > 0) { 41 | return exactMatches; 42 | } 43 | 44 | // If there are no exact matches, do a more expensive search for matches 45 | // with errors. 46 | return approxSearch(text, str, maxErrors); 47 | } 48 | 49 | /** 50 | * Compute a score between 0 and 1.0 for the similarity between `text` and `str`. 51 | * 52 | * @param {string} text 53 | * @param {string} str 54 | */ 55 | function textMatchScore(text, str) { 56 | /* istanbul ignore next - `scoreMatch` will never pass an empty string */ 57 | if (str.length === 0 || text.length === 0) { 58 | return 0.0; 59 | } 60 | const matches = search(text, str, str.length); 61 | 62 | // prettier-ignore 63 | return 1 - (matches[0].errors / str.length); 64 | } 65 | 66 | /** 67 | * Find the best approximate match for `quote` in `text`. 68 | * 69 | * Returns `null` if no match exceeding the minimum quality threshold was found. 70 | * 71 | * @param {string} text - Document text to search 72 | * @param {string} quote - String to find within `text` 73 | * @param {Object} context - 74 | * Context in which the quote originally appeared. This is used to choose the 75 | * best match. 76 | * @param {string} [context.prefix] - Expected text before the quote 77 | * @param {string} [context.suffix] - Expected text after the quote 78 | * @param {number} [context.hint] - Expected offset of match within text 79 | * @return {Match|null} 80 | */ 81 | export function matchQuote(text, quote, context = {}) { 82 | if (quote.length === 0) { 83 | return null; 84 | } 85 | 86 | // Choose the maximum number of errors to allow for the initial search. 87 | // This choice involves a tradeoff between: 88 | // 89 | // - Recall (proportion of "good" matches found) 90 | // - Precision (proportion of matches found which are "good") 91 | // - Cost of the initial search and of processing the candidate matches [1] 92 | // 93 | // [1] Specifically, the expected-time complexity of the initial search is 94 | // `O((maxErrors / 32) * text.length)`. See `approx-string-match` docs. 95 | const maxErrors = Math.min(256, quote.length / 2); 96 | 97 | // Find closest matches for `quote` in `text` based on edit distance. 98 | const matches = search(text, quote, maxErrors); 99 | 100 | if (matches.length === 0) { 101 | return null; 102 | } 103 | 104 | /** 105 | * Compute a score between 0 and 1.0 for a match candidate. 106 | * 107 | * @param {StringMatch} match 108 | */ 109 | const scoreMatch = match => { 110 | const quoteWeight = 50; // Similarity of matched text to quote. 111 | const prefixWeight = 20; // Similarity of text before matched text to `context.prefix`. 112 | const suffixWeight = 20; // Similarity of text after matched text to `context.suffix`. 113 | const posWeight = 2; // Proximity to expected location. Used as a tie-breaker. 114 | 115 | const quoteScore = 1 - match.errors / quote.length; 116 | 117 | const prefixScore = context.prefix 118 | ? textMatchScore( 119 | text.slice(Math.max(0, match.start - context.prefix.length), match.start), 120 | context.prefix 121 | ) 122 | : 1.0; 123 | const suffixScore = context.suffix 124 | ? textMatchScore( 125 | text.slice(match.end, match.end + context.suffix.length), 126 | context.suffix 127 | ) 128 | : 1.0; 129 | 130 | let posScore = 1.0; 131 | if (typeof context.hint === 'number') { 132 | const offset = Math.abs(match.start - context.hint); 133 | posScore = 1.0 - offset / text.length; 134 | } 135 | 136 | const rawScore = 137 | quoteWeight * quoteScore + 138 | prefixWeight * prefixScore + 139 | suffixWeight * suffixScore + 140 | posWeight * posScore; 141 | const maxScore = quoteWeight + prefixWeight + suffixWeight + posWeight; 142 | const normalizedScore = rawScore / maxScore; 143 | 144 | return normalizedScore; 145 | }; 146 | 147 | // Rank matches based on similarity of actual and expected surrounding text 148 | // and actual/expected offset in the document text. 149 | const scoredMatches = matches.map(m => ({ 150 | start: m.start, 151 | end: m.end, 152 | score: scoreMatch(m), 153 | })); 154 | 155 | // Choose match with highest score. 156 | scoredMatches.sort((a, b) => b.score - a.score); 157 | return scoredMatches[0]; 158 | } 159 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/placeholder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS selector that will match the placeholder within a page/tile container. 3 | */ 4 | const placeholderSelector = '.annotator-placeholder'; 5 | 6 | /** 7 | * Create or return a placeholder element for anchoring. 8 | * 9 | * In document viewers such as PDF.js which only render a subset of long 10 | * documents at a time, it may not be possible to anchor annotations to the 11 | * actual text in pages which are off-screen. For these non-rendered pages, 12 | * a "placeholder" element is created in the approximate X/Y location (eg. 13 | * middle of the page) where the content will appear. Any highlights for that 14 | * page are then rendered inside the placeholder. 15 | * 16 | * When the viewport is scrolled to the non-rendered page, the placeholder 17 | * is removed and annotations are re-anchored to the real content. 18 | * 19 | * @param {HTMLElement} container - The container element for the page or tile 20 | * which is not rendered. 21 | */ 22 | export function createPlaceholder(container) { 23 | let placeholder = container.querySelector(placeholderSelector); 24 | if (placeholder) { 25 | return placeholder; 26 | } 27 | placeholder = document.createElement('span'); 28 | placeholder.classList.add('annotator-placeholder'); 29 | placeholder.textContent = 'Loading annotations...'; 30 | container.appendChild(placeholder); 31 | return placeholder; 32 | } 33 | 34 | /** 35 | * Return true if a page/tile container has a placeholder. 36 | * 37 | * @param {HTMLElement} container 38 | */ 39 | export function hasPlaceholder(container) { 40 | return container.querySelector(placeholderSelector) !== null; 41 | } 42 | 43 | /** 44 | * Remove the placeholder element in `container`, if present. 45 | * 46 | * @param {HTMLElement} container 47 | */ 48 | export function removePlaceholder(container) { 49 | container.querySelector(placeholderSelector)?.remove(); 50 | } 51 | 52 | /** 53 | * Return true if `node` is inside a placeholder element created with `createPlaceholder`. 54 | * 55 | * This is typically used to test if a highlight element associated with an 56 | * anchor is inside a placeholder. 57 | * 58 | * @param {Node} node 59 | */ 60 | export function isInPlaceholder(node) { 61 | if (!node.parentElement) { 62 | return false; 63 | } 64 | return node.parentElement.closest(placeholderSelector) !== null; 65 | } 66 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/test/html-anchoring-fixture.html: -------------------------------------------------------------------------------- 1 |

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

2 | 3 |

Header Level 2

4 | 5 |
    6 |
  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. 7 |
  3. Aliquam tincidunt mauris eu risus.
  4. 8 |
9 | 10 |

Lorem sed do eiusmod tempor.

11 | 12 |
Humani generis

Lorem sed do eiusmod tempor.


Mortalium animos
13 | 14 |
Humani generis


Lorem sed do eiusmod tempor.



Mortalium animos
15 | 16 |

Header Level 2

17 | 18 |

19 | Mauris lacinia ipsum nulla, id iaculis quam egestas quis. 20 |

21 | 22 |

23 | Fusce fermentum sit amet augue sed rutrum. 24 |

25 | 26 | 27 | 28 |
Test 29 |
30 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/test/html-baselines/index.js: -------------------------------------------------------------------------------- 1 | // Fixtures for anchoring baseline tests. The goals of these tests are to: 2 | // 3 | // 1) Check for unexpected changes in the selectors captured when describing 4 | // a Range in a web page. 5 | // 2) Test anchoring in larger and more complex pages than the basic anchoring 6 | // unit tests. 7 | // 8 | // Each fixture consists of: 9 | // 10 | // - An HTML file for a web page 11 | // - A set of annotations in the JSON format returned by the Hypothesis API 12 | // 13 | // To add a new fixture: 14 | // 15 | // 1. Open up a web page in the browser and annotate it. 16 | // 2. Save the web page (HTML only) via File -> Save Page As... as 17 | // `.html` in this directory. 18 | // It is important to save the page exactly as it was when annotated, since 19 | // many pages have at least some dynamic content. 20 | // 3. Fetch the annotations for the web page via the Hypothesis API and save 21 | // them as `.json` in this directory 22 | // 4. Add an entry to the fixture list below. 23 | 24 | export default [ 25 | { 26 | name: 'Minimal Document', 27 | html: require('./minimal.html'), 28 | annotations: require('./minimal.json'), 29 | }, 30 | { 31 | name: 'Wikipedia - Regression Testing', 32 | html: require('./wikipedia-regression-testing.html'), 33 | annotations: require('./wikipedia-regression-testing.json'), 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/test/html-baselines/minimal.html: -------------------------------------------------------------------------------- 1 |
One Two Three
2 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/test/html-baselines/minimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 1, 3 | "rows": [{ 4 | "id": "test", 5 | "target": [{ 6 | "selector": [{ 7 | "type": "RangeSelector", 8 | "startContainer": "/div[1]", 9 | "endContainer": "/div[1]", 10 | "startOffset": 4, 11 | "endOffset": 7 12 | },{ 13 | "type": "TextPositionSelector", 14 | "start": 4, 15 | "end": 7 16 | },{ 17 | "type": "TextQuoteSelector", 18 | "prefix": "One ", 19 | "suffix": " Three\n", 20 | "exact": "Two" 21 | }] 22 | }] 23 | }] 24 | } 25 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/test/match-quote-test.js: -------------------------------------------------------------------------------- 1 | import { matchQuote } from '../match-quote'; 2 | 3 | const fixtures = { 4 | solitude: `Many years later, as he faced the firing squad, 5 | Colonel Aureliano Buendía was to remember that distant afternoon 6 | when his father took him to discover ice`, 7 | 8 | twoCities: `It was the best of times, it was the worst of times, 9 | it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, 10 | it was the epoch of incredulity, it was the season of Light, it was the 11 | season of Darkness, it was the spring of hope, it was the winter of despair, we had 12 | everything before us, we had nothing before us, we were all going direct to Heaven, 13 | we were all going direct the other way.`, 14 | }; 15 | 16 | function normalize(str) { 17 | // Normalize whitespace. 18 | return str.replace(/\s+/g, ' '); 19 | } 20 | 21 | Object.keys(fixtures).forEach(k => (fixtures[k] = normalize(fixtures[k]))); 22 | 23 | describe('matchQuote', () => { 24 | it('finds exact match', () => { 25 | const match = matchQuote(fixtures.solitude, 'discover ice'); 26 | assert.equal(match.score, 1.0); 27 | assert.equal( 28 | fixtures.solitude.slice(match.start, match.end), 29 | 'discover ice' 30 | ); 31 | }); 32 | 33 | it('finds best approximate match if there is no exact match', () => { 34 | const match = matchQuote(fixtures.solitude, 'some years later'); 35 | assert.isTrue(match.score > 0); 36 | assert.isTrue(match.score < 1); 37 | assert.equal( 38 | fixtures.solitude.slice(match.start, match.end), 39 | 'Many years later' 40 | ); 41 | }); 42 | 43 | it('scores matches based on quote similarity', () => { 44 | // List of quotes in descending order of similarity to the text. 45 | const quotes = [ 46 | 'Many years later', 47 | 'Many yers later', 48 | 'Some years later', 49 | 'Some years after', 50 | ]; 51 | 52 | const scores = quotes.map(q => matchQuote(fixtures.solitude, q).score); 53 | 54 | for (let i = 1; i < scores.length; i++) { 55 | assert.isBelow(scores[i], scores[i - 1]); 56 | } 57 | }); 58 | 59 | it('scores matches based on prefix similarity', () => { 60 | // List of prefixes in descending order of similarity to the actual prefix 61 | // of the quote. 62 | const prefixes = [ 63 | 'Many years later', 64 | 'Many yers later', 65 | 'Some years later', 66 | 'Some years after', 67 | ]; 68 | 69 | const quote = ', as he faced the firing squad'; 70 | const scores = prefixes.map( 71 | p => matchQuote(fixtures.solitude, quote, { prefix: p }).score 72 | ); 73 | 74 | for (let i = 1; i < scores.length; i++) { 75 | assert.isBelow(scores[i], scores[i - 1]); 76 | } 77 | }); 78 | 79 | it('scores matches based on suffix similarity', () => { 80 | // List of suffixes in descending order of similarity to the actual suffix 81 | // of the quote. 82 | const suffixes = [ 83 | ', as he faced the firing squad', 84 | ', as she faced the firing squad', 85 | ', as he awaited the firing squad', 86 | ', as he awaited his death', 87 | ]; 88 | 89 | const quote = 'Many years later'; 90 | const scores = suffixes.map( 91 | s => matchQuote(fixtures.solitude, quote, { suffix: s }).score 92 | ); 93 | 94 | for (let i = 1; i < scores.length; i++) { 95 | assert.isBelow(scores[i], scores[i - 1]); 96 | } 97 | }); 98 | 99 | it('returns `null` if there is no acceptable approximate match', () => { 100 | const match = matchQuote(fixtures.twoCities, fixtures.solitude); 101 | assert.equal(match, null); 102 | }); 103 | 104 | it('returns `null` if quote is empty', () => { 105 | assert.equal(matchQuote('foobar', ''), null); 106 | }); 107 | 108 | it('returns `null` if text is empty', () => { 109 | assert.equal(matchQuote('', 'foobar'), null); 110 | }); 111 | 112 | [ 113 | // Exact prefix matches. 114 | { 115 | quote: 'before us', 116 | prefix: 'we had everything', 117 | expected: 'before us, we had nothing', 118 | }, 119 | { 120 | quote: 'before us', 121 | prefix: 'we had nothing', 122 | expected: 'before us, we were all going', 123 | }, 124 | 125 | // Approximate prefix matches. 126 | { 127 | quote: 'before us', 128 | prefix: 'we had every-thing', 129 | expected: 'before us, we had nothing', 130 | }, 131 | { 132 | quote: 'before us', 133 | prefix: 'we had nout', 134 | expected: 'before us, we were all going', 135 | }, 136 | 137 | // Exact suffix matches. 138 | { 139 | quote: 'we had', 140 | suffix: 'everything', 141 | expected: 'we had everything', 142 | }, 143 | { 144 | quote: 'we had', 145 | suffix: 'nothing', 146 | expected: 'we had nothing', 147 | }, 148 | 149 | // Approximate suffix matches. 150 | { 151 | quote: 'we had', 152 | suffix: 'ever ting', 153 | expected: 'we had everything', 154 | }, 155 | { 156 | quote: 'we had', 157 | suffix: 'nutting', 158 | expected: 'we had nothing', 159 | }, 160 | ].forEach(({ quote, prefix, suffix, expected }, i) => { 161 | it(`finds match with best context match (${i})`, () => { 162 | const text = fixtures.twoCities; 163 | const match = matchQuote(text, quote, { 164 | prefix, 165 | suffix, 166 | }); 167 | assert.ok(match); 168 | assert.equal(text.slice(match.start, match.end), quote); 169 | assert.equal(match.start, text.indexOf(expected)); 170 | }); 171 | }); 172 | 173 | it('uses `hint` as a tie-breaker to choose between matches with close scores', () => { 174 | const text = fixtures.twoCities; 175 | const posA = text.indexOf('everything before us') + 'everything '.length; 176 | const posB = text.indexOf('nothing before us') + 'nothing '.length; 177 | 178 | // Search for a quote that appears multiple times in the text. Since no 179 | // context is provided, there will be several matches with equal scores to 180 | // choose between. 181 | const matchHintA = matchQuote(text, 'befor us', { hint: posA }); 182 | const matchHintB = matchQuote(text, 'befor us', { hint: posB }); 183 | const matchNoHint = matchQuote(text, 'befor us'); 184 | 185 | // When a hint is provided, `matchQuote` should choose between otherwise 186 | // equal matches based on how close the match start is to `hint`. 187 | assert.ok(matchHintA); 188 | assert.equal(matchHintA.start, posA, 'Wrong match for hint `posA`'); 189 | 190 | assert.ok(matchHintB); 191 | assert.equal(matchHintB.start, posB, 'Wrong match for hint `posB`'); 192 | 193 | // When no hint is provided, the first match (ie. lowest `match.start`) 194 | // should be chosen. 195 | assert.ok(matchNoHint); 196 | assert.equal(matchNoHint.start, posA, 'Wrong match with no hint'); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/test/placeholder-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | createPlaceholder, 3 | hasPlaceholder, 4 | isInPlaceholder, 5 | removePlaceholder, 6 | } from '../placeholder'; 7 | 8 | describe('annotator/anchoring/placeholder', () => { 9 | describe('createPlaceholder', () => { 10 | it('adds a placeholder element to the container', () => { 11 | const container = document.createElement('div'); 12 | const placeholder = createPlaceholder(container); 13 | 14 | assert.ok(placeholder); 15 | assert.equal( 16 | container.querySelector('.annotator-placeholder'), 17 | placeholder 18 | ); 19 | }); 20 | 21 | it('returns the existing placeholder if present', () => { 22 | const container = document.createElement('div'); 23 | const placeholderA = createPlaceholder(container); 24 | const placeholderB = createPlaceholder(container); 25 | 26 | assert.equal(placeholderA, placeholderB); 27 | }); 28 | }); 29 | 30 | describe('removePlaceholder', () => { 31 | it('removes the placeholder if present', () => { 32 | const container = document.createElement('div'); 33 | createPlaceholder(container); 34 | 35 | assert.isTrue(hasPlaceholder(container)); 36 | removePlaceholder(container); 37 | assert.isFalse(hasPlaceholder(container)); 38 | }); 39 | 40 | it('does nothing if placeholder is not present', () => { 41 | const container = document.createElement('div'); 42 | removePlaceholder(container); 43 | removePlaceholder(container); 44 | }); 45 | }); 46 | 47 | describe('isInPlaceholder', () => { 48 | it('returns true if node is inside a placeholder', () => { 49 | const container = document.createElement('div'); 50 | const placeholder = createPlaceholder(container); 51 | const child = document.createElement('div'); 52 | placeholder.append(child); 53 | 54 | assert.isTrue(isInPlaceholder(child)); 55 | }); 56 | 57 | it('returns false if node is not inside a placeholder', () => { 58 | assert.isFalse(isInPlaceholder(document.body)); 59 | }); 60 | 61 | it('returns false if node has no parent', () => { 62 | const el = document.createElement('div'); 63 | assert.isFalse(isInPlaceholder(el)); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/test/xpath-test.js: -------------------------------------------------------------------------------- 1 | import { nodeFromXPath, xpathFromNode } from '../xpath'; 2 | 3 | describe('annotator/anchoring/xpath', () => { 4 | describe('xpathFromNode', () => { 5 | let container; 6 | const html = ` 7 |

text

8 |

text

text

9 |

text

text
text

10 | 11 |
    12 |
  • text1
  • 13 |
  • text
  • 14 |
  • text
  • 15 |
16 |
`; 17 | 18 | beforeEach(() => { 19 | container = document.createElement('div'); 20 | container.innerHTML = html; 21 | document.body.appendChild(container); 22 | }); 23 | 24 | afterEach(() => { 25 | container.remove(); 26 | }); 27 | 28 | it('throws an error if the provided node is not a descendant of the root node', () => { 29 | const node = document.createElement('p'); // not attached to DOM 30 | assert.throws(() => { 31 | xpathFromNode(node, document.body); 32 | }, 'Node is not a descendant of root'); 33 | }); 34 | 35 | [ 36 | { 37 | id: 'a-1', 38 | xpaths: ['/div[1]/p[1]/a[1]', '/div[1]/p[1]/a[1]/text()[1]'], 39 | }, 40 | { 41 | id: 'h1-1', 42 | xpaths: ['/div[1]/h1[1]', '/div[1]/h1[1]/text()[1]'], 43 | }, 44 | { 45 | id: 'p-1', 46 | xpaths: ['/div[1]/p[1]', '/div[1]/p[1]/text()[1]'], 47 | }, 48 | { 49 | id: 'a-1', 50 | xpaths: ['/div[1]/p[1]/a[1]', '/div[1]/p[1]/a[1]/text()[1]'], 51 | }, 52 | { 53 | id: 'p-2', 54 | xpaths: [ 55 | '/div[1]/p[2]', 56 | '/div[1]/p[2]/text()[1]', 57 | '/div[1]/p[2]/text()[2]', 58 | ], 59 | }, 60 | { 61 | id: 'em-1', 62 | xpaths: ['/div[1]/p[2]/em[1]', '/div[1]/p[2]/em[1]/text()[1]'], 63 | }, 64 | { 65 | id: 'li-3', 66 | xpaths: [ 67 | '/div[1]/span[1]/ul[1]/li[3]', 68 | '/div[1]/span[1]/ul[1]/li[3]/text()[1]', 69 | ], 70 | }, 71 | ].forEach(test => { 72 | it('produces the correct xpath for the provided node', () => { 73 | let node = document.getElementById(test.id); 74 | assert.equal(xpathFromNode(node, document.body), test.xpaths[0]); 75 | }); 76 | 77 | it('produces the correct xpath for the provided text node(s)', () => { 78 | let node = document.getElementById(test.id).firstChild; 79 | // collect all text nodes after the target queried node. 80 | const textNodes = []; 81 | while (node) { 82 | if (node.nodeType === Node.TEXT_NODE) { 83 | textNodes.push(node); 84 | } 85 | node = node.nextSibling; 86 | } 87 | textNodes.forEach((node, index) => { 88 | assert.equal( 89 | xpathFromNode(node, document.body), 90 | test.xpaths[index + 1] 91 | ); 92 | }); 93 | }); 94 | }); 95 | }); 96 | 97 | describe('nodeFromXPath', () => { 98 | let container; 99 | const html = ` 100 |

text

101 |

text

text

102 |

text

text
text

103 | 104 |
    105 |
  • text 1
  • 106 |
  • text 2
  • 107 |
  • text 3
  • 108 | text 4 109 |
110 | 111 | x+1 112 | 113 | 114 | Hello 115 | world 116 | 117 |
`; 118 | 119 | beforeEach(() => { 120 | container = document.createElement('div'); 121 | container.innerHTML = html; 122 | document.body.appendChild(container); 123 | 124 | sinon.spy(document, 'evaluate'); 125 | }); 126 | 127 | afterEach(() => { 128 | document.evaluate.restore(); 129 | 130 | container.remove(); 131 | }); 132 | 133 | [ 134 | // Single element path 135 | { 136 | xpath: '/h1[1]', 137 | nodeName: 'H1', 138 | }, 139 | // Multi-element path 140 | { 141 | xpath: '/p[1]/a[1]', 142 | nodeName: 'A', 143 | }, 144 | { 145 | xpath: '/span[1]/ul[1]/li[2]', 146 | nodeName: 'LI', 147 | }, 148 | // Upper-case element names 149 | { 150 | xpath: '/SPAN[1]/UL[1]/LI[2]', 151 | nodeName: 'LI', 152 | }, 153 | // Element path with implicit `[1]` index 154 | { 155 | xpath: '/h1', 156 | nodeName: 'H1', 157 | }, 158 | // Custom element 159 | { 160 | xpath: '/span/ul/custom-element', 161 | nodeName: 'CUSTOM-ELEMENT', 162 | }, 163 | // Embedded MathML 164 | { 165 | xpath: '/span/math/msqrt/mrow/mi', 166 | nodeName: 'mi', 167 | }, 168 | { 169 | xpath: '/SPAN/MATH/MSQRT/MROW/MI', 170 | nodeName: 'mi', 171 | }, 172 | // Embedded SVG 173 | { 174 | xpath: '/span[1]/svg[1]/text[2]', 175 | nodeName: 'text', 176 | }, 177 | ].forEach(test => { 178 | it('evaluates simple XPaths without using `document.evaluate`', () => { 179 | const result = nodeFromXPath(test.xpath, container); 180 | assert.notCalled(document.evaluate); 181 | assert.equal(result?.nodeName, test.nodeName); 182 | }); 183 | }); 184 | 185 | ['/missing/element', '/span[0]'].forEach(xpath => { 186 | it('returns `null` if simple XPath evaluation fails', () => { 187 | const result = nodeFromXPath(xpath, container); 188 | assert.notCalled(document.evaluate); 189 | assert.strictEqual(result, null); 190 | }); 191 | }); 192 | 193 | [ 194 | ['/*[local-name()="h1"]', 'H1'], 195 | ['/span[-1]', null], 196 | ].forEach(([xpath, expectedNodeName]) => { 197 | it('uses `document.evaluate` for non-simple XPaths', () => { 198 | const result = nodeFromXPath(xpath, container); 199 | assert.calledOnce(document.evaluate); 200 | assert.strictEqual(result?.nodeName ?? result, expectedNodeName); 201 | }); 202 | }); 203 | 204 | ['not-a-valid-xpath'].forEach(xpath => { 205 | it('throws if XPath is invalid', () => { 206 | assert.throws(() => { 207 | nodeFromXPath(xpath, container); 208 | }, /The string '.*' is not a valid XPath expression/); 209 | }); 210 | }); 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/src/vendor/hypothesis/anchoring/xpath.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the node name for use in generating an xpath expression. 3 | * 4 | * @param {Node} node 5 | */ 6 | function getNodeName(node) { 7 | const nodeName = node.nodeName.toLowerCase(); 8 | let result = nodeName; 9 | if (nodeName === '#text') { 10 | result = 'text()'; 11 | } 12 | return result; 13 | } 14 | 15 | /** 16 | * Get the index of the node as it appears in its parent's child list 17 | * 18 | * @param {Node} node 19 | */ 20 | function getNodePosition(node) { 21 | let pos = 0; 22 | /** @type {Node|null} */ 23 | let tmp = node; 24 | while (tmp) { 25 | if (tmp.nodeName === node.nodeName) { 26 | pos += 1; 27 | } 28 | tmp = tmp.previousSibling; 29 | } 30 | return pos; 31 | } 32 | 33 | function getPathSegment(node) { 34 | const name = getNodeName(node); 35 | const pos = getNodePosition(node); 36 | return `${name}[${pos}]`; 37 | } 38 | 39 | /** 40 | * A simple XPath generator which can generate XPaths of the form 41 | * /tag[index]/tag[index]. 42 | * 43 | * @param {Node} node - The node to generate a path to 44 | * @param {Node} root - Root node to which the returned path is relative 45 | */ 46 | export function xpathFromNode(node, root) { 47 | let xpath = ''; 48 | 49 | /** @type {Node|null} */ 50 | let elem = node; 51 | while (elem !== root) { 52 | if (!elem) { 53 | throw new Error('Node is not a descendant of root'); 54 | } 55 | xpath = getPathSegment(elem) + '/' + xpath; 56 | elem = elem.parentNode; 57 | } 58 | xpath = '/' + xpath; 59 | xpath = xpath.replace(/\/$/, ''); // Remove trailing slash 60 | 61 | return xpath; 62 | } 63 | 64 | /** 65 | * Return the `index`'th immediate child of `element` whose tag name is 66 | * `nodeName` (case insensitive). 67 | * 68 | * @param {Element} element 69 | * @param {string} nodeName 70 | * @param {number} index 71 | */ 72 | function nthChildOfType(element, nodeName, index) { 73 | nodeName = nodeName.toUpperCase(); 74 | 75 | let matchIndex = -1; 76 | for (let i = 0; i < element.children.length; i++) { 77 | const child = element.children[i]; 78 | if (child.nodeName.toUpperCase() === nodeName) { 79 | ++matchIndex; 80 | if (matchIndex === index) { 81 | return child; 82 | } 83 | } 84 | } 85 | 86 | return null; 87 | } 88 | 89 | /** 90 | * Evaluate a _simple XPath_ relative to a `root` element and return the 91 | * matching element. 92 | * 93 | * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings. 94 | * 95 | * Unlike `document.evaluate` this function: 96 | * 97 | * - Only supports simple XPaths 98 | * - Is not affected by the document's _type_ (HTML or XML/XHTML) 99 | * - Ignores element namespaces when matching element names in the XPath against 100 | * elements in the DOM tree 101 | * - Is case insensitive for all elements, not just HTML elements 102 | * 103 | * The matching element is returned or `null` if no such element is found. 104 | * An error is thrown if `xpath` is not a simple XPath. 105 | * 106 | * @param {string} xpath 107 | * @param {Element} root 108 | * @return {Element|null} 109 | */ 110 | function evaluateSimpleXPath(xpath, root) { 111 | const isSimpleXPath = 112 | xpath.match(/^(\/[A-Za-z0-9-]+(\[[0-9]+\])?)+$/) !== null; 113 | if (!isSimpleXPath) { 114 | throw new Error('Expression is not a simple XPath'); 115 | } 116 | 117 | const segments = xpath.split('/'); 118 | let element = root; 119 | 120 | // Remove leading empty segment. The regex above validates that the XPath 121 | // has at least two segments, with the first being empty and the others non-empty. 122 | segments.shift(); 123 | 124 | for (let segment of segments) { 125 | let elementName; 126 | let elementIndex; 127 | 128 | const separatorPos = segment.indexOf('['); 129 | if (separatorPos !== -1) { 130 | elementName = segment.slice(0, separatorPos); 131 | 132 | const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']')); 133 | elementIndex = parseInt(indexStr) - 1; 134 | if (elementIndex < 0) { 135 | return null; 136 | } 137 | } else { 138 | elementName = segment; 139 | elementIndex = 0; 140 | } 141 | 142 | const child = nthChildOfType(element, elementName, elementIndex); 143 | if (!child) { 144 | return null; 145 | } 146 | 147 | element = child; 148 | } 149 | 150 | return element; 151 | } 152 | 153 | /** 154 | * Finds an element node using an XPath relative to `root` 155 | * 156 | * Example: 157 | * node = nodeFromXPath('/main/article[1]/p[3]', document.body) 158 | * 159 | * @param {string} xpath 160 | * @param {Element} [root] 161 | * @return {Node|null} 162 | */ 163 | export function nodeFromXPath(xpath, root = document.body) { 164 | try { 165 | return evaluateSimpleXPath(xpath, root); 166 | } catch (err) { 167 | return document.evaluate( 168 | '.' + xpath, 169 | root, 170 | 171 | // nb. The `namespaceResolver` and `result` arguments are optional in the spec 172 | // but required in Edge Legacy. 173 | null /* namespaceResolver */, 174 | XPathResult.FIRST_ORDERED_NODE_TYPE, 175 | null /* result */ 176 | ).singleNodeValue; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/_scripts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: "production", 5 | devtool: "eval-source-map", 6 | entry: { 7 | reflowable: "./src/index-reflowable.js", 8 | fixed: "./src/index-fixed.js", 9 | }, 10 | output: { 11 | filename: "readium-[name].js", 12 | path: path.resolve(__dirname, "../readium/scripts"), 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/divina/divinaPlayer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DiViNa Player 6 | 15 | 16 | 17 | 18 | 19 | 26 | 27 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/divina/divinaTouchHandling.js: -------------------------------------------------------------------------------- 1 | var singleTouchGesture = false; 2 | var startX = 0; 3 | var startY = 0; 4 | var availWidth = window.screen.availWidth; 5 | var availHeight = window.screen.availHeight; 6 | 7 | 8 | window.addEventListener("load", function(){ // on page load 9 | // Get screen X and Y sizes. 10 | // Events listeners for the touches. 11 | window.document.addEventListener("touchstart", handleTouchStart, false); 12 | window.document.addEventListener("touchend", handleTouchEnd, false); 13 | // When device orientation changes, screen X and Y sizes are recalculated. 14 | }, false); 15 | 16 | 17 | 18 | // When a touch is detected records its starting coordinates and if it's a singleTouchGesture. 19 | var handleTouchStart = function(event) { 20 | var node = event.target.nodeName.toUpperCase() 21 | if (node === 'A' || node === 'VIDEO') { 22 | console.log("Touched a link or video."); 23 | // singleTouchGesture = false; 24 | return; 25 | } 26 | console.log("Touch sent to native code."); 27 | singleTouchGesture = event.touches.length == 1; 28 | 29 | var touch = event.changedTouches[0]; 30 | 31 | startX = touch.screenX % availWidth; 32 | startY = touch.screenY % availHeight; 33 | 34 | }; 35 | 36 | // When a touch ends, check if any action has to be made, and contact native code. 37 | var handleTouchEnd = function(event) { 38 | if(!singleTouchGesture) { 39 | return; 40 | } 41 | 42 | var touch = event.changedTouches[0]; 43 | 44 | var relativeDistanceX = Math.abs(((touch.screenX % availWidth) - startX) / availWidth); 45 | var relativeDistanceY = Math.abs(((touch.screenY % availHeight) - startY) / availHeight); 46 | var touchDistance = Math.max(relativeDistanceX, relativeDistanceY); 47 | 48 | var scrollWidth = document.scrollWidth; 49 | var screenWidth = availWidth; 50 | var tapAreaWidth = availWidth * 0.2; 51 | 52 | if(touchDistance < 0.01) { 53 | var position = (touch.screenX % availWidth) / availWidth; 54 | if (position <= 0.2) { 55 | console.log("LeftTapped"); 56 | } else if(position >= 0.8) { 57 | console.log("RightTapped"); 58 | } else { 59 | console.log("CenterTapped"); 60 | Android.centerTapped(); 61 | } 62 | event.stopPropagation(); 63 | event.preventDefault(); 64 | return; 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/fonts/OpenDyslexic-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/r2-navigator/src/main/assets/readium/fonts/OpenDyslexic-Regular.otf -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/readium-css/ReadMe.md: -------------------------------------------------------------------------------- 1 | # Dealing with Languages/scripts 2 | 3 | You’ll find default stylesheets at the root of this folder, and languages/script-specific ones in their dedicated folder. 4 | 5 | This is a temporary solution to a complex issue (i18n), which is aligned with our current implementation so that we can start testing those specific styles as soon as possible. 6 | 7 | ## Right to Left 8 | 9 | If the publication has: 10 | 11 | - a `page-progression-direction` with the value set to `rtl`; 12 | - and language is either Arabic (`ar`), Farsi (`fa`) or Hebrew (`he`). 13 | 14 | Then stylesheets in the `rtl` folder should be used. 15 | 16 | It is also important the `dir` attribute and `xml:lang` be appended to the `html` element of each document if needed. 17 | 18 | Finally, page progression is impacted: 19 | 20 | - previous page is `right`; 21 | - next page is `left`. 22 | 23 | ### User settings 24 | 25 | Disabled user settings: 26 | 27 | - `hyphens`; 28 | - `word-spacing`; 29 | - `letter-spacing`. 30 | 31 | Added user settings: 32 | 33 | - `font-variant-ligatures` (mapped to `--USER__ligatures` CSS variable). 34 | 35 | ## CJK 36 | 37 | Chinese, Japanese, Korean, and Mongolian can be either written `horizontal-tb` or `vertical-*`. Consequently, there are stylesheets for horizontal and vertical writing modes. 38 | 39 | ### Horizontal 40 | 41 | If the publication has: 42 | 43 | - a `page-progression-direction` with the value set to `ltr` – or no attribute –; 44 | - and (at least one) language item is either Chinese (`zh`), Japanese (`ja`), or Korean (`ko`). 45 | 46 | Then stylesheets in the `cjk/horizontal` subfolder should be used. 47 | 48 | It is also important the `xml:lang` be appended to the `html` element of each document if needed. 49 | 50 | #### User settings 51 | 52 | Disabled user settings: 53 | 54 | - `text-align`; 55 | - `hyphens`; 56 | - paragraphs’ indent; 57 | - `word-spacing`; 58 | - `letter-spacing`. 59 | 60 | ### Vertical 61 | 62 | If the publication has: 63 | 64 | - a `page-progression-direction` with the value set to `rtl`; 65 | - and (at least one) language item is either Chinese (`zh`), Japanese (`ja`), or Korean (`ko`). 66 | 67 | Then stylesheets in the `cjk/vertical` subfolder should be used. 68 | 69 | It is also important the `xml:lang` be appended to the `html` element of each document if needed. You MUST NOT append a `dir` attribute. 70 | 71 | Please note the `vertical-rl` writing mode will be enforced in this case. 72 | 73 | Finally, page progression is impacted: 74 | 75 | - previous page (`right`) goes up; 76 | - next page (`left`) goes down. 77 | 78 | This means that taps/swipes should behave as usual in horizontal writing i.e. `x-axis` but the app programmatically handles the progression on the `y-axis`. You might therefore want to disable page-transition animations in this case. 79 | 80 | This is consistent with the Readium 1 implementation so the same logic can apply. 81 | 82 | #### User settings 83 | 84 | Disabled user settings: 85 | 86 | - `column-count` (number of columns); 87 | - `text-align`; 88 | - `hyphens`; 89 | - paragraphs’ indent; 90 | - `word-spacing`; 91 | - `letter-spacing`. 92 | 93 | ### EBPAJ Polyfill 94 | 95 | The EBPAJ template only references fonts from MS Windows so we must reference fonts from other platforms and override authors’ stylesheets. What we do in this polyfill is keeping their default value and providing fallbacks. 96 | 97 | You might want to load this polyfill (at the end, after `ReadiumCSS-after-cjk(-*)?.css`) only if you find one of the following metadata items in the OPF package: 98 | 99 | - version 1: `ebpaj-guide-1.0` 100 | - version 1.1: `1.1` 101 | 102 | Since we must use `@font-face` to align with their specific implementation (we have to go through 9–11 `local` sources in the worst-case scenario), expect a “rendering debt” though. Do not hesitate to report performance issues for this polyfill. 103 | 104 | ### Mongolian 105 | 106 | This is currently an edge case as we still have to see whether we want to support it and how we can support it. Indeed, the situation is the following: 107 | 108 | - Traditional is written `vertical-lr` so we can’t use `page-progression-direction` as an hint, and we must check if the language item (`mn`) is enough: 109 | - if `mn-Mong` is set, then `vertical-lr` must be used; 110 | - if `mn-Cyrl` is set, then the publication is in cyrillic and it is `horizontal-tb`. 111 | - We don’t currently support the `mn` language, and we can’t rely on system fonts to do so, we’ll have to embed one. 112 | 113 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/readium-css/ReadiumCSS-default.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Readium CSS (v. 1.0.0-beta.3) 3 | * Developers: Jiminy Panoz 4 | * Copyright (c) 2017. Readium Foundation. All rights reserved. 5 | * Use of this source code is governed by a BSD-style license which is detailed in the 6 | * LICENSE file present in the project repository where this source code is maintained. 7 | */@namespace url("http://www.w3.org/1999/xhtml");@namespace epub url("http://www.idpf.org/2007/ops");@namespace m url("http://www.w3.org/1998/Math/MathML/");@namespace svg url("http://www.w3.org/2000/svg");:root{--RS__compFontFamily:var(--RS__baseFontFamily);--RS__codeFontFamily:var(--RS__monospaceTf);--RS__typeScale:1.125;--RS__baseFontSize:100%;--RS__flowSpacing:1.5rem;--RS__paraSpacing:0;--RS__paraIndent:1em;--RS__linkColor:#0000EE;--RS__visitedColor:#551A8B}body{font-size:var(--RS__baseFontSize)}h1,h2,h3,h4,h5,h6{font-family:var(--RS__compFontFamily)}aside,blockquote,figure,footer,form,hr,p,pre{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing)}p{margin-top:var(--RS__paraSpacing);margin-bottom:var(--RS__paraSpacing);text-indent:var(--RS__paraIndent)}h1+p,h2+p,h3+p,h4+p,h5+p,h6+p,hr+p{text-indent:0}pre{font-family:var(--RS__codeFontFamily)}code,kbd,samp,tt{font-family:var(--RS__codeFontFamily)}sub,sup{position:relative;font-size:67.5%;line-height:1}sub{bottom:-.2ex}sup{bottom:0}:link{color:var(--RS__linkColor)}:visited{color:var(--RS__visitedColor)}h1{margin-top:calc(var(--RS__flowSpacing) * 2);margin-bottom:calc(var(--RS__flowSpacing) * 2);font-size:calc(((1em * var(--RS__typeScale)) * var(--RS__typeScale)) * var(--RS__typeScale))}h2{margin-top:calc(var(--RS__flowSpacing) * 2);margin-bottom:var(--RS__flowSpacing);font-size:calc((1em * var(--RS__typeScale)) * var(--RS__typeScale))}h3{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing);font-size:calc(1em * var(--RS__typeScale))}h4{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing);font-size:1em}h5{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing);font-size:1em;font-variant:small-caps}h6{margin-top:var(--RS__flowSpacing);margin-bottom:0;font-size:1em;text-transform:lowercase;font-variant:small-caps}dl,ol,ul{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing)}table{margin:var(--RS__flowSpacing) 0;border:1px solid currentColor;border-collapse:collapse;empty-cells:show}table>tr,tbody,tfoot,thead{vertical-align:top}th{text-align:left}td,th{padding:4px;border:1px solid currentColor} -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/readium-css/ReadiumCSS-ebpaj_fonts_patch.css: -------------------------------------------------------------------------------- 1 | /* Readium CSS 2 | EBPAJ Fonts Patch module 3 | 4 | A stylesheet improving EBPAJ @font-face declarations to cover all platforms 5 | 6 | Repo: https://github.com/readium/readium-css */ 7 | 8 | /* EBPAJ template only references fonts from MS Windows… 9 | so we must reference fonts from other platforms 10 | and override authors’ stylesheets. 11 | What we do there is keeping their default value and providing fallbacks. 12 | 13 | /!\ /!\ /!\ /!\ /!\ 14 | FYI, you might want to load this polyfill only if you find 15 | one of the following metadata items in the OPF package: 16 | - version 1: 17 | ebpaj-guide-1.0 18 | - version 1.1: 19 | 1.1 20 | */ 21 | 22 | /* 横組み用 (horizontal writing) */ 23 | 24 | @font-face { 25 | font-family: "serif-ja"; 26 | src: local("MS P明朝"), 27 | local("MS PMincho"), 28 | local("Hiragino Mincho Pro"), 29 | local("ヒラギノ明朝 Pro W3"), 30 | local("游明朝"), 31 | local("YuMincho"), 32 | local("MS 明朝"), 33 | local("MS Mincho"), 34 | local("Hiragino Mincho ProN"); 35 | } 36 | 37 | @font-face { 38 | font-family: "sans-serif-ja"; 39 | src: local("MS Pゴシック"), 40 | local("MS PGothic"), 41 | local("Hiragino Kaku Gothic Pro W3"), 42 | local("ヒラギノ角ゴ Pro W3"), 43 | local("Hiragino Sans GB"), 44 | local("ヒラギノ角ゴシック W3"), 45 | local("游ゴシック"), 46 | local("YuGothic"), 47 | local("MS ゴシック"), 48 | local("MS Gothic"), 49 | local("Hiragino Sans"); 50 | } 51 | 52 | /* 縦組み用 (vertical writing) */ 53 | 54 | @font-face { 55 | font-family: "serif-ja-v"; 56 | src: local("MS 明朝"), 57 | local("MS Mincho"), 58 | local("Hiragino Mincho Pro"), 59 | local("ヒラギノ明朝 Pro W3"), 60 | local("游明朝"), 61 | local("YuMincho"), 62 | local("MS P明朝"), 63 | local("MS PMincho"), 64 | local("Hiragino Mincho ProN"); 65 | } 66 | 67 | @font-face { 68 | font-family: "sans-serif-ja-v"; 69 | src: local("MS ゴシック"), 70 | local("MS Gothic"), 71 | local("Hiragino Kaku Gothic Pro W3"), 72 | local("ヒラギノ角ゴ Pro W3"), 73 | local("Hiragino Sans GB"), 74 | local("ヒラギノ角ゴシック W3"), 75 | local("游ゴシック"), 76 | local("YuGothic"), 77 | local("MS Pゴシック"), 78 | local("MS PGothic"), 79 | local("Hiragino Sans"); 80 | } -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/readium-css/cjk-horizontal/ReadiumCSS-default.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Readium CSS (v. 1.0.0-beta.3) 3 | * Developers: Jiminy Panoz 4 | * Copyright (c) 2017. Readium Foundation. All rights reserved. 5 | * Use of this source code is governed by a BSD-style license which is detailed in the 6 | * LICENSE file present in the project repository where this source code is maintained. 7 | */@namespace url("http://www.w3.org/1999/xhtml");@namespace epub url("http://www.idpf.org/2007/ops");@namespace m url("http://www.w3.org/1998/Math/MathML/");@namespace svg url("http://www.w3.org/2000/svg");:root{--RS__compFontFamily:var(--RS__baseFontFamily);--RS__codeFontFamily:var(--RS__monospaceTf);--RS__typeScale:1.125;--RS__baseFontSize:87.5%;--RS__flowSpacing:1.5rem;--RS__paraSpacing:0;--RS__paraIndent:1em;--RS__linkColor:#0000EE;--RS__visitedColor:#551A8B}:root:lang(zh){--RS__paraIndent:2em}:root{quotes:"\201c" "\201d" "\2018" "\2019"}body{font-size:var(--RS__baseFontSize);text-align:justify;text-justify:inter-character}h1,h2,h3,h4,h5,h6{font-family:var(--RS__baseFontFamily);text-align:left;text-align:start}aside,blockquote,figure,footer,form,hr,p,pre{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing)}p{margin-top:var(--RS__paraSpacing);margin-bottom:var(--RS__paraSpacing);text-indent:var(--RS__paraIndent)}pre{font-family:var(--RS__codeFontFamily)}code,kbd,samp,tt{font-family:var(--RS__codeFontFamily)}sub,sup{position:relative;font-size:67.5%;line-height:1}sub{bottom:-.2ex}sup{bottom:0}em{-webkit-text-emphasis:dot;-epub-text-emphasis:dot;text-emphasis:dot}:link{color:var(--RS__linkColor)}:visited{color:var(--RS__visitedColor)}h1{margin-top:calc(var(--RS__flowSpacing) * 2);margin-bottom:calc(var(--RS__flowSpacing) * 2);font-size:calc(((1em * var(--RS__typeScale)) * var(--RS__typeScale)) * var(--RS__typeScale));text-align:center}h2{margin-top:calc(var(--RS__flowSpacing) * 2);margin-bottom:var(--RS__flowSpacing);font-size:calc((1em * var(--RS__typeScale)) * var(--RS__typeScale));text-align:center}h3{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing);font-size:calc(1em * var(--RS__typeScale));text-align:center}h4{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing);font-family:var(--RS__compFontFamily);font-size:1em}h5{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing);font-family:var(--RS__compFontFamily);font-size:smaller}h6{margin-top:var(--RS__flowSpacing);margin-bottom:0;font-family:var(--RS__compFontFamily);font-size:smaller;font-weight:400}dl,ol,ul{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing)}table{margin:var(--RS__flowSpacing) 0;border:1px solid currentColor;border-collapse:collapse;empty-cells:show}table>tr,tbody,tfoot,thead{vertical-align:top}th{text-align:left}td,th{padding:4px;border:1px solid currentColor} -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/readium-css/cjk-vertical/ReadiumCSS-default.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Readium CSS (v. 1.0.0-beta.3) 3 | * Developers: Jiminy Panoz 4 | * Copyright (c) 2017. Readium Foundation. All rights reserved. 5 | * Use of this source code is governed by a BSD-style license which is detailed in the 6 | * LICENSE file present in the project repository where this source code is maintained. 7 | */@namespace url("http://www.w3.org/1999/xhtml");@namespace epub url("http://www.idpf.org/2007/ops");@namespace m url("http://www.w3.org/1998/Math/MathML/");@namespace svg url("http://www.w3.org/2000/svg");:root{--RS__compFontFamily:var(--RS__baseFontFamily);--RS__codeFontFamily:var(--RS__monospaceTf);--RS__typeScale:1.125;--RS__baseFontSize:87.5%;--RS__flowSpacing:1.5rem;--RS__paraSpacing:0;--RS__paraIndent:1em;--RS__linkColor:#0000EE;--RS__visitedColor:#551A8B}:root:lang(zh){--RS__paraIndent:2em}:lang("mn-Mong"){--RS__baseFontSize:100%}body{font-size:var(--RS__baseFontSize);text-align:justify;text-justify:inter-character}h1,h2,h3,h4,h5,h6{font-family:var(--RS__baseFontFamily);text-align:left;text-align:start}aside,blockquote,figure,footer,form,hr,p,pre{margin-right:var(--RS__flowSpacing);margin-left:var(--RS__flowSpacing)}p{margin-right:var(--RS__paraSpacing);margin-left:var(--RS__paraSpacing);text-indent:var(--RS__paraIndent)}pre{font-family:var(--RS__codeFontFamily)}code,kbd,samp,tt{font-family:var(--RS__codeFontFamily)}sub,sup{position:relative;font-size:67.5%;line-height:1}sub{left:-.2ex}sup{right:0}em{-webkit-text-emphasis:sesame;-epub-text-emphasis:sesame;text-emphasis:sesame}:link{color:var(--RS__linkColor)}:visited{color:var(--RS__visitedColor)}h1{margin-right:calc(var(--RS__flowSpacing) * 2);margin-left:calc(var(--RS__flowSpacing) * 2);font-size:calc(((1em * var(--RS__typeScale)) * var(--RS__typeScale)) * var(--RS__typeScale));text-indent:2rem}h2{margin-right:calc(var(--RS__flowSpacing) * 2);margin-left:var(--RS__flowSpacing);font-size:calc((1em * var(--RS__typeScale)) * var(--RS__typeScale));text-indent:3rem}h3{margin-right:var(--RS__flowSpacing);margin-left:var(--RS__flowSpacing);font-size:calc(1em * var(--RS__typeScale));text-indent:4rem}h4{margin-right:var(--RS__flowSpacing);margin-left:var(--RS__flowSpacing);font-family:var(--RS__compFontFamily);font-size:1em;text-indent:4rem}h5{margin-right:var(--RS__flowSpacing);margin-left:var(--RS__flowSpacing);font-family:var(--RS__compFontFamily);font-size:smaller;text-indent:4rem}h6{margin-right:var(--RS__flowSpacing);margin-left:0;font-family:var(--RS__compFontFamily);font-size:smaller;font-weight:400;text-indent:4rem}dl,ol,ul{margin-right:var(--RS__flowSpacing);margin-left:var(--RS__flowSpacing)}table{margin:0 var(--RS__flowSpacing);border:1px solid currentColor;border-collapse:collapse;empty-cells:show}table>tr,tbody,tfoot,thead{vertical-align:top}th{text-align:left}td,th{padding:4px;border:1px solid currentColor} -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/readium-css/fonts/AccessibleDfA.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/r2-navigator/src/main/assets/readium/readium-css/fonts/AccessibleDfA.otf -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/readium-css/fonts/LICENSE-AccessibleDfa: -------------------------------------------------------------------------------- 1 | Copyright (c) Orange 2015, www.orange.com 2 | with Reserved Font Name Accessible-Dfa. 3 | 4 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 5 | This license is copied below, and is also available with a FAQ at: 6 | http://scripts.sil.org/OFL 7 | 8 | 9 | ----------------------------------------------------------- 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 11 | ----------------------------------------------------------- 12 | 13 | PREAMBLE 14 | The goals of the Open Font License (OFL) are to stimulate worldwide 15 | development of collaborative font projects, to support the font creation 16 | efforts of academic and linguistic communities, and to provide a free and 17 | open framework in which fonts may be shared and improved in partnership 18 | with others. 19 | 20 | The OFL allows the licensed fonts to be used, studied, modified and 21 | redistributed freely as long as they are not sold by themselves. The 22 | fonts, including any derivative works, can be bundled, embedded, 23 | redistributed and/or sold with any software provided that any reserved 24 | names are not used by derivative works. The fonts and derivatives, 25 | however, cannot be released under any other type of license. The 26 | requirement for fonts to remain under this license does not apply 27 | to any document created using the fonts or their derivatives. 28 | 29 | DEFINITIONS 30 | "Font Software" refers to the set of files released by the Copyright 31 | Holder(s) under this license and clearly marked as such. This may 32 | include source files, build scripts and documentation. 33 | 34 | "Reserved Font Name" refers to any names specified as such after the 35 | copyright statement(s). 36 | 37 | "Original Version" refers to the collection of Font Software components as 38 | distributed by the Copyright Holder(s). 39 | 40 | "Modified Version" refers to any derivative made by adding to, deleting, 41 | or substituting -- in part or in whole -- any of the components of the 42 | Original Version, by changing formats or by porting the Font Software to a 43 | new environment. 44 | 45 | "Author" refers to any designer, engineer, programmer, technical 46 | writer or other person who contributed to the Font Software. 47 | 48 | PERMISSION & CONDITIONS 49 | Permission is hereby granted, free of charge, to any person obtaining 50 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 51 | redistribute, and sell modified and unmodified copies of the Font 52 | Software, subject to the following conditions: 53 | 54 | 1) Neither the Font Software nor any of its individual components, 55 | in Original or Modified Versions, may be sold by itself. 56 | 57 | 2) Original or Modified Versions of the Font Software may be bundled, 58 | redistributed and/or sold with any software, provided that each copy 59 | contains the above copyright notice and this license. These can be 60 | included either as stand-alone text files, human-readable headers or 61 | in the appropriate machine-readable metadata fields within text or 62 | binary files as long as those fields can be easily viewed by the user. 63 | 64 | 3) No Modified Version of the Font Software may use the Reserved Font 65 | Name(s) unless explicit written permission is granted by the corresponding 66 | Copyright Holder. This restriction only applies to the primary font name as 67 | presented to the users. 68 | 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 70 | Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except to acknowledge the contribution(s) of the 72 | Copyright Holder(s) and the Author(s) or with their explicit written 73 | permission. 74 | 75 | 5) The Font Software, modified or unmodified, in part or in whole, 76 | must be distributed entirely under this license, and must not be 77 | distributed under any other license. The requirement for fonts to 78 | remain under this license does not apply to any document created 79 | using the Font Software. 80 | 81 | TERMINATION 82 | This license becomes null and void if any of the above conditions are 83 | not met. 84 | 85 | DISCLAIMER 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 94 | OTHER DEALINGS IN THE FONT SOFTWARE. 95 | 96 | -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/readium-css/fonts/LICENSE-IaWriterDuospace.md: -------------------------------------------------------------------------------- 1 | # IBM Plex Typeface 2 | 3 | Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" 4 | 5 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 6 | This license is copied below, and is also available with a FAQ at: 7 | http://scripts.sil.org/OFL 8 | 9 | ----------------------------------------------------------- 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 11 | ----------------------------------------------------------- 12 | 13 | PREAMBLE 14 | The goals of the Open Font License (OFL) are to stimulate worldwide 15 | development of collaborative font projects, to support the font creation 16 | efforts of academic and linguistic communities, and to provide a free and 17 | open framework in which fonts may be shared and improved in partnership 18 | with others. 19 | 20 | The OFL allows the licensed fonts to be used, studied, modified and 21 | redistributed freely as long as they are not sold by themselves. The 22 | fonts, including any derivative works, can be bundled, embedded, 23 | redistributed and/or sold with any software provided that any reserved 24 | names are not used by derivative works. The fonts and derivatives, 25 | however, cannot be released under any other type of license. The 26 | requirement for fonts to remain under this license does not apply 27 | to any document created using the fonts or their derivatives. 28 | 29 | DEFINITIONS 30 | "Font Software" refers to the set of files released by the Copyright 31 | Holder(s) under this license and clearly marked as such. This may 32 | include source files, build scripts and documentation. 33 | 34 | "Reserved Font Name" refers to any names specified as such after the 35 | copyright statement(s). 36 | 37 | "Original Version" refers to the collection of Font Software components as 38 | distributed by the Copyright Holder(s). 39 | 40 | "Modified Version" refers to any derivative made by adding to, deleting, 41 | or substituting -- in part or in whole -- any of the components of the 42 | Original Version, by changing formats or by porting the Font Software to a 43 | new environment. 44 | 45 | "Author" refers to any designer, engineer, programmer, technical 46 | writer or other person who contributed to the Font Software. 47 | 48 | PERMISSION & CONDITIONS 49 | Permission is hereby granted, free of charge, to any person obtaining 50 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 51 | redistribute, and sell modified and unmodified copies of the Font 52 | Software, subject to the following conditions: 53 | 54 | 1) Neither the Font Software nor any of its individual components, 55 | in Original or Modified Versions, may be sold by itself. 56 | 57 | 2) Original or Modified Versions of the Font Software may be bundled, 58 | redistributed and/or sold with any software, provided that each copy 59 | contains the above copyright notice and this license. These can be 60 | included either as stand-alone text files, human-readable headers or 61 | in the appropriate machine-readable metadata fields within text or 62 | binary files as long as those fields can be easily viewed by the user. 63 | 64 | 3) No Modified Version of the Font Software may use the Reserved Font 65 | Name(s) unless explicit written permission is granted by the corresponding 66 | Copyright Holder. This restriction only applies to the primary font name as 67 | presented to the users. 68 | 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 70 | Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except to acknowledge the contribution(s) of the 72 | Copyright Holder(s) and the Author(s) or with their explicit written 73 | permission. 74 | 75 | 5) The Font Software, modified or unmodified, in part or in whole, 76 | must be distributed entirely under this license, and must not be 77 | distributed under any other license. The requirement for fonts to 78 | remain under this license does not apply to any document created 79 | using the Font Software. 80 | 81 | TERMINATION 82 | This license becomes null and void if any of the above conditions are 83 | not met. 84 | 85 | DISCLAIMER 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 94 | OTHER DEALINGS IN THE FONT SOFTWARE. 95 | 96 | 97 | # IBM Type Code 98 | 99 | Copyright 2017 IBM 100 | Licensed under the Apache License, Version 2.0 (the "License"); 101 | you may not use this file except in compliance with the License. 102 | You may obtain a copy of the License at 103 | 104 | http://www.apache.org/licenses/LICENSE-2.0 105 | 106 | Unless required by applicable law or agreed to in writing, software 107 | distributed under the License is distributed on an "AS IS" BASIS, 108 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 109 | See the License for the specific language governing permissions and 110 | limitations under the License. -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/readium-css/fonts/iAWriterDuospace-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/r2-navigator/src/main/assets/readium/readium-css/fonts/iAWriterDuospace-Regular.ttf -------------------------------------------------------------------------------- /r2-navigator/src/main/assets/readium/readium-css/rtl/ReadiumCSS-default.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Readium CSS (v. 1.0.0-beta.3) 3 | * Developers: Jiminy Panoz 4 | * Copyright (c) 2017. Readium Foundation. All rights reserved. 5 | * Use of this source code is governed by a BSD-style license which is detailed in the 6 | * LICENSE file present in the project repository where this source code is maintained. 7 | */@namespace url("http://www.w3.org/1999/xhtml");@namespace epub url("http://www.idpf.org/2007/ops");@namespace m url("http://www.w3.org/1998/Math/MathML/");@namespace svg url("http://www.w3.org/2000/svg");:root{--RS__compFontFamily:var(--RS__baseFontFamily);--RS__codeFontFamily:var(--RS__monospaceTf);--RS__typeScale:1.125;--RS__baseFontSize:100%;--RS__flowSpacing:1.5rem;--RS__paraSpacing:0;--RS__paraIndent:1em;--RS__linkColor:#0000EE;--RS__visitedColor:#551A8B}body{font-size:var(--RS__baseFontSize);text-align:justify}h1,h2,h3,h4,h5,h6{font-family:var(--RS__compFontFamily);text-align:right}aside,blockquote,figure,footer,form,hr,p,pre{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing)}p{margin-top:var(--RS__paraSpacing);margin-bottom:var(--RS__paraSpacing);text-indent:var(--RS__paraIndent)}h1+p,h2+p,h3+p,h4+p,h5+p,h6+p,hr+p{text-indent:0}pre{font-family:var(--RS__codeFontFamily)}code,kbd,samp,tt{font-family:var(--RS__codeFontFamily)}sub,sup{position:relative;font-size:67.5%;line-height:1}sub{bottom:-.2ex}sup{bottom:0}:link{color:var(--RS__linkColor)}:visited{color:var(--RS__visitedColor)}h1{margin-top:calc(var(--RS__flowSpacing) * 2);margin-bottom:calc(var(--RS__flowSpacing) * 2);font-size:calc(((1em * var(--RS__typeScale)) * var(--RS__typeScale)) * var(--RS__typeScale))}h2{margin-top:calc(var(--RS__flowSpacing) * 2);margin-bottom:var(--RS__flowSpacing);font-size:calc((1em * var(--RS__typeScale)) * var(--RS__typeScale))}h3{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing);font-size:calc(1em * var(--RS__typeScale))}h4{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing);font-size:1em}h5{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing);font-size:smaller}h6{margin-top:var(--RS__flowSpacing);margin-bottom:0;font-size:smaller;font-weight:400}dl,ol,ul{margin-top:var(--RS__flowSpacing);margin-bottom:var(--RS__flowSpacing)}table{margin:var(--RS__flowSpacing) 0;border:1px solid currentColor;border-collapse:collapse;empty-cells:show}table>tr,tbody,tfoot,thead{vertical-align:top}th{text-align:initial}td,th{padding:4px;border:1px solid currentColor} -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/Experimental.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator 8 | 9 | @RequiresOptIn(message = "Support for the Decorator API is still experimental. The API may be changed in the future without notice.") 10 | @Retention(AnnotationRetention.BINARY) 11 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) 12 | annotation class ExperimentalDecorator 13 | 14 | @RequiresOptIn(message = "The new Audiobook navigator is still experimental. The API may be changed in the future without notice.") 15 | @Retention(AnnotationRetention.BINARY) 16 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) 17 | annotation class ExperimentalAudiobook 18 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/GlobalVars.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi, Clément Baumann 4 | * 5 | * Copyright (c) 2018. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator 11 | 12 | /** 13 | * Created by aferditamuriqi on 10/3/17. 14 | */ 15 | 16 | 17 | /** 18 | * Global Parameters 19 | */ 20 | @Deprecated("Use Publication::localBaseUrlOf() instead") 21 | const val BASE_URL = "http://127.0.0.1" 22 | 23 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/IR2Activity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi 4 | * 5 | * Copyright (c) 2019. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator 11 | 12 | import android.content.SharedPreferences 13 | import android.view.View 14 | import org.readium.r2.navigator.pager.R2ViewPager 15 | import org.readium.r2.shared.publication.Publication 16 | 17 | interface IR2Activity { 18 | 19 | val publication: Publication 20 | val preferences: SharedPreferences 21 | val publicationIdentifier: String 22 | val publicationFileName: String 23 | val publicationPath: String 24 | val bookId: Long 25 | val resourcePager: R2ViewPager? 26 | get() = null 27 | val allowToggleActionBar: Boolean 28 | get() = true 29 | 30 | fun toggleActionBar() {} 31 | fun toggleActionBar(v: View? = null) {} 32 | fun nextResource(v: View? = null) {} 33 | fun previousResource(v: View? = null) {} 34 | fun onPageChanged(pageIndex: Int, totalPages: Int, url: String) {} 35 | fun onPageEnded(end: Boolean) {} 36 | fun onPageLoaded() {} 37 | fun highlightActivated(id: String) {} 38 | fun highlightAnnotationMarkActivated(id: String) {} 39 | } 40 | 41 | interface IR2TTS { 42 | fun playTextChanged(text: String) {} 43 | fun playStateChanged(playing: Boolean) {} 44 | fun dismissScreenReader() {} 45 | } 46 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/NavigatorFragmentFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Mickaël Menu 4 | * 5 | * Copyright (c) 2020. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator 11 | 12 | import androidx.fragment.app.FragmentFactory 13 | import org.readium.r2.shared.publication.Locator 14 | import org.readium.r2.shared.publication.Publication 15 | 16 | /** 17 | * Factory for the Navigator Fragments. 18 | * 19 | * @param publication Publication to render in the navigator. 20 | * @param baseUrl A base URL where this publication is served from, when relevant. This is required 21 | * only for an EPUB publication. 22 | * @param initialLocator The first location which should be visible when rendering the publication. 23 | * Can be used to restore the last reading location. 24 | * @param listener Optional listener to implement to observe events, such as user taps. 25 | */ 26 | @Deprecated("Each [Fragment] has now its own factory, such as `EpubNavigatorFragment.createFactory()`. To use a single [Activity] with several navigator fragments, you can compose the factories with [CompositeFragmentFactory].", level = DeprecationLevel.ERROR) 27 | class NavigatorFragmentFactory( 28 | private val publication: Publication, 29 | private val baseUrl: String? = null, 30 | private val initialLocator: Locator? = null, 31 | private val listener: Navigator.Listener? = null 32 | ) : FragmentFactory() 33 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/SelectableNavigator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator 8 | 9 | import android.graphics.RectF 10 | import org.readium.r2.shared.publication.Locator 11 | 12 | /** 13 | * A navigator supporting user selection. 14 | */ 15 | interface SelectableNavigator : Navigator { 16 | 17 | /** Currently selected content. */ 18 | suspend fun currentSelection(): Selection? 19 | 20 | /** Clears the current selection. */ 21 | fun clearSelection() 22 | } 23 | 24 | /** 25 | * Represents a user content selection in a navigator. 26 | * 27 | * @param locator Location of the user selection in the publication. 28 | * @param rect Frame of the bounding rect for the selection, in the coordinate of the navigator 29 | * view. This is only useful in the context of a VisualNavigator. 30 | */ 31 | data class Selection( 32 | val locator: Locator, 33 | val rect: RectF?, 34 | ) -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.audio 8 | 9 | import android.net.Uri 10 | import com.google.android.exoplayer2.C.LENGTH_UNSET 11 | import com.google.android.exoplayer2.C.RESULT_END_OF_INPUT 12 | import com.google.android.exoplayer2.upstream.BaseDataSource 13 | import com.google.android.exoplayer2.upstream.DataSource 14 | import com.google.android.exoplayer2.upstream.DataSpec 15 | import com.google.android.exoplayer2.upstream.TransferListener 16 | import kotlinx.coroutines.runBlocking 17 | import org.readium.r2.shared.fetcher.Resource 18 | import org.readium.r2.shared.fetcher.buffered 19 | import org.readium.r2.shared.publication.Publication 20 | import java.io.IOException 21 | 22 | /** 23 | * An ExoPlayer's [DataSource] which retrieves resources from a [Publication]. 24 | */ 25 | internal class PublicationDataSource(private val publication: Publication) : BaseDataSource(/* isNetwork = */ true) { 26 | 27 | class Factory(private val publication: Publication, private val transferListener: TransferListener? = null) : DataSource.Factory { 28 | 29 | override fun createDataSource(): DataSource = 30 | PublicationDataSource(publication).apply { 31 | if (transferListener != null) { 32 | addTransferListener(transferListener) 33 | } 34 | } 35 | 36 | } 37 | 38 | sealed class Exception(message: String, cause: Throwable?) : IOException(message, cause) { 39 | class NotOpened(message: String) : Exception(message, null) 40 | class NotFound(message: String) : Exception(message, null) 41 | class ReadFailed(uri: Uri, offset: Int, readLength: Int, cause: Throwable) : Exception("Failed to read $readLength bytes of URI $uri at offset $offset.", cause) 42 | } 43 | 44 | private data class OpenedResource( 45 | val resource: Resource, 46 | val uri: Uri, 47 | var position: Long, 48 | ) 49 | 50 | private var openedResource: OpenedResource? = null 51 | 52 | override fun open(dataSpec: DataSpec): Long { 53 | val link = publication.linkWithHref(dataSpec.uri.toString()) 54 | ?: throw Exception.NotFound("Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest.") 55 | 56 | val resource = publication.get(link) 57 | // Significantly improves performances, in particular with deflated ZIP entries. 58 | .buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) 59 | 60 | openedResource = OpenedResource( 61 | resource = resource, 62 | uri = dataSpec.uri, 63 | position = dataSpec.position, 64 | ) 65 | 66 | val bytesToRead = 67 | if (dataSpec.length != LENGTH_UNSET.toLong()) { 68 | dataSpec.length 69 | } else { 70 | val contentLength = contentLengthOf(dataSpec.uri, resource) 71 | ?: return dataSpec.length 72 | contentLength - dataSpec.position 73 | } 74 | 75 | return bytesToRead 76 | } 77 | 78 | /** Cached content lengths indexed by their URL. */ 79 | private var cachedLengths: MutableMap = mutableMapOf() 80 | 81 | private fun contentLengthOf(uri: Uri, resource: Resource): Long? { 82 | cachedLengths[uri.toString()]?.let { return it } 83 | 84 | val length = runBlocking { resource.length() }.getOrNull() 85 | ?: return null 86 | 87 | cachedLengths[uri.toString()] = length 88 | return length 89 | } 90 | 91 | override fun read(target: ByteArray, offset: Int, length: Int): Int { 92 | if (length <= 0) { 93 | return 0 94 | } 95 | 96 | val openedResource = openedResource ?: throw Exception.NotOpened("No opened resource to read from. Did you call open()?") 97 | 98 | try { 99 | val data = runBlocking { 100 | openedResource.resource 101 | .read(range = openedResource.position until (openedResource.position + length)) 102 | .getOrThrow() 103 | } 104 | 105 | if (data.isEmpty()) { 106 | return RESULT_END_OF_INPUT 107 | } 108 | 109 | data.copyInto( 110 | destination = target, 111 | destinationOffset = offset, 112 | startIndex = 0, 113 | endIndex = data.size 114 | ) 115 | 116 | openedResource.position += data.count() 117 | return data.count() 118 | 119 | } catch (e: Exception) { 120 | if (e is InterruptedException) { 121 | return 0 122 | } 123 | throw Exception.ReadFailed(uri = openedResource.uri, offset = offset, readLength = length, cause = e) 124 | } 125 | } 126 | 127 | override fun getUri(): Uri? = openedResource?.uri 128 | 129 | override fun close() { 130 | openedResource?.run { 131 | try { 132 | runBlocking { resource.close() } 133 | } catch (e: Exception) { 134 | if (e !is InterruptedException) { 135 | throw e 136 | } 137 | } 138 | } 139 | openedResource = null 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/audiobook/R2MediaPlayer.kt: -------------------------------------------------------------------------------- 1 | package org.readium.r2.navigator.audiobook 2 | 3 | import android.app.ProgressDialog 4 | import android.media.MediaPlayer 5 | import android.media.MediaPlayer.OnPreparedListener 6 | import android.net.Uri 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.launch 10 | import org.readium.r2.shared.publication.Link 11 | import java.io.IOException 12 | 13 | 14 | class R2MediaPlayer(private var items: List, private var callback: MediaPlayerCallback) : OnPreparedListener { 15 | 16 | private val uiScope = CoroutineScope(Dispatchers.Main) 17 | 18 | var progress: ProgressDialog? = null 19 | 20 | var mediaPlayer: MediaPlayer = MediaPlayer() 21 | 22 | val isPlaying: Boolean 23 | get() = mediaPlayer.isPlaying 24 | 25 | val duration: Double 26 | get() = mediaPlayer.duration.toDouble() //if (isPrepared) {mediaPlayer.duration.toDouble()}else {0.0} 27 | 28 | val currentPosition: Double 29 | get() = mediaPlayer.currentPosition.toDouble() //if (isPrepared) {mediaPlayer.currentPosition.toDouble()}else {0.0} 30 | 31 | var isPaused: Boolean 32 | var isPrepared: Boolean 33 | 34 | private var index: Int 35 | 36 | init { 37 | isPaused = false 38 | isPrepared = false 39 | index = 0 40 | if (mediaPlayer.isPlaying) { 41 | mediaPlayer.stop() 42 | mediaPlayer.release() 43 | } 44 | toggleProgress(true) 45 | } 46 | 47 | 48 | /** 49 | * Called when the media file is ready for playback. 50 | * 51 | * @param mp the MediaPlayer that is ready for playback 52 | */ 53 | override fun onPrepared(mp: MediaPlayer?) { 54 | toggleProgress(false) 55 | this.start() 56 | isPrepared = true 57 | callback.onPrepared() 58 | } 59 | 60 | fun startPlayer() { 61 | mediaPlayer.reset() 62 | try { 63 | mediaPlayer.setDataSource(Uri.parse(items[index].href).toString()) 64 | mediaPlayer.setOnPreparedListener(this) 65 | mediaPlayer.prepareAsync() 66 | toggleProgress(true) 67 | } catch (e: IllegalArgumentException) { 68 | e.printStackTrace() 69 | } catch (e: IllegalStateException) { 70 | e.printStackTrace() 71 | } catch (e: IOException) { 72 | e.printStackTrace() 73 | } 74 | } 75 | 76 | private fun toggleProgress(show: Boolean) { 77 | uiScope.launch { 78 | if (show) progress?.show() 79 | else progress?.hide() 80 | } 81 | } 82 | 83 | fun seekTo(progression: Any) { 84 | when (progression) { 85 | is Double -> mediaPlayer.seekTo(progression.toInt()) 86 | is Int -> mediaPlayer.seekTo(progression) 87 | else -> mediaPlayer.seekTo(progression.toString().toInt()) 88 | } 89 | } 90 | 91 | fun stop() { 92 | if (isPrepared) { 93 | mediaPlayer.stop() 94 | isPrepared = false 95 | } 96 | } 97 | 98 | fun pause() { 99 | if (isPrepared) { 100 | mediaPlayer.pause() 101 | isPaused = true 102 | } 103 | } 104 | 105 | fun start() { 106 | mediaPlayer.start() 107 | isPaused = false 108 | isPrepared = false 109 | mediaPlayer.setOnCompletionListener { 110 | callback.onComplete(index, it.currentPosition, it.duration) 111 | } 112 | } 113 | 114 | fun resume() { 115 | if (isPrepared) { 116 | mediaPlayer.start() 117 | isPaused = false 118 | } 119 | } 120 | 121 | fun goTo(index: Int) { 122 | this.index = index 123 | isPaused = false 124 | isPrepared = false 125 | if (mediaPlayer.isPlaying) { 126 | mediaPlayer.stop() 127 | } 128 | toggleProgress(true) 129 | } 130 | 131 | fun previous() { 132 | index -= 1 133 | isPaused = false 134 | isPrepared = false 135 | if (mediaPlayer.isPlaying) { 136 | mediaPlayer.stop() 137 | } 138 | toggleProgress(true) 139 | } 140 | 141 | fun next() { 142 | index += 1 143 | isPaused = false 144 | isPrepared = false 145 | if (mediaPlayer.isPlaying) { 146 | mediaPlayer.stop() 147 | } 148 | toggleProgress(true) 149 | } 150 | 151 | } 152 | 153 | interface MediaPlayerCallback { 154 | fun onPrepared() 155 | fun onComplete(index: Int, currentPosition: Int, duration: Int) 156 | } 157 | 158 | 159 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/divina/R2DiViNaActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi 4 | * 5 | * Copyright (c) 2018. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.divina 11 | 12 | import android.annotation.SuppressLint 13 | import android.app.Activity 14 | import android.content.Context 15 | import android.content.SharedPreferences 16 | import android.os.Bundle 17 | import android.view.View 18 | import android.webkit.WebView 19 | import androidx.appcompat.app.AppCompatActivity 20 | import androidx.webkit.WebViewClientCompat 21 | import kotlinx.coroutines.CoroutineScope 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.launch 24 | import org.readium.r2.navigator.* 25 | import org.readium.r2.navigator.databinding.ActivityR2DivinaBinding 26 | import org.readium.r2.shared.extensions.destroyPublication 27 | import org.readium.r2.shared.extensions.getPublication 28 | import org.readium.r2.shared.publication.Publication 29 | import kotlin.coroutines.CoroutineContext 30 | 31 | 32 | open class R2DiViNaActivity : AppCompatActivity(), CoroutineScope, IR2Activity, VisualNavigator.Listener { 33 | 34 | /** 35 | * Context of this scope. 36 | */ 37 | override val coroutineContext: CoroutineContext 38 | get() = Dispatchers.Main 39 | 40 | override lateinit var preferences: SharedPreferences 41 | override lateinit var publication: Publication 42 | override lateinit var publicationIdentifier: String 43 | override lateinit var publicationPath: String 44 | override lateinit var publicationFileName: String 45 | override var bookId: Long = -1 46 | 47 | lateinit var divinaWebView: R2BasicWebView 48 | 49 | private lateinit var binding: ActivityR2DivinaBinding 50 | 51 | @SuppressLint("SetJavaScriptEnabled") 52 | override fun onCreate(savedInstanceState: Bundle?) { 53 | super.onCreate(savedInstanceState) 54 | binding = ActivityR2DivinaBinding.inflate(layoutInflater) 55 | setContentView(binding.root) 56 | 57 | preferences = getSharedPreferences("org.readium.r2.settings", Context.MODE_PRIVATE) 58 | divinaWebView = binding.divinaWebView 59 | //divinaWebView.listener = this 60 | 61 | publication = intent.getPublication(this) 62 | publicationPath = intent.getStringExtra("publicationPath") ?: throw Exception("publicationPath required") 63 | publicationFileName = intent.getStringExtra("publicationFileName") ?: throw Exception("publicationFileName required") 64 | 65 | publicationIdentifier = publication.metadata.identifier ?: "" 66 | title = publication.metadata.title 67 | 68 | // Set up divinaWebView to enable JavaScript and access to local URLs 69 | divinaWebView.settings.javaScriptEnabled = true 70 | divinaWebView.settings.allowFileAccess = true 71 | divinaWebView.settings.allowFileAccessFromFileURLs = true 72 | divinaWebView.webViewClient = object : WebViewClientCompat() { 73 | 74 | override fun onPageFinished(view: WebView?, url: String?) { 75 | super.onPageFinished(view, url) 76 | // Define the JS toggleMenu function that will call Android's toggleActionBar 77 | // divinaWebView.evaluateJavascript("window.androidObj = function AndroidClass(){};", null) 78 | // divinaWebView.evaluateJavascript("window.androidObj.toggleMenu = function() { Android.toggleMenu() };", null) 79 | 80 | // Now launch the DiViNa player for the folderPath = publicationPath 81 | divinaWebView.evaluateJavascript("if (player) { player.openDiViNaFromPath('${publicationPath}'); };", null) 82 | } 83 | } 84 | divinaWebView.loadUrl("file:///android_asset/readium/divina/divinaPlayer.html") 85 | 86 | } 87 | 88 | @Suppress("DEPRECATION") 89 | override fun toggleActionBar() { 90 | launch { 91 | if (supportActionBar!!.isShowing) { 92 | divinaWebView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE 93 | or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 94 | or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 95 | or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 96 | or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar 97 | or View.SYSTEM_UI_FLAG_IMMERSIVE) 98 | } else { 99 | divinaWebView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE 100 | or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 101 | or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) 102 | } 103 | } 104 | } 105 | 106 | override fun finish() { 107 | setResult(Activity.RESULT_OK, intent) 108 | super.finish() 109 | } 110 | 111 | override fun onDestroy() { 112 | super.onDestroy() 113 | divinaWebView.evaluateJavascript("if (player) { player.destroy(); };", null) 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.epub 8 | 9 | import android.graphics.PointF 10 | import android.graphics.RectF 11 | import androidx.lifecycle.ViewModel 12 | import org.readium.r2.navigator.* 13 | import org.readium.r2.navigator.epub.extensions.javascriptForGroup 14 | import org.readium.r2.navigator.html.HtmlDecorationTemplates 15 | import org.readium.r2.navigator.util.createViewModelFactory 16 | import org.readium.r2.shared.publication.Link 17 | import kotlin.reflect.KClass 18 | 19 | @OptIn(ExperimentalDecorator::class) 20 | internal class EpubNavigatorViewModel( 21 | val decorationTemplates: HtmlDecorationTemplates 22 | ) : ViewModel() { 23 | 24 | data class RunScriptCommand(val script: String, val scope: Scope) { 25 | sealed class Scope { 26 | object CurrentResource : Scope() 27 | object LoadedResources : Scope() 28 | data class Resource(val href: String) : Scope() 29 | data class WebView(val webView: R2BasicWebView) : Scope() 30 | } 31 | } 32 | 33 | fun onResourceLoaded(link: Link?, webView: R2BasicWebView): RunScriptCommand { 34 | val templates = decorationTemplates.toJSON().toString() 35 | .replace("\\n", " ") 36 | var script = "readium.registerDecorationTemplates($templates);\n" 37 | 38 | if (link != null) { 39 | for ((group, decorations) in decorations) { 40 | val changes = decorations 41 | .filter { it.locator.href == link.href } 42 | .map { DecorationChange.Added(it) } 43 | 44 | val groupScript = changes.javascriptForGroup(group, decorationTemplates) ?: continue 45 | script += "$groupScript\n" 46 | } 47 | } 48 | 49 | return RunScriptCommand(script, scope = RunScriptCommand.Scope.WebView(webView)) 50 | } 51 | 52 | // Selection 53 | 54 | fun clearSelection(): RunScriptCommand = 55 | RunScriptCommand( 56 | "window.getSelection().removeAllRanges();", 57 | scope = RunScriptCommand.Scope.CurrentResource 58 | ) 59 | 60 | // Decorations 61 | 62 | /** Current decorations, indexed by the group name. */ 63 | private val decorations: MutableMap> = mutableMapOf() 64 | 65 | fun supportsDecorationStyle(style: KClass): Boolean = 66 | decorationTemplates.styles.containsKey(style) 67 | 68 | suspend fun applyDecorations(decorations: List, group: String): List { 69 | val source = this.decorations[group] ?: emptyList() 70 | val target = decorations.toList() 71 | this.decorations[group] = target 72 | 73 | val cmds = mutableListOf() 74 | 75 | if (target.isEmpty()) { 76 | cmds.add(RunScriptCommand( 77 | // The updates command are using `requestAnimationFrame()`, so we need it for 78 | // `clear()` as well otherwise we might recreate a highlight after it has been 79 | // cleared. 80 | "requestAnimationFrame(function () { readium.getDecorations('$group').clear(); });", 81 | scope = RunScriptCommand.Scope.LoadedResources 82 | )) 83 | } else { 84 | for ((href, changes) in source.changesByHref(target)) { 85 | val script = changes.javascriptForGroup(group, decorationTemplates) ?: continue 86 | cmds.add(RunScriptCommand(script, scope = RunScriptCommand.Scope.Resource(href))) 87 | } 88 | } 89 | 90 | return cmds 91 | } 92 | 93 | /** Decoration group listeners, indexed by the group name. */ 94 | private val decorationListeners: MutableMap> = mutableMapOf() 95 | 96 | fun addDecorationListener(group: String, listener: DecorableNavigator.Listener) { 97 | val listeners = decorationListeners[group] ?: emptyList() 98 | decorationListeners[group] = listeners + listener 99 | } 100 | 101 | fun removeDecorationListener(listener: DecorableNavigator.Listener) { 102 | for ((group, listeners) in decorationListeners) { 103 | decorationListeners[group] = listeners.filter { it != listener } 104 | } 105 | } 106 | 107 | fun onDecorationActivated(id: DecorationId, group: String, rect: RectF, point: PointF): Boolean { 108 | val listeners = decorationListeners[group] 109 | ?: return false 110 | 111 | val decoration = decorations[group] 112 | ?.firstOrNull { it.id == id } 113 | ?: return false 114 | 115 | val event = DecorableNavigator.OnActivatedEvent( 116 | decoration = decoration, group = group, rect = rect, point = point 117 | ) 118 | for (listener in listeners) { 119 | if (listener.onDecorationActivated(event)) { 120 | return true 121 | } 122 | } 123 | 124 | return false 125 | } 126 | 127 | companion object { 128 | fun createFactory(decorationTemplates: HtmlDecorationTemplates) = createViewModelFactory { 129 | EpubNavigatorViewModel(decorationTemplates) 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/epub/IR2Highlightable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Taehyun Kim, Seongjin Kim 4 | * 5 | * Copyright (c) 2019. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.epub 11 | 12 | import android.graphics.Rect 13 | import org.readium.r2.shared.publication.Locator 14 | 15 | interface IR2Highlightable { 16 | fun showHighlight(highlight: Highlight) 17 | 18 | fun showHighlights(highlights: Array) 19 | 20 | fun hideHighlightWithID(id: String) 21 | 22 | fun hideAllHighlights() 23 | 24 | fun rectangleForHighlightWithID(id: String, callback: (Rect?) -> Unit) 25 | 26 | fun rectangleForHighlightAnnotationMarkWithID(id: String): Rect? 27 | 28 | fun registerHighlightAnnotationMarkStyle(name: String, css: String) 29 | 30 | fun highlightActivated(id: String) 31 | 32 | fun highlightAnnotationMarkActivated(id: String) 33 | } 34 | 35 | data class Highlight( 36 | val id: String, 37 | val locator: Locator, 38 | val color: Int, 39 | val style: Style, 40 | val annotationMarkStyle: String? = null 41 | ) 42 | 43 | enum class Style { 44 | highlight, underline, strikethrough 45 | } -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/epub/IR2Selectable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Taehyun Kim, Seongjin Kim 4 | * 5 | * Copyright (c) 2019. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.epub 11 | 12 | import org.readium.r2.shared.publication.Locator 13 | 14 | interface IR2Selectable { 15 | fun currentSelection(callback: (Locator?) -> Unit) 16 | } -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/epub/extensions/Decoration.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.epub.extensions 8 | 9 | import org.json.JSONObject 10 | import org.readium.r2.navigator.Decoration 11 | import org.readium.r2.navigator.DecorationChange 12 | import org.readium.r2.navigator.ExperimentalDecorator 13 | import org.readium.r2.navigator.html.HtmlDecorationTemplates 14 | import timber.log.Timber 15 | 16 | // Decoration extensions related to HTML/EPUB. 17 | 18 | /** 19 | * Generates the JavaScript used to apply the receiver list of [DecorationChange] in a web view. 20 | */ 21 | @ExperimentalDecorator 22 | internal fun List.javascriptForGroup(group: String, templates: HtmlDecorationTemplates): String? { 23 | if (isEmpty()) return null 24 | 25 | return """ 26 | // Using requestAnimationFrame helps to make sure the page is fully laid out before adding the 27 | // decorations. 28 | requestAnimationFrame(function () { 29 | let group = readium.getDecorations('$group'); 30 | ${mapNotNull { it.javascript(templates) }.joinToString("\n")} 31 | }); 32 | """ 33 | } 34 | 35 | /** 36 | * Generates the JavaScript used to apply the receiver [DecorationChange] in a web view. 37 | */ 38 | @ExperimentalDecorator 39 | internal fun DecorationChange.javascript(templates: HtmlDecorationTemplates): String? { 40 | fun toJSON(decoration: Decoration): JSONObject? { 41 | val template = templates[decoration.style::class] ?: run { 42 | Timber.e("Decoration style not registered: ${decoration.style::class}") 43 | return null 44 | } 45 | return decoration.toJSON().apply { 46 | put("element", template.element(decoration)) 47 | } 48 | } 49 | 50 | return when (this) { 51 | is DecorationChange.Added -> 52 | toJSON(decoration)?.let { "group.add($it);" } 53 | 54 | is DecorationChange.Moved -> 55 | null // Not supported for now 56 | 57 | is DecorationChange.Removed -> 58 | "group.remove('$id');" 59 | 60 | is DecorationChange.Updated -> 61 | toJSON(decoration)?.let { "group.update($it);" } 62 | } 63 | } -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLOnDoubleTapListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi 4 | * 5 | * Copyright (c) 2018. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.epub.fxl 11 | 12 | class R2FXLOnDoubleTapListener(private var threeStep: Boolean) : R2FXLLayout.OnDoubleTapListener { 13 | 14 | override fun onDoubleTap(view: R2FXLLayout, info: R2FXLLayout.TapInfo): Boolean { 15 | try { 16 | if (threeStep) { 17 | threeStep(view, info.x, info.y) 18 | } else { 19 | twoStep(view, info.x, info.y) 20 | } 21 | } catch (e: ArrayIndexOutOfBoundsException) { 22 | // Can sometimes happen when getX() and getY() is called 23 | } 24 | 25 | return true 26 | } 27 | 28 | private fun twoStep(view: R2FXLLayout, x: Float, y: Float) { 29 | if (view.scale > view.minScale) { 30 | view.setScale(view.minScale, true) 31 | } else { 32 | view.setScale(view.maxScale, x, y, true) 33 | } 34 | } 35 | 36 | private fun threeStep(view: R2FXLLayout, x: Float, y: Float) { 37 | val scale = view.scale 38 | val medium = view.minScale + (view.maxScale - view.minScale) * 0.3f 39 | if (scale < medium) { 40 | view.setScale(medium, x, y, true) 41 | } else if (scale >= medium && scale < view.maxScale) { 42 | view.setScale(view.maxScale, x, y, true) 43 | } else { 44 | view.setScale(view.minScale, true) 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLScroller.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi 4 | * 5 | * Copyright (c) 2018. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.epub.fxl 11 | 12 | import android.content.Context 13 | import android.widget.OverScroller 14 | 15 | abstract class R2FXLScroller { 16 | 17 | abstract val isFinished: Boolean 18 | abstract val currX: Int 19 | abstract val currY: Int 20 | abstract fun computeScrollOffset(): Boolean 21 | abstract fun fling(startX: Int, startY: Int, velocityX: Int, velocityY: Int, minX: Int, maxX: Int, minY: Int, 22 | maxY: Int, overX: Int, overY: Int) 23 | abstract fun forceFinished(finished: Boolean) 24 | 25 | private class Scroller internal constructor(context: Context) : R2FXLScroller() { 26 | 27 | internal var scroller: OverScroller = OverScroller(context) 28 | 29 | override val isFinished: Boolean 30 | get() = scroller.isFinished 31 | 32 | override val currX: Int 33 | get() = scroller.currX 34 | 35 | override val currY: Int 36 | get() = scroller.currY 37 | 38 | override fun computeScrollOffset(): Boolean { 39 | return scroller.computeScrollOffset() 40 | } 41 | 42 | override fun fling(startX: Int, startY: Int, velocityX: Int, velocityY: Int, minX: Int, maxX: Int, minY: Int, maxY: Int, overX: Int, overY: Int) { 43 | scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) 44 | } 45 | 46 | override fun forceFinished(finished: Boolean) { 47 | scroller.forceFinished(finished) 48 | } 49 | 50 | } 51 | 52 | companion object { 53 | fun getScroller(context: Context): R2FXLScroller { 54 | return Scroller(context) 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi 4 | * 5 | * Copyright (c) 2018. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.epub.fxl 11 | 12 | import android.graphics.Rect 13 | import android.graphics.RectF 14 | import kotlin.math.roundToInt 15 | import kotlin.math.roundToLong 16 | 17 | object R2FXLUtils { 18 | 19 | /** 20 | * Round and set the values on the rectangle 21 | * @param rect the rectangle to set 22 | * @param array the array to read the values from 23 | */ 24 | fun setRect(rect: Rect, array: FloatArray) { 25 | setRect(rect, array[0], array[1], array[2], array[3]) 26 | } 27 | 28 | /** 29 | * Round and set the values on the rectangle 30 | * @param rect the rectangle to set 31 | * @param array the array to read the values from 32 | */ 33 | fun setRect(rect: RectF, array: FloatArray) { 34 | setRect(rect, array[0], array[1], array[2], array[3]) 35 | } 36 | 37 | /** 38 | * Round and set the values on the rectangle 39 | * @param rect the rectangle to set 40 | * @param left left 41 | * @param top top 42 | * @param right right 43 | * @param bottom bottom 44 | */ 45 | fun setRect(rect: RectF, left: Float, top: Float, right: Float, bottom: Float) { 46 | rect.set(left.roundToLong().toFloat(), top.roundToLong().toFloat(), right.roundToLong().toFloat(), bottom.roundToLong().toFloat()) 47 | } 48 | 49 | /** 50 | * Round and set the values on the rectangle 51 | * @param rect the rectangle to set 52 | * @param left left 53 | * @param top top 54 | * @param right right 55 | * @param bottom bottom 56 | */ 57 | private fun setRect(rect: Rect, left: Float, top: Float, right: Float, bottom: Float) { 58 | rect.set(left.roundToInt(), top.roundToInt(), right.roundToInt(), bottom.roundToInt()) 59 | } 60 | 61 | fun setArray(array: FloatArray, rect: Rect) { 62 | array[0] = rect.left.toFloat() 63 | array[1] = rect.top.toFloat() 64 | array[2] = rect.right.toFloat() 65 | array[3] = rect.bottom.toFloat() 66 | } 67 | 68 | fun setArray(array: FloatArray, rect: RectF) { 69 | array[0] = rect.left 70 | array[1] = rect.top 71 | array[2] = rect.right 72 | array[3] = rect.bottom 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/extensions/ControlFlow.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.extensions 8 | 9 | /** 10 | * Unwraps the two given arguments and pass them to the [closure] if they are not null. 11 | */ 12 | internal inline fun let(a: A?, b: B?, closure: (A, B) -> R?): R? = 13 | if (a == null || b == null) { 14 | null 15 | } else { 16 | closure(a, b) 17 | } 18 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/extensions/Duration.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.extensions 8 | 9 | import android.text.format.DateUtils 10 | import kotlin.time.Duration 11 | import kotlin.time.DurationUnit 12 | import kotlin.time.ExperimentalTime 13 | import kotlin.time.seconds 14 | 15 | @ExperimentalTime 16 | internal fun List.sum(): Duration = 17 | fold(0.seconds) { a, b -> a + b } 18 | 19 | @JvmName("sumNullable") 20 | @ExperimentalTime 21 | internal fun List.sum(): Duration = 22 | fold(0.seconds) { a, b -> a + (b ?: 0.seconds) } 23 | 24 | @ExperimentalTime 25 | internal fun Duration.formatElapsedTime(): String = 26 | DateUtils.formatElapsedTime(toLong(DurationUnit.SECONDS)) 27 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/extensions/Extensions.kt: -------------------------------------------------------------------------------- 1 | package org.readium.r2.navigator.extensions 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import androidx.annotation.ColorInt 6 | import androidx.annotation.ColorRes 7 | import androidx.core.content.ContextCompat 8 | import androidx.core.view.ViewCompat 9 | 10 | /** 11 | * Extensions 12 | */ 13 | 14 | 15 | /** returns true if the resolved layout direction of the content view in this 16 | * activity is ViewCompat.LAYOUT_DIRECTION_RTL. Otherwise false. */ 17 | fun Activity.layoutDirectionIsRTL(): Boolean { 18 | return ViewCompat.getLayoutDirection(findViewById(android.R.id.content)) == ViewCompat.LAYOUT_DIRECTION_RTL 19 | } 20 | 21 | 22 | @ColorInt 23 | fun Context.color(@ColorRes id: Int): Int { 24 | return ContextCompat.getColor(this, id) 25 | } -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/extensions/JSON.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.extensions 8 | 9 | import android.graphics.RectF 10 | import org.json.JSONObject 11 | 12 | /** 13 | * Parses a [RectF] from its JSON representation. 14 | */ 15 | fun JSONObject.optRectF(name: String): RectF? = 16 | optJSONObject(name)?.let { json -> 17 | val left = json.optDouble("left").toFloat() 18 | val top = json.optDouble("top").toFloat() 19 | val right = json.optDouble("right").toFloat() 20 | val bottom = json.optDouble("bottom").toFloat() 21 | RectF(left, top, right, bottom) 22 | } 23 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/extensions/Link.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Quentin Gliosca 4 | * 5 | * Copyright (c) 2020. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.extensions 11 | 12 | import android.net.Uri 13 | import org.readium.r2.shared.publication.Link 14 | import org.readium.r2.shared.publication.Publication 15 | 16 | internal fun Link.withBaseUrl(baseUrl: String): Link { 17 | // Already an absolute URL? 18 | if (Uri.parse(href).scheme != null) { 19 | return this 20 | } 21 | 22 | check(!baseUrl.endsWith("/")) 23 | check(href.startsWith("/")) 24 | return copy(href = baseUrl + href) 25 | } 26 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/extensions/Locator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Mickaël Menu 4 | * 5 | * Copyright (c) 2020. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.extensions 11 | 12 | import org.readium.r2.shared.publication.Locator 13 | import java.util.* 14 | import kotlin.time.Duration 15 | import kotlin.time.ExperimentalTime 16 | import kotlin.time.seconds 17 | 18 | // FIXME: This should be in r2-shared once this public API is specified. 19 | 20 | // Reference: https://www.w3.org/TR/fragid-best-practices 21 | 22 | /** 23 | * All named parameters found in the fragments, such as `p=5`. 24 | */ 25 | internal val Locator.Locations.fragmentParameters: Map get() = 26 | fragments 27 | // Concatenates fragments together, after dropping any # 28 | .map { it.removePrefix("#") } 29 | .joinToString(separator = "&") 30 | // Splits parameters 31 | .split("&") 32 | .filter { !it.startsWith("=") } 33 | .map { it.split("=") } 34 | // Only keep named parameters 35 | .filter { it.size == 2 } 36 | .associate { it[0].trim().lowercase(Locale.ROOT) to it[1].trim() } 37 | 38 | /** 39 | * HTML ID fragment identifier. 40 | */ 41 | internal val Locator.Locations.htmlId: String? get() { 42 | // The HTML 5 specification (used for WebPub) allows any character in an HTML ID, except 43 | // spaces. This is an issue to differentiate with named parameters, so we ignore any 44 | // ID containing `=`. 45 | val id = fragments.firstOrNull { !it.isBlank() && !it.contains("=") } 46 | ?: fragmentParameters["id"] 47 | ?: fragmentParameters["name"] 48 | 49 | return id?.removePrefix("#") 50 | } 51 | 52 | /** 53 | * Page fragment identifier, used for example in PDF. 54 | */ 55 | internal val Locator.Locations.page: Int? get() = 56 | fragmentParameters["page"]?.toIntOrNull() 57 | 58 | /** 59 | * Media fragment, used for example in audiobooks. 60 | * 61 | * https://www.w3.org/TR/media-frags/ 62 | */ 63 | @OptIn(ExperimentalTime::class) 64 | internal val Locator.Locations.time: Duration? get() = 65 | fragmentParameters["t"]?.toIntOrNull()?.seconds 66 | 67 | /** 68 | * Computes the time position from the resource duration. 69 | */ 70 | @OptIn(ExperimentalTime::class) 71 | internal fun Locator.Locations.timeWithDuration(duration: Duration?): Duration? = 72 | let(duration, progression) { d, p -> (p * d.inSeconds).seconds } 73 | ?: time 74 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/extensions/Publication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Mickaël Menu 4 | * 5 | * Copyright (c) 2020. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.extensions 11 | 12 | import kotlinx.coroutines.runBlocking 13 | import org.readium.r2.shared.extensions.tryOrNull 14 | import org.readium.r2.shared.publication.Locator 15 | import org.readium.r2.shared.publication.Publication 16 | import org.readium.r2.shared.publication.services.positions 17 | import java.net.URL 18 | 19 | /** Computes an absolute URL to the given HREF. */ 20 | internal fun Publication.urlToHref(href: String): URL? { 21 | val baseUrl = this.baseUrl?.toString()?.removeSuffix("/") 22 | val urlString = if (baseUrl != null && href.startsWith("/")) { 23 | "$baseUrl${href}" 24 | } else { 25 | href 26 | } 27 | return tryOrNull { URL(urlString) } 28 | } 29 | 30 | // These extensions will be removed in the next release, with `PositionsService`. 31 | 32 | internal val Publication.positionsSync: List 33 | get() = runBlocking { positions() } 34 | 35 | internal val Publication.positionsByResource: Map> 36 | get() = runBlocking { positions().groupBy { it.href } } 37 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/extensions/String.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.extensions 8 | 9 | /** 10 | * Splits a [String] in two components, at the given delimiter. 11 | */ 12 | // FIXME: Move to an internal module when the monorepo will be setup 13 | internal fun String.splitAt(delimiter: String): Pair { 14 | val components = split(delimiter, limit = 2) 15 | return Pair(components[0], components.getOrNull(1)) 16 | } 17 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayback.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.media 8 | 9 | import org.readium.r2.navigator.ExperimentalAudiobook 10 | import kotlin.time.Duration 11 | import kotlin.time.ExperimentalTime 12 | 13 | /** 14 | * State of the playback at a point in time. 15 | * 16 | * @param state State of the playback. 17 | * @param rate Speed of the playback, defaults to 1.0. 18 | * @param timeline Position and duration of the current resource. 19 | */ 20 | @OptIn(ExperimentalTime::class) 21 | @ExperimentalAudiobook 22 | data class MediaPlayback(val state: State, val rate: Double, val timeline: Timeline) { 23 | 24 | enum class State { 25 | Idle, Loading, Playing, Paused; 26 | 27 | val isPlaying: Boolean get() = 28 | (this == Playing || this == Loading) 29 | } 30 | 31 | data class Timeline( 32 | val position: Duration, 33 | val duration: Duration?, 34 | val buffered: Duration? 35 | ) 36 | 37 | val isPlaying: Boolean get() = state.isPlaying 38 | 39 | } 40 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.media 8 | 9 | import android.app.Notification 10 | import android.graphics.Bitmap 11 | import android.os.Bundle 12 | import android.os.ResultReceiver 13 | import org.readium.r2.navigator.ExperimentalAudiobook 14 | import org.readium.r2.shared.fetcher.Resource 15 | import org.readium.r2.shared.publication.Link 16 | import org.readium.r2.shared.publication.Locator 17 | import org.readium.r2.shared.publication.Publication 18 | import org.readium.r2.shared.publication.PublicationId 19 | 20 | /** 21 | * Media player compatible with Android's MediaSession and handling the playback for 22 | * [MediaSessionNavigator]. 23 | * 24 | * MediaSession works with media IDs associated with a bundle of extras. To work with 25 | * [MediaService], implementers MUST map a location in the [Publication] to a media ID 26 | * `publicationId#resourceHref` with a [Locator] as a `locator` extra field. 27 | */ 28 | @ExperimentalAudiobook 29 | interface MediaPlayer { 30 | 31 | data class NotificationMetadata( 32 | val publicationTitle: String?, 33 | val trackTitle: String?, 34 | val authors: String? 35 | ) { 36 | constructor(publication: Publication, link: Link) : this( 37 | publicationTitle = publication.metadata.title, 38 | trackTitle = link.title, 39 | authors = publication.metadata.authors.joinToString(", ") { it.name }.takeIf { it.isNotBlank() } 40 | ) 41 | } 42 | 43 | interface Listener { 44 | 45 | fun locatorFromMediaId(mediaId: String, extras: Bundle?): Locator? 46 | 47 | suspend fun coverOfPublication(publication: Publication, publicationId: PublicationId): Bitmap? 48 | 49 | fun onNotificationPosted(notificationId: Int, notification: Notification) 50 | fun onNotificationCancelled(notificationId: Int) 51 | fun onCommand(command: String, args: Bundle?, cb: ResultReceiver?): Boolean 52 | 53 | fun onPlayerStopped() 54 | 55 | /** 56 | * Called when a resource failed to be loaded, for example because the Internet connection 57 | * is offline and the resource is streamed. 58 | */ 59 | fun onResourceLoadFailed(link: Link, error: Resource.Exception) 60 | 61 | /** 62 | * Creates the [NotificationMetadata] for the given resource [link]. 63 | * 64 | * The metadata will be used for the media-style notification. 65 | */ 66 | fun onCreateNotificationMetadata(publication: Publication, publicationId: PublicationId, link: Link): NotificationMetadata 67 | } 68 | 69 | // FIXME: ExoPlayer's media session connector doesn't handle the playback speed yet, so I used a custom solution until we create our own connector 70 | var playbackRate: Double 71 | 72 | var listener: Listener? 73 | 74 | fun onDestroy() 75 | 76 | } -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/media/PendingMedia.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.media 8 | 9 | import org.readium.r2.navigator.ExperimentalAudiobook 10 | import org.readium.r2.shared.publication.Locator 11 | import org.readium.r2.shared.publication.Publication 12 | import org.readium.r2.shared.publication.PublicationId 13 | 14 | /** 15 | * Holds information about a media-based [publication] waiting to be rendered by a [MediaPlayer]. 16 | */ 17 | @ExperimentalAudiobook 18 | data class PendingMedia( 19 | val publication: Publication, 20 | val publicationId: PublicationId, 21 | val locator: Locator 22 | ) 23 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/media/extensions/MediaControllerCompat.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.media.extensions 8 | 9 | import android.support.v4.media.session.MediaControllerCompat 10 | import org.readium.r2.navigator.ExperimentalAudiobook 11 | import org.readium.r2.navigator.media.MediaService 12 | import org.readium.r2.shared.publication.PublicationId 13 | 14 | @ExperimentalAudiobook 15 | internal val MediaControllerCompat.publicationId: PublicationId? 16 | get() = extras?.getString(MediaService.EXTRA_PUBLICATION_ID) 17 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/media/extensions/MediaMetadataCompat.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved2 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.media.extensions 8 | 9 | import android.support.v4.media.MediaMetadataCompat 10 | import org.readium.r2.navigator.extensions.splitAt 11 | 12 | internal val MediaMetadataCompat.id: String? get() = 13 | getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID) 14 | 15 | internal val MediaMetadataCompat.publicationId: String? get() = 16 | id?.splitAt("#")?.first 17 | 18 | internal val MediaMetadataCompat.resourceHref: String? get() = 19 | id?.splitAt("#")?.second 20 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/media/extensions/MediaSessionCompat.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.media.extensions 8 | 9 | import android.os.Bundle 10 | import android.support.v4.media.session.MediaSessionCompat 11 | import org.readium.r2.navigator.ExperimentalAudiobook 12 | import org.readium.r2.navigator.media.MediaService 13 | import org.readium.r2.shared.publication.PublicationId 14 | 15 | @ExperimentalAudiobook 16 | internal var MediaSessionCompat.publicationId: PublicationId? 17 | get() = controller.publicationId 18 | set(value) { 19 | val extras = Bundle(controller.extras ?: Bundle()) 20 | setExtras(extras.apply { 21 | putString(MediaService.EXTRA_PUBLICATION_ID, value) 22 | }) 23 | sendSessionEvent(MediaService.EVENT_PUBLICATION_CHANGED, Bundle().apply { 24 | putString(MediaService.EXTRA_PUBLICATION_ID, value) 25 | }) 26 | } -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/media/extensions/PlaybackStateCompat.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.media.extensions 8 | 9 | import android.os.SystemClock 10 | import android.support.v4.media.session.PlaybackStateCompat 11 | import org.readium.r2.navigator.ExperimentalAudiobook 12 | import org.readium.r2.navigator.media.MediaPlayback 13 | 14 | internal inline val PlaybackStateCompat.isPrepared get() = 15 | (state == PlaybackStateCompat.STATE_BUFFERING) || 16 | (state == PlaybackStateCompat.STATE_PLAYING) || 17 | (state == PlaybackStateCompat.STATE_PAUSED) 18 | 19 | internal inline val PlaybackStateCompat.isPlaying get() = 20 | (state == PlaybackStateCompat.STATE_BUFFERING) || 21 | (state == PlaybackStateCompat.STATE_PLAYING) 22 | 23 | internal inline val PlaybackStateCompat.canPlay get() = 24 | (actions and PlaybackStateCompat.ACTION_PLAY != 0L) || 25 | ((actions and PlaybackStateCompat.ACTION_PLAY_PAUSE != 0L) && (state == PlaybackStateCompat.STATE_PAUSED)) 26 | 27 | /** 28 | * Calculates the current playback position based on last update time along with playback 29 | * state and speed. 30 | */ 31 | internal inline val PlaybackStateCompat.elapsedPosition: Long get() = 32 | if (state == PlaybackStateCompat.STATE_PLAYING) { 33 | val timeDelta = SystemClock.elapsedRealtime() - lastPositionUpdateTime 34 | (position + (timeDelta * playbackSpeed)).toLong() 35 | } else { 36 | position 37 | } 38 | 39 | @ExperimentalAudiobook 40 | internal fun PlaybackStateCompat.toPlaybackState(): MediaPlayback.State = 41 | when (state) { 42 | PlaybackStateCompat.STATE_BUFFERING, PlaybackStateCompat.STATE_CONNECTING, 43 | PlaybackStateCompat.STATE_SKIPPING_TO_NEXT, PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS, 44 | PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM -> 45 | MediaPlayback.State.Loading 46 | 47 | PlaybackStateCompat.STATE_PLAYING, PlaybackStateCompat.STATE_FAST_FORWARDING, 48 | PlaybackStateCompat.STATE_REWINDING -> 49 | MediaPlayback.State.Playing 50 | 51 | PlaybackStateCompat.STATE_PAUSED -> MediaPlayback.State.Paused 52 | 53 | else -> MediaPlayback.State.Idle 54 | } 55 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/pager/R2CbzPageFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi, Mostapha Idoubihi 4 | * 5 | * Copyright (c) 2018. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.pager 11 | 12 | import android.graphics.BitmapFactory 13 | import android.os.Bundle 14 | import android.view.LayoutInflater 15 | import android.view.View 16 | import android.view.ViewGroup 17 | import android.view.WindowManager 18 | import androidx.core.view.ViewCompat 19 | import androidx.lifecycle.lifecycleScope 20 | import com.github.chrisbanes.photoview.PhotoView 21 | import kotlinx.coroutines.CoroutineScope 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.launch 24 | import org.readium.r2.navigator.R 25 | import org.readium.r2.navigator.databinding.ViewpagerFragmentCbzBinding 26 | import org.readium.r2.shared.publication.Link 27 | import org.readium.r2.shared.publication.Publication 28 | import kotlin.coroutines.CoroutineContext 29 | 30 | 31 | class R2CbzPageFragment(private val publication: Publication, private val onTapListener: (Float, Float) -> Unit) 32 | : androidx.fragment.app.Fragment(), CoroutineScope { 33 | 34 | override val coroutineContext: CoroutineContext 35 | get() = Dispatchers.Main 36 | 37 | private val link: Link 38 | get() = requireArguments().getParcelable("link")!! 39 | 40 | private lateinit var containerView: View 41 | private lateinit var photoView: PhotoView 42 | 43 | private var _binding: ViewpagerFragmentCbzBinding? = null 44 | private val binding get() = _binding!! 45 | 46 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 47 | 48 | _binding = ViewpagerFragmentCbzBinding.inflate(inflater, container, false) 49 | containerView = binding.root 50 | photoView = binding.imageView 51 | photoView.setOnViewTapListener { _, x, y -> onTapListener(x, y) } 52 | 53 | setupPadding() 54 | 55 | launch { 56 | publication.get(link) 57 | .read() 58 | .getOrNull() 59 | ?.let { BitmapFactory.decodeByteArray(it, 0, it.size) } 60 | ?.let { photoView.setImageBitmap(it) } 61 | } 62 | 63 | return containerView 64 | } 65 | 66 | override fun onDestroyView() { 67 | super.onDestroyView() 68 | _binding = null 69 | } 70 | 71 | private fun setupPadding() { 72 | updatePadding() 73 | 74 | // Update padding when the window insets change, for example when the navigation and status 75 | // bars are toggled. 76 | ViewCompat.setOnApplyWindowInsetsListener(containerView) { _, insets -> 77 | updatePadding() 78 | insets 79 | } 80 | } 81 | 82 | private fun updatePadding() { 83 | viewLifecycleOwner.lifecycleScope.launchWhenResumed { 84 | val window = activity?.window ?: return@launchWhenResumed 85 | var top = 0 86 | var bottom = 0 87 | 88 | // Add additional padding to take into account the display cutout, if needed. 89 | if ( 90 | android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P && 91 | window.attributes.layoutInDisplayCutoutMode != WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 92 | ) { 93 | // Request the display cutout insets from the decor view because the ones given by 94 | // setOnApplyWindowInsetsListener are not always correct for preloaded views. 95 | window.decorView.rootWindowInsets?.displayCutout?.let { displayCutoutInsets -> 96 | top += displayCutoutInsets.safeInsetTop 97 | bottom += displayCutoutInsets.safeInsetBottom 98 | } 99 | } 100 | 101 | photoView.setPadding(0, top, 0, bottom) 102 | } 103 | } 104 | 105 | } 106 | 107 | 108 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/pager/R2FXLPageFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi, Clément Baumann, Mostapha Idoubihi, Paul Stoica 4 | * 5 | * Copyright (c) 2018. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.pager 11 | 12 | import android.annotation.SuppressLint 13 | import android.graphics.PointF 14 | import android.os.Bundle 15 | import android.view.LayoutInflater 16 | import android.view.View 17 | import android.view.ViewGroup 18 | import android.webkit.WebResourceRequest 19 | import android.webkit.WebResourceResponse 20 | import android.webkit.WebView 21 | import androidx.fragment.app.Fragment 22 | import androidx.webkit.WebViewClientCompat 23 | import org.readium.r2.navigator.Navigator 24 | import org.readium.r2.navigator.R 25 | import org.readium.r2.navigator.R2BasicWebView 26 | import org.readium.r2.navigator.databinding.FragmentFxllayoutDoubleBinding 27 | import org.readium.r2.navigator.databinding.FragmentFxllayoutSingleBinding 28 | import org.readium.r2.navigator.epub.EpubNavigatorFragment 29 | import org.readium.r2.navigator.epub.fxl.R2FXLLayout 30 | import org.readium.r2.navigator.epub.fxl.R2FXLOnDoubleTapListener 31 | 32 | class R2FXLPageFragment : Fragment() { 33 | 34 | private val firstResourceUrl: String? 35 | get() = requireArguments().getString("firstUrl") 36 | 37 | private val secondResourceUrl: String? 38 | get() = requireArguments().getString("secondUrl") 39 | 40 | private var webViews = mutableListOf() 41 | 42 | private var _doubleBinding: FragmentFxllayoutDoubleBinding? = null 43 | private val doubleBinding get() = _doubleBinding!! 44 | 45 | private var _singleBinding: FragmentFxllayoutSingleBinding? = null 46 | private val singleBinding get() = _singleBinding!! 47 | 48 | @SuppressLint("SetJavaScriptEnabled") 49 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 50 | 51 | secondResourceUrl?.let { 52 | _doubleBinding = FragmentFxllayoutDoubleBinding.inflate(inflater, container, false) 53 | val view: View = doubleBinding.root 54 | view.setPadding(0, 0, 0, 0) 55 | 56 | val r2FXLLayout = doubleBinding.r2FXLLayout 57 | r2FXLLayout.isAllowParentInterceptOnScaled = true 58 | 59 | val left = doubleBinding.firstWebView 60 | val right = doubleBinding.secondWebView 61 | 62 | setupWebView(left, firstResourceUrl) 63 | setupWebView(right, secondResourceUrl) 64 | 65 | r2FXLLayout.addOnDoubleTapListener(R2FXLOnDoubleTapListener(true)) 66 | r2FXLLayout.addOnTapListener(object : R2FXLLayout.OnTapListener { 67 | override fun onTap(view: R2FXLLayout, info: R2FXLLayout.TapInfo): Boolean { 68 | return left.listener.onTap(PointF(info.x, info.y)) 69 | } 70 | }) 71 | 72 | return view 73 | }?:run { 74 | _singleBinding = FragmentFxllayoutSingleBinding.inflate(inflater, container, false) 75 | val view: View = singleBinding.root 76 | view.setPadding(0, 0, 0, 0) 77 | 78 | val r2FXLLayout = singleBinding.r2FXLLayout 79 | r2FXLLayout.isAllowParentInterceptOnScaled = true 80 | 81 | val webview = singleBinding.webViewSingle 82 | 83 | setupWebView(webview, firstResourceUrl) 84 | 85 | r2FXLLayout.addOnDoubleTapListener(R2FXLOnDoubleTapListener(true)) 86 | r2FXLLayout.addOnTapListener(object : R2FXLLayout.OnTapListener { 87 | override fun onTap(view: R2FXLLayout, info: R2FXLLayout.TapInfo): Boolean { 88 | return webview.listener.onTap(PointF(info.x, info.y)) 89 | } 90 | }) 91 | 92 | return view 93 | } 94 | } 95 | 96 | override fun onDetach() { 97 | super.onDetach() 98 | 99 | // Prevent the web views from leaking when their parent is detached. 100 | // See https://stackoverflow.com/a/19391512/1474476 101 | for (wv in webViews) { 102 | (wv.parent as? ViewGroup)?.removeView(wv) 103 | wv.removeAllViews() 104 | wv.destroy() 105 | } 106 | } 107 | 108 | override fun onDestroyView() { 109 | super.onDestroyView() 110 | _singleBinding = null 111 | _doubleBinding = null 112 | } 113 | 114 | @SuppressLint("SetJavaScriptEnabled") 115 | private fun setupWebView(webView: R2BasicWebView, resourceUrl: String?) { 116 | webViews.add(webView) 117 | webView.navigator = parentFragment as Navigator 118 | webView.listener = parentFragment as R2BasicWebView.Listener 119 | 120 | webView.settings.javaScriptEnabled = true 121 | webView.isVerticalScrollBarEnabled = false 122 | webView.isHorizontalScrollBarEnabled = false 123 | webView.settings.useWideViewPort = true 124 | webView.settings.loadWithOverviewMode = true 125 | webView.settings.setSupportZoom(true) 126 | webView.settings.builtInZoomControls = true 127 | webView.settings.displayZoomControls = false 128 | 129 | webView.setInitialScale(1) 130 | 131 | webView.setPadding(0, 0, 0, 0) 132 | webView.addJavascriptInterface(webView, "Android") 133 | 134 | 135 | webView.webViewClient = object : WebViewClientCompat() { 136 | 137 | override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean = 138 | (webView as? R2BasicWebView)?.shouldOverrideUrlLoading(request) ?: false 139 | 140 | // prevent favicon.ico to be loaded, this was causing NullPointerException in NanoHttp 141 | override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { 142 | if (!request.isForMainFrame && request.url.path?.endsWith("/favicon.ico") == true) { 143 | try { 144 | return WebResourceResponse("image/png", null, null) 145 | } catch (e: Exception) { 146 | } 147 | } 148 | return null 149 | } 150 | 151 | } 152 | webView.isHapticFeedbackEnabled = false 153 | webView.isLongClickable = false 154 | webView.setOnLongClickListener { 155 | true 156 | } 157 | 158 | resourceUrl?.let { webView.loadUrl(it) } 159 | } 160 | 161 | companion object { 162 | 163 | fun newInstance(url: String, url2: String? = null): R2FXLPageFragment = 164 | R2FXLPageFragment().apply { 165 | arguments = Bundle().apply { 166 | putString("firstUrl", url) 167 | putString("secondUrl", url2) 168 | } 169 | } 170 | 171 | } 172 | 173 | } 174 | 175 | 176 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/pager/R2FragmentPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi, Clément Baumann, Mostapha Idoubihi, Paul Stoica 4 | * 5 | * Copyright (c) 2018. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.pager 11 | 12 | import android.annotation.SuppressLint 13 | import android.os.Bundle 14 | import android.os.Parcelable 15 | import android.view.View 16 | import android.view.ViewGroup 17 | import androidx.collection.LongSparseArray 18 | import androidx.fragment.app.Fragment 19 | import androidx.fragment.app.FragmentManager 20 | import androidx.fragment.app.FragmentTransaction 21 | import androidx.viewpager.widget.PagerAdapter 22 | 23 | 24 | abstract class R2FragmentPagerAdapter(private val mFragmentManager: FragmentManager) : androidx.fragment.app.FragmentStatePagerAdapter(mFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { 25 | 26 | val mFragments = LongSparseArray() 27 | private val mSavedStates = LongSparseArray() 28 | private var mCurTransaction: FragmentTransaction? = null 29 | private var mCurrentPrimaryItem: Fragment? = null 30 | 31 | abstract override fun getItem(position: Int): Fragment 32 | 33 | override fun startUpdate(container: ViewGroup) { 34 | if (container.id == View.NO_ID) { 35 | throw IllegalStateException("ViewPager with adapter $this requires a view id") 36 | } 37 | } 38 | 39 | @SuppressLint("CommitTransaction") 40 | override fun instantiateItem(container: ViewGroup, position: Int): Any { 41 | val tag = getItemId(position) 42 | var fragment: Fragment? = mFragments.get(tag) 43 | 44 | if (fragment != null) { 45 | return fragment 46 | } 47 | 48 | if (mCurTransaction == null) { 49 | mCurTransaction = mFragmentManager.beginTransaction() 50 | } 51 | 52 | fragment = getItem(position) 53 | 54 | val savedState = mSavedStates.get(tag) 55 | if (savedState != null) { 56 | fragment.setInitialSavedState(savedState) 57 | } 58 | fragment.setMenuVisibility(false) 59 | fragment.userVisibleHint = false 60 | mFragments.put(tag, fragment) 61 | mCurTransaction!!.add(container.id, fragment, "f$tag") 62 | 63 | return fragment 64 | } 65 | 66 | @SuppressLint("CommitTransaction") 67 | override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { 68 | val fragment = `object` as Fragment 69 | val currentPosition = getItemPosition(fragment) 70 | 71 | val index = mFragments.indexOfValue(fragment) 72 | var fragmentKey: Long = -1 73 | if (index != -1) { 74 | fragmentKey = mFragments.keyAt(index) 75 | mFragments.removeAt(index) 76 | } 77 | 78 | 79 | if (fragment.isAdded && currentPosition != PagerAdapter.POSITION_NONE) { 80 | mSavedStates.put(fragmentKey, mFragmentManager.saveFragmentInstanceState(fragment)) 81 | } else { 82 | mSavedStates.remove(fragmentKey) 83 | } 84 | 85 | if (mCurTransaction == null) { 86 | mCurTransaction = mFragmentManager.beginTransaction() 87 | } 88 | 89 | mCurTransaction!!.remove(fragment) 90 | } 91 | 92 | override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) { 93 | val fragment = `object` as Fragment? 94 | if (fragment !== mCurrentPrimaryItem) { 95 | if (mCurrentPrimaryItem != null) { 96 | mCurrentPrimaryItem!!.setMenuVisibility(false) 97 | mCurrentPrimaryItem!!.userVisibleHint = false 98 | } 99 | if (fragment != null) { 100 | fragment.setMenuVisibility(true) 101 | fragment.userVisibleHint = true 102 | } 103 | mCurrentPrimaryItem = fragment 104 | } 105 | } 106 | 107 | override fun finishUpdate(container: ViewGroup) { 108 | if (mCurTransaction != null) { 109 | mCurTransaction!!.commitNowAllowingStateLoss() 110 | mCurTransaction = null 111 | } 112 | } 113 | 114 | override fun isViewFromObject(view: View, `object`: Any): Boolean { 115 | return (`object` as Fragment).view === view 116 | } 117 | 118 | override fun saveState(): Parcelable? { 119 | var state: Bundle? = null 120 | if (mSavedStates.size() > 0) { 121 | 122 | state = Bundle() 123 | val stateIds = LongArray(mSavedStates.size()) 124 | for (i in 0 until mSavedStates.size()) { 125 | val entry = mSavedStates.valueAt(i) 126 | stateIds[i] = mSavedStates.keyAt(i) 127 | state.putParcelable(stateIds[i].toString(), entry) 128 | } 129 | state.putLongArray("states", stateIds) 130 | } 131 | for (i in 0 until mFragments.size()) { 132 | val f = mFragments.valueAt(i) 133 | if (f != null && f.isAdded) { 134 | if (state == null) { 135 | state = Bundle() 136 | } 137 | val key = "f" + mFragments.keyAt(i) 138 | mFragmentManager.putFragment(state, key, f) 139 | } 140 | } 141 | return state 142 | } 143 | 144 | override fun restoreState(state: Parcelable?, loader: ClassLoader?) { 145 | if (state != null) { 146 | val bundle = state as Bundle? 147 | bundle!!.classLoader = loader 148 | val fss = bundle.getLongArray("states") 149 | mSavedStates.clear() 150 | mFragments.clear() 151 | if (fss != null) { 152 | for (fs in fss) { 153 | mSavedStates.put(fs, bundle.getParcelable(fs.toString()) as Fragment.SavedState) 154 | } 155 | } 156 | val keys = bundle.keySet() 157 | for (key in keys) { 158 | if (key.startsWith("f")) { 159 | val f = mFragmentManager.getFragment(bundle, key) 160 | if (f != null) { 161 | f.setMenuVisibility(false) 162 | mFragments.put(java.lang.Long.parseLong(key.substring(1)), f) 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | 170 | fun getItemId(position: Int): Long { 171 | return position.toLong() 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/pager/R2PagerAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi, Clément Baumann 4 | * 5 | * Copyright (c) 2018. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.pager 11 | 12 | import android.os.Bundle 13 | import android.view.ViewGroup 14 | import androidx.fragment.app.Fragment 15 | import androidx.fragment.app.FragmentManager 16 | import org.readium.r2.shared.publication.Link 17 | 18 | 19 | class R2PagerAdapter internal constructor(val fm: FragmentManager, private val resources: List) : R2FragmentPagerAdapter(fm) { 20 | 21 | internal sealed class PageResource { 22 | data class EpubReflowable(val link: Link, val url: String) : PageResource() 23 | data class EpubFxl(val url1: String, val url2: String? = null) : PageResource() 24 | data class Cbz(val link: Link) : PageResource() 25 | } 26 | 27 | private var currentFragment: Fragment? = null 28 | private var previousFragment: Fragment? = null 29 | private var nextFragment: Fragment? = null 30 | 31 | fun getCurrentFragment(): Fragment? { 32 | return currentFragment 33 | } 34 | 35 | fun getPreviousFragment(): Fragment? { 36 | return previousFragment 37 | } 38 | 39 | fun getNextFragment(): Fragment? { 40 | return nextFragment 41 | } 42 | 43 | override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) { 44 | if (getCurrentFragment() !== `object`) { 45 | currentFragment = `object` as Fragment 46 | nextFragment = mFragments.get(getItemId(position + 1)) 47 | previousFragment = mFragments.get(getItemId(position - 1)) 48 | } 49 | super.setPrimaryItem(container, position, `object`) 50 | } 51 | 52 | override fun getItem(position: Int): Fragment = 53 | when (val resource = resources[position]) { 54 | is PageResource.EpubReflowable -> { 55 | R2EpubPageFragment.newInstance(resource.url, resource.link) 56 | } 57 | is PageResource.EpubFxl -> { 58 | R2FXLPageFragment.newInstance(resource.url1, resource.url2) 59 | } 60 | is PageResource.Cbz -> { 61 | fm.fragmentFactory 62 | .instantiate(ClassLoader.getSystemClassLoader(), R2CbzPageFragment::class.java.name) 63 | .also { 64 | it.arguments = Bundle().apply { 65 | putParcelable("link", resource.link) 66 | } 67 | } 68 | } 69 | } 70 | 71 | override fun getCount(): Int { 72 | return resources.size 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/pager/R2ViewPager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Aferdita Muriqi, Clément Baumann 4 | * 5 | * Copyright (c) 2018. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.pager 11 | 12 | import android.content.Context 13 | import android.util.AttributeSet 14 | import android.view.MotionEvent 15 | import org.readium.r2.navigator.BuildConfig.DEBUG 16 | import org.readium.r2.shared.publication.Publication 17 | import timber.log.Timber 18 | 19 | class R2ViewPager : R2RTLViewPager { 20 | 21 | 22 | lateinit var type: Publication.TYPE 23 | 24 | constructor(context: Context) : super(context) 25 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 26 | 27 | override fun setCurrentItem(item: Int) { 28 | super.setCurrentItem(item, false) 29 | } 30 | 31 | override fun onTouchEvent(ev: MotionEvent): Boolean { 32 | if (DEBUG) Timber.d("ev.action ${ev.action}") 33 | if (type == Publication.TYPE.EPUB) { 34 | when (ev.action and MotionEvent.ACTION_MASK) { 35 | MotionEvent.ACTION_DOWN -> { 36 | // prevent swipe from view pager directly 37 | if (DEBUG) Timber.d("ACTION_DOWN") 38 | return false 39 | } 40 | } 41 | } 42 | 43 | return try { 44 | // The super implementation sometimes triggers: 45 | // java.lang.IllegalArgumentException: pointerIndex out of range 46 | // i.e. https://stackoverflow.com/q/48496257/1474476 47 | return super.onTouchEvent(ev) 48 | 49 | } catch (ex: IllegalArgumentException) { 50 | Timber.e(ex) 51 | false 52 | } 53 | } 54 | 55 | override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { 56 | if (DEBUG) Timber.d("onInterceptTouchEvent ev.action ${ev.action}") 57 | if (type == Publication.TYPE.EPUB) { 58 | when (ev.action and MotionEvent.ACTION_MASK) { 59 | MotionEvent.ACTION_DOWN -> { 60 | // prevent swipe from view pager directly 61 | if (DEBUG) Timber.d("onInterceptTouchEvent ACTION_DOWN") 62 | return false 63 | } 64 | } 65 | } 66 | 67 | return try { 68 | // The super implementation sometimes triggers: 69 | // java.lang.IllegalArgumentException: pointerIndex out of range 70 | // i.e. https://stackoverflow.com/q/48496257/1474476 71 | super.onInterceptTouchEvent(ev) 72 | } catch (ex: IllegalArgumentException) { 73 | Timber.e(ex) 74 | false 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/pdf/R2PdfActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-navigator-kotlin 3 | * Developers: Mickaël Menu 4 | * 5 | * Copyright (c) 2020. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.navigator.pdf 11 | 12 | import android.app.Activity 13 | import android.os.Build 14 | import android.os.Bundle 15 | import android.view.WindowManager 16 | import androidx.appcompat.app.AppCompatActivity 17 | import androidx.lifecycle.Observer 18 | import androidx.lifecycle.asLiveData 19 | import org.readium.r2.navigator.Navigator 20 | import org.readium.r2.navigator.R 21 | import org.readium.r2.navigator.util.CompositeFragmentFactory 22 | import org.readium.r2.shared.PdfSupport 23 | import org.readium.r2.shared.extensions.getPublication 24 | import org.readium.r2.shared.publication.Locator 25 | import org.readium.r2.shared.publication.Publication 26 | 27 | @PdfSupport 28 | abstract class R2PdfActivity : AppCompatActivity(), PdfNavigatorFragment.Listener { 29 | 30 | protected lateinit var publication: Publication 31 | 32 | protected val navigator: Navigator get() = 33 | supportFragmentManager.findFragmentById(R.id.r2_pdf_navigator) as Navigator 34 | 35 | /** 36 | * Override this event handler to save the current location in the publication in a database. 37 | */ 38 | open fun onCurrentLocatorChanged(locator: Locator) {} 39 | 40 | override fun onCreate(savedInstanceState: Bundle?) { 41 | publication = intent.getPublication(this) 42 | 43 | // This must be done before the call to super.onCreate, including by reading apps. 44 | // Because they may want to set their own factories, let's use a CompositeFragmentFactory that retains 45 | // previously set factories. 46 | supportFragmentManager.fragmentFactory = CompositeFragmentFactory( 47 | supportFragmentManager.fragmentFactory, 48 | PdfNavigatorFragment.createFactory( 49 | publication = publication, 50 | initialLocator = intent.getParcelableExtra("locator"), 51 | listener = this 52 | ) 53 | ) 54 | 55 | super.onCreate(savedInstanceState) 56 | 57 | setContentView(R.layout.r2_pdf_activity) 58 | 59 | navigator.currentLocator.asLiveData().observe(this, Observer { locator -> 60 | locator ?: return@Observer 61 | 62 | onCurrentLocatorChanged(locator) 63 | }) 64 | 65 | // Display cutouts are not compatible with the underlying `PdfNavigatorFragment` yet. 66 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 67 | window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 68 | } 69 | } 70 | 71 | override fun finish() { 72 | setResult(Activity.RESULT_OK, intent) 73 | super.finish() 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/util/BaseActionModeCallback.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.util 8 | 9 | import android.view.ActionMode 10 | import android.view.Menu 11 | import android.view.MenuItem 12 | 13 | /** 14 | * A convenient base implementation of [ActionMode.Callback], when you don't need to override all 15 | * methods. 16 | */ 17 | abstract class BaseActionModeCallback : ActionMode.Callback { 18 | override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = false 19 | override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = false 20 | override fun onDestroyActionMode(mode: ActionMode) {} 21 | } 22 | -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/util/FragmentFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.util 8 | 9 | import androidx.fragment.app.Fragment 10 | import androidx.fragment.app.FragmentFactory 11 | import org.readium.r2.shared.extensions.tryOrNull 12 | 13 | /** 14 | * Creates a [FragmentFactory] for a single type of [Fragment] using the result of the given 15 | * [factory] closure. 16 | */ 17 | internal inline fun createFragmentFactory(crossinline factory: () -> T): FragmentFactory = object : FragmentFactory() { 18 | 19 | override fun instantiate(classLoader: ClassLoader, className: String): Fragment { 20 | return when (className) { 21 | T::class.java.name -> factory() 22 | else -> super.instantiate(classLoader, className) 23 | } 24 | } 25 | 26 | } 27 | 28 | /** 29 | * A [FragmentFactory] which will iterate over a provided list of [factories] until finding one 30 | * instantiating successfully the requested [Fragment]. 31 | * 32 | * ``` 33 | * supportFragmentManager.fragmentFactory = CompositeFragmentFactory( 34 | * EpubNavigatorFragment.createFactory(publication, baseUrl, initialLocator, this), 35 | * PdfNavigatorFragment.createFactory(publication, initialLocator, this) 36 | * ) 37 | * ``` 38 | */ 39 | class CompositeFragmentFactory(private val factories: List) : FragmentFactory() { 40 | 41 | constructor(vararg factories: FragmentFactory) : this(factories.toList()) 42 | 43 | override fun instantiate(classLoader: ClassLoader, className: String): Fragment { 44 | for (factory in factories) { 45 | tryOrNull { factory.instantiate(classLoader, className) } 46 | ?.let { return it } 47 | } 48 | 49 | return super.instantiate(classLoader, className) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /r2-navigator/src/main/java/org/readium/r2/navigator/util/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.navigator.util 8 | 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.ViewModelProvider 11 | 12 | /** 13 | * Creates a [ViewModelProvider.Factory] for a single type of [ViewModel] using the result of the 14 | * given [factory] closure. 15 | */ 16 | internal inline fun createViewModelFactory(crossinline factory: () -> T): ViewModelProvider.Factory = 17 | 18 | object : ViewModelProvider.Factory { 19 | override fun create(modelClass: Class): V { 20 | if (!modelClass.isAssignableFrom(T::class.java)) { 21 | throw IllegalAccessException("Unknown ViewModel class") 22 | } 23 | @Suppress("UNCHECKED_CAST") 24 | return factory() as V 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/drawable/baseline_forward_10_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/r2-navigator/src/main/res/drawable/baseline_forward_10_white_24.png -------------------------------------------------------------------------------- /r2-navigator/src/main/res/drawable/baseline_replay_10_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/r2-navigator/src/main/res/drawable/baseline_replay_10_white_24.png -------------------------------------------------------------------------------- /r2-navigator/src/main/res/drawable/ic_pause_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/r2-navigator/src/main/res/drawable/ic_pause_white_24dp.png -------------------------------------------------------------------------------- /r2-navigator/src/main/res/drawable/ic_play_arrow_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/r2-navigator/src/main/res/drawable/ic_play_arrow_white_24dp.png -------------------------------------------------------------------------------- /r2-navigator/src/main/res/drawable/ic_skip_next_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/r2-navigator/src/main/res/drawable/ic_skip_next_white_24dp.png -------------------------------------------------------------------------------- /r2-navigator/src/main/res/drawable/ic_skip_previous_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-navigator-kotlin/4d61498286d418f209a178334a85a528f827a994/r2-navigator/src/main/res/drawable/ic_skip_previous_white_24dp.png -------------------------------------------------------------------------------- /r2-navigator/src/main/res/drawable/r2_media_notification_fastforward.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/drawable/r2_media_notification_rewind.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/activity_r2_divina.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/activity_r2_epub.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/activity_r2_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/activity_r2_viewpager.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/fragment_fxllayout_double.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 21 | 22 | 26 | 27 | 34 | 35 | 42 | 43 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/fragment_fxllayout_single.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 21 | 22 | 26 | 27 | 34 | 35 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/item_spinner_font.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/popup_footnote.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/r2_pdf_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/viewpager_fragment_cbz.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/layout/viewpager_fragment_epub.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/values-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20dp 4 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | #1f21b5 13 | #17188c 14 | #eceaeb 15 | #17188c 16 | 17 | 18 | @color/colorPrimaryDark 19 | @color/colorAccent 20 | 21 | 22 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 40dp 4 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | end of chapter 13 | ~ ~ ~ 14 | 00:00 15 | epub_navigator 16 | image_navigator 17 | Audiobook 18 | Audiobook currently being played 19 | 20 | 21 | -------------------------------------------------------------------------------- /r2-navigator/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 16 | 17 | 22 | 23 | 26 | 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':r2-navigator' 2 | --------------------------------------------------------------------------------