├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── menu
│ │ │ └── menu.xml
│ │ ├── drawable
│ │ │ ├── ic_dehaze.xml
│ │ │ └── ic_settings.xml
│ │ └── layout
│ │ │ ├── table_of_contents_item.xml
│ │ │ └── activity_main.xml
│ │ ├── assets
│ │ ├── aayesha.epub
│ │ ├── PhysicsSyllabus.epub
│ │ ├── The Silver Chair.epub
│ │ ├── fonts
│ │ │ └── Diplomata_SC
│ │ │ │ ├── DiplomataSC-Regular.ttf
│ │ │ │ └── OFL.txt
│ │ └── books
│ │ │ ├── styles
│ │ │ └── night_mode.css
│ │ │ └── javascript
│ │ │ ├── rangy
│ │ │ ├── rangy-serializer.min.js
│ │ │ ├── rangy-highlighter.min.js
│ │ │ └── rangy-classapplier.min.js
│ │ │ └── highlight.js
│ │ ├── java
│ │ └── com
│ │ │ └── smartmobilefactory
│ │ │ └── epubreader
│ │ │ └── sample
│ │ │ ├── EpubSampleApp.java
│ │ │ ├── NightmodePlugin.java
│ │ │ ├── ChapterJavaScriptBridge.java
│ │ │ ├── TableOfContentsAdapter.java
│ │ │ └── MainActivity.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── library
├── .gitignore
├── src
│ └── main
│ │ ├── assets
│ │ └── epubreaderandroid
│ │ │ ├── style.css
│ │ │ ├── script.js
│ │ │ └── helper_functions.js
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── smartmobilefactory
│ │ │ └── epubreader
│ │ │ ├── UrlInterceptor.java
│ │ │ ├── model
│ │ │ ├── EpubFont.java
│ │ │ ├── LazyResource.java
│ │ │ ├── UncompressedEpubReader.java
│ │ │ ├── EpubLocation.java
│ │ │ ├── FileUtils.java
│ │ │ ├── EpubStorageHelper.java
│ │ │ ├── Unzipper.java
│ │ │ └── Epub.java
│ │ │ ├── utils
│ │ │ └── BaseDisposableObserver.kt
│ │ │ ├── display
│ │ │ ├── view
│ │ │ │ ├── InternalEpubBridge.kt
│ │ │ │ ├── BaseViewPagerAdapter.kt
│ │ │ │ ├── JsApi.kt
│ │ │ │ └── EpubWebView.kt
│ │ │ ├── binding
│ │ │ │ ├── EpubVerticalVerticalContentBinding.kt
│ │ │ │ ├── ItemVerticalVerticalContentBinding.kt
│ │ │ │ ├── EpubHorizontalVerticalContentBinding.kt
│ │ │ │ └── ItemEpubVerticalContentBinding.kt
│ │ │ ├── WebViewHelper.kt
│ │ │ ├── EpubDisplayStrategy.kt
│ │ │ ├── vertical_content
│ │ │ │ ├── VerticalEpubWebView.kt
│ │ │ │ ├── SingleChapterVerticalEpubDisplayStrategy.kt
│ │ │ │ ├── horizontal_chapters
│ │ │ │ │ ├── HorizontalWithVerticalContentEpubDisplayStrategy.kt
│ │ │ │ │ └── PagerAdapter.kt
│ │ │ │ ├── VerticalContentBinderHelper.kt
│ │ │ │ └── vertical_chapters
│ │ │ │ │ ├── VerticalWithVerticalContentEpubDisplayStrategy.kt
│ │ │ │ │ └── ChapterAdapter.kt
│ │ │ └── EpubDisplayHelper.kt
│ │ │ ├── EpubViewPlugin.kt
│ │ │ ├── EpubScrollDirection.kt
│ │ │ ├── SavedState.kt
│ │ │ ├── InternalEpubViewSettings.kt
│ │ │ ├── EpubViewSettings.java
│ │ │ └── EpubView.kt
│ │ └── res
│ │ └── layout
│ │ ├── epub_horizontal_vertical_content.xml
│ │ ├── epub_vertical_vertical_content.xml
│ │ ├── item_vertical_vertical_content.xml
│ │ └── item_epub_vertical_content.xml
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── scripts
├── hprof_dump.sh
├── dumpapp
└── stetho_open.py
├── LICENSE
├── README.md
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':library'
2 |
--------------------------------------------------------------------------------
/library/src/main/assets/epubreaderandroid/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 2%;
3 | }
4 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | EpubReaderAndroid
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/assets/aayesha.epub:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/assets/aayesha.epub
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/assets/PhysicsSyllabus.epub:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/assets/PhysicsSyllabus.epub
--------------------------------------------------------------------------------
/app/src/main/assets/The Silver Chair.epub:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/assets/The Silver Chair.epub
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/assets/fonts/Diplomata_SC/DiplomataSC-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smartmobilefactory/EpubReaderAndroid/HEAD/app/src/main/assets/fonts/Diplomata_SC/DiplomataSC-Regular.ttf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 | .idea/
11 | app/src/main/assets/private/
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/UrlInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader;
2 |
3 | public interface UrlInterceptor {
4 | boolean shouldOverrideUrlLoading(String url);
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/assets/books/styles/night_mode.css:
--------------------------------------------------------------------------------
1 | body { zoom: 100%; } * { background: #111111 !important; color: #ABABAB !important; } :link, :link * { color: #DBDBDC !important } :visited, :visited * { color: #5B5B5B !important }
2 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 25 09:41:49 CET 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
7 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/epub_horizontal_vertical_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/epub_vertical_vertical_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_dehaze.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/table_of_contents_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/item_vertical_vertical_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/model/EpubFont.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.model;
2 |
3 | import android.support.annotation.Nullable;
4 |
5 | import com.google.auto.value.AutoValue;
6 |
7 | @AutoValue
8 | public abstract class EpubFont {
9 |
10 | @Nullable
11 | public abstract String name();
12 |
13 | @Nullable
14 | public abstract String uri();
15 |
16 | public static EpubFont fromUri(String name, String uri) {
17 | return new AutoValue_EpubFont(name, uri);
18 | }
19 |
20 | public static EpubFont fromFontFamily(String fontFamily) {
21 | return new AutoValue_EpubFont(fontFamily, null);
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/utils/BaseDisposableObserver.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.utils
2 |
3 | import io.reactivex.SingleObserver
4 | import io.reactivex.disposables.CompositeDisposable
5 | import io.reactivex.observers.DisposableObserver
6 |
7 | internal class BaseDisposableObserver : DisposableObserver(), SingleObserver {
8 | override fun onNext(o: E) {
9 |
10 | }
11 |
12 | override fun onSuccess(e: E) {
13 |
14 | }
15 |
16 | override fun onError(e: Throwable) {
17 | e.printStackTrace()
18 | }
19 |
20 | override fun onComplete() {
21 |
22 | }
23 |
24 | fun addTo(compositeDisposable: CompositeDisposable) {
25 | compositeDisposable.add(this)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/view/InternalEpubBridge.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.view
2 |
3 | import android.support.annotation.Keep
4 | import android.webkit.JavascriptInterface
5 |
6 | import io.reactivex.Observable
7 | import io.reactivex.subjects.PublishSubject
8 |
9 | internal open class InternalEpubBridge {
10 |
11 | private val xPathSubject = PublishSubject.create()
12 |
13 | @Keep
14 | @JavascriptInterface
15 | fun onLocationChanged(xPath: String?) {
16 | if (xPath == null) {
17 | return
18 | }
19 | xPathSubject.onNext(xPath)
20 | }
21 |
22 | fun xPath(): Observable {
23 | return xPathSubject.distinctUntilChanged()
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/EpubViewPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader
2 |
3 | import io.reactivex.Observable
4 | import io.reactivex.subjects.PublishSubject
5 |
6 | abstract class EpubViewPlugin {
7 |
8 | private val dataChangedSubject = PublishSubject.create()
9 |
10 | open val customChapterCss: List = listOf()
11 |
12 | open val customChapterScripts: List = listOf()
13 |
14 | open val javascriptBridge: EpubJavaScriptBridge? = null
15 |
16 | internal fun dataChanged(): Observable = dataChangedSubject
17 |
18 | fun notifyDataChanged() {
19 | dataChangedSubject.onNext(Any())
20 | }
21 |
22 | }
23 |
24 | data class EpubJavaScriptBridge(
25 | val name: String,
26 | val bridge: Any
27 | )
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/smartmobilefactory/epubreader/sample/EpubSampleApp.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.sample;
2 |
3 | import android.app.Application;
4 |
5 | import com.facebook.stetho.Stetho;
6 | import com.facebook.stetho.dumpapp.plugins.FilesDumperPlugin;
7 |
8 | public class EpubSampleApp extends Application {
9 |
10 | @Override
11 | public void onCreate() {
12 | super.onCreate();
13 |
14 | Stetho.initialize(Stetho.newInitializerBuilder(this)
15 | .enableDumpapp(() -> new Stetho.DefaultDumperPluginsBuilder(this)
16 | .provide(new FilesDumperPlugin(this))
17 | .finish())
18 | .enableWebKitInspector(Stetho.defaultInspectorModulesProvider(this))
19 | .build());
20 |
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/scripts/hprof_dump.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4 | DUMPAPP="$DIR/dumpapp"
5 |
6 | set -e
7 |
8 | # This will generate an hprof on the device, download it locally, convert the
9 | # hprof to the standard format, and store it in the current working directory.
10 | # The resulting file can be explored with a tool such as the standalone Eclipse
11 | # MemoryAnalyzer: https://eclipse.org/mat/
12 |
13 | if [[ -z "$1" ]]; then
14 | OUTFILE="out.hprof"
15 | else
16 | OUTFILE=$1
17 | fi
18 | TEMPFILE="${OUTFILE}-dalvik.tmp"
19 |
20 | echo "Generating hprof on device (this can take a while)..."
21 | $DUMPAPP "$@" hprof - > ${TEMPFILE}
22 |
23 | echo "Converting $TEMPFILE to standard format..."
24 | hprof-conv $TEMPFILE $OUTFILE
25 | rm $TEMPFILE
26 |
27 | echo "Stored ${OUTFILE}"
28 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/model/LazyResource.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.model;
2 |
3 | import java.io.BufferedInputStream;
4 | import java.io.FileInputStream;
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 |
8 | import nl.siegmann.epublib.domain.Resource;
9 | import nl.siegmann.epublib.util.IOUtil;
10 |
11 | class LazyResource extends Resource {
12 |
13 | private String fileName;
14 |
15 | LazyResource(String fileName, long size, String href) {
16 | super(fileName, size, href);
17 | this.fileName = fileName;
18 | }
19 |
20 | @Override
21 | public byte[] getData() throws IOException {
22 | InputStream in = new BufferedInputStream(new FileInputStream(fileName));
23 | try {
24 | return IOUtil.toByteArray(in);
25 | } finally {
26 | in.close();
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/binding/EpubVerticalVerticalContentBinding.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.binding
2 |
3 | import android.support.v7.widget.RecyclerView
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 |
8 | import com.smartmobilefactory.epubreader.R
9 |
10 | internal class EpubVerticalVerticalContentBinding private constructor(
11 | @JvmField
12 | val root: View
13 | ) {
14 |
15 | @JvmField
16 | val recyclerview: RecyclerView = root.findViewById(R.id.recyclerview) as RecyclerView
17 |
18 | companion object {
19 |
20 | @JvmStatic
21 | fun inflate(inflater: LayoutInflater, root: ViewGroup, attachToRoot: Boolean): EpubVerticalVerticalContentBinding {
22 | return EpubVerticalVerticalContentBinding(inflater.inflate(R.layout.epub_vertical_vertical_content, root, attachToRoot))
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/binding/ItemVerticalVerticalContentBinding.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.binding
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 |
7 | import com.smartmobilefactory.epubreader.R
8 | import com.smartmobilefactory.epubreader.display.view.EpubWebView
9 |
10 | internal class ItemVerticalVerticalContentBinding private constructor(
11 | @JvmField
12 | val root: View
13 | ) {
14 |
15 | @JvmField
16 | val webview: EpubWebView = root.findViewById(R.id.webview) as EpubWebView
17 |
18 | companion object {
19 |
20 | @JvmStatic
21 | fun inflate(inflater: LayoutInflater, root: ViewGroup, attachToRoot: Boolean): ItemVerticalVerticalContentBinding {
22 | return ItemVerticalVerticalContentBinding(inflater.inflate(R.layout.item_vertical_vertical_content, root, attachToRoot))
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/binding/EpubHorizontalVerticalContentBinding.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.binding
2 |
3 | import android.support.v4.view.ViewPager
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 |
8 | import com.smartmobilefactory.epubreader.R
9 |
10 | import io.reactivex.annotations.NonNull
11 |
12 | internal class EpubHorizontalVerticalContentBinding private constructor(
13 | @JvmField
14 | val root: View
15 | ) {
16 |
17 | @JvmField
18 | val pager: ViewPager = root.findViewById(R.id.pager) as ViewPager
19 |
20 | companion object {
21 |
22 | @JvmStatic
23 | fun inflate(inflater: LayoutInflater, root: ViewGroup, attachToRoot: Boolean): EpubHorizontalVerticalContentBinding {
24 | return EpubHorizontalVerticalContentBinding(inflater.inflate(R.layout.epub_horizontal_vertical_content, root, attachToRoot))
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app/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/timfreiheit/ProgrammeOther/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 |
--------------------------------------------------------------------------------
/library/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/timfreiheit/ProgrammeOther/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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Smart Mobile Factory GmbH
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_settings.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 25
5 |
6 | defaultConfig {
7 | applicationId "com.smartmobilefactory.epubreader.sample"
8 | minSdkVersion 16
9 | targetSdkVersion 25
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 |
15 | dataBinding {
16 | enabled true
17 | }
18 |
19 | compileOptions {
20 | sourceCompatibility JavaVersion.VERSION_1_8
21 | targetCompatibility JavaVersion.VERSION_1_8
22 | }
23 |
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
28 | }
29 | }
30 | }
31 |
32 | dependencies {
33 | compile fileTree(include: ['*.jar'], dir: 'libs')
34 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
35 | exclude group: 'com.android.support', module: 'support-annotations'
36 | })
37 | implementation 'com.android.support:appcompat-v7:25.2.0'
38 |
39 |
40 | implementation 'com.facebook.stetho:stetho:1.4.2'
41 |
42 | testCompile 'junit:junit:4.12'
43 | implementation project(':library')
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/smartmobilefactory/epubreader/sample/NightmodePlugin.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.sample;
2 |
3 | import android.graphics.Color;
4 |
5 | import com.smartmobilefactory.epubreader.EpubView;
6 | import com.smartmobilefactory.epubreader.EpubViewPlugin;
7 |
8 | import org.jetbrains.annotations.NotNull;
9 |
10 | import java.util.Collections;
11 | import java.util.List;
12 |
13 | public class NightmodePlugin extends EpubViewPlugin {
14 |
15 | private EpubView epubView;
16 |
17 | public NightmodePlugin(EpubView epubView) {
18 | this.epubView = epubView;
19 | }
20 |
21 | private boolean nightModeEnabled = false;
22 |
23 | public void setNightModeEnabled(boolean enabled) {
24 | nightModeEnabled = enabled;
25 | if (nightModeEnabled) {
26 | epubView.setBackgroundColor(Color.parseColor("#111111"));
27 | } else {
28 | epubView.setBackgroundColor(Color.WHITE);
29 | }
30 | notifyDataChanged();
31 | }
32 |
33 | @NotNull
34 | @Override
35 | public List getCustomChapterCss() {
36 | if (nightModeEnabled) {
37 | return Collections.singletonList("file:///android_asset/books/styles/night_mode.css");
38 | } else {
39 | return Collections.emptyList();
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/EpubScrollDirection.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader
2 |
3 | enum class EpubScrollDirection {
4 |
5 | /**
6 | * make chapters internally vertical scrollable
7 | * but each chapter has its own page
8 | * the user can swipe horizontally through the chapters
9 | *
10 | * | chapter1 | | chapter2 | | chapter3 | | chapter4 |
11 | * | chapter1 | | chapter2 | | chapter3 | | chapter4 |
12 | * | chapter1 | | chapter2 | | chapter4 |
13 | * | chapter1 |
14 | * | chapter1 |
15 | */
16 | HORIZONTAL_WITH_VERTICAL_CONTENT,
17 |
18 | /**
19 | * make all chapters vertical scrollable
20 | * WARNING: this mode is not well tested and experimental
21 | *
22 | * | chapter1 |
23 | * | chapter1 |
24 | * | chapter1 |
25 | * | chapter1 |
26 | * | chapter1 |
27 | * | chapter2 |
28 | * | chapter2 |
29 | * | chapter2 |
30 | * | chapter3 |
31 | * | chapter4 |
32 | */
33 | VERTICAL_WITH_VERTICAL_CONTENT,
34 |
35 | /**
36 | * show only a single chapter which is vertically scrollable
37 | *
38 | * | chapter1 |
39 | * | chapter1 |
40 | * | chapter1 |
41 | * | chapter1 |
42 | * | chapter1 |
43 | */
44 | SINGLE_CHAPTER_VERTICAL
45 | }
46 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/item_epub_vertical_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
17 |
18 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/view/BaseViewPagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.view
2 |
3 | import android.support.v4.view.PagerAdapter
4 | import android.util.SparseArray
5 | import android.view.View
6 | import android.view.ViewGroup
7 |
8 | abstract class BaseViewPagerAdapter : PagerAdapter() {
9 |
10 | private val _attachedViews = SparseArray()
11 |
12 | val attachedViews: List
13 | get() = (0 until _attachedViews.size()).map { _attachedViews.valueAt(it) }
14 |
15 | fun getViewIfAttached(position: Int): View? {
16 | return _attachedViews.get(position)
17 | }
18 |
19 | abstract fun getView(position: Int, parent: ViewGroup): View
20 |
21 | open fun onItemDestroyed(position: Int, view: View) {
22 |
23 | }
24 |
25 | override fun instantiateItem(collection: ViewGroup, position: Int): Any {
26 | val view = getView(position, collection)
27 | _attachedViews.append(position, view)
28 | collection.addView(view, 0)
29 | return view
30 | }
31 |
32 | override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
33 | container.removeView(`object` as View)
34 | _attachedViews.remove(position)
35 | onItemDestroyed(position, `object`)
36 | }
37 |
38 | override fun isViewFromObject(view: View, `object`: Any): Boolean {
39 | return view === `object`
40 | }
41 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/binding/ItemEpubVerticalContentBinding.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.binding
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.ProgressBar
7 |
8 | import com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBar
9 | import com.smartmobilefactory.epubreader.R
10 | import com.smartmobilefactory.epubreader.display.vertical_content.VerticalEpubWebView
11 |
12 | internal class ItemEpubVerticalContentBinding private constructor(
13 | @JvmField
14 | val root: View
15 | ) {
16 |
17 | @JvmField
18 | val progressBar: ProgressBar = root.findViewById(R.id.progressBar) as ProgressBar
19 |
20 | @JvmField
21 | val seekbar: VerticalSeekBar = root.findViewById(R.id.seekbar) as VerticalSeekBar
22 |
23 | @JvmField
24 | val webview: VerticalEpubWebView = root.findViewById(R.id.webview) as VerticalEpubWebView
25 |
26 | companion object {
27 |
28 | @JvmStatic
29 | fun inflate(inflater: LayoutInflater, root: ViewGroup, attachToRoot: Boolean): ItemEpubVerticalContentBinding {
30 | return ItemEpubVerticalContentBinding(inflater.inflate(R.layout.item_epub_vertical_content, root, attachToRoot))
31 | }
32 |
33 | @JvmStatic
34 | fun bind(root: View): ItemEpubVerticalContentBinding {
35 | return ItemEpubVerticalContentBinding(root)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/WebViewHelper.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display
2 |
3 | import android.os.Build
4 | import android.support.annotation.MainThread
5 | import android.webkit.WebView
6 |
7 | import com.google.gson.Gson
8 |
9 | internal class WebViewHelper(private val webView: WebView) {
10 |
11 | var gson = Gson()
12 |
13 | fun callJavaScriptMethod(method: String, vararg args: Any?) {
14 | executeCommand(createJsMethodCall(method, *args))
15 | }
16 |
17 | /**
18 | * creates javascript method call
19 | */
20 | private fun createJsMethodCall(method: String, vararg args: Any?): String {
21 | val builder = StringBuilder(method.length + args.size * 32)
22 | .append(method).append('(')
23 |
24 | args.forEachIndexed { index, arg ->
25 | if (index != 0) {
26 | builder.append(',')
27 | }
28 | builder.append(gson.toJson(arg))
29 | }
30 |
31 | builder.append(')')
32 | return builder.toString()
33 | }
34 |
35 | @MainThread
36 | private fun executeCommand(javascriptCommand: String) {
37 | @Suppress("NAME_SHADOWING")
38 | var javascriptCommand = javascriptCommand
39 | javascriptCommand = "javascript:" + javascriptCommand
40 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
41 | webView.evaluateJavascript(javascriptCommand, null)
42 | } else {
43 | webView.loadUrl(javascriptCommand)
44 | }
45 | }
46 |
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/model/UncompressedEpubReader.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.model;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 |
6 | import nl.siegmann.epublib.domain.Book;
7 | import nl.siegmann.epublib.domain.Resource;
8 | import nl.siegmann.epublib.domain.Resources;
9 | import nl.siegmann.epublib.epub.EpubReader;
10 |
11 | class UncompressedEpubReader {
12 |
13 | static Book readUncompressedBook(File folder) throws IOException {
14 | Resources resources = readLazyResources(folder);
15 | return new EpubReader().readEpub(resources);
16 | }
17 |
18 | private static Resources readLazyResources(File folder) throws IOException {
19 | Resources result = new Resources();
20 | readLazyResources(folder, result, folder);
21 | return result;
22 | }
23 |
24 | private static void readLazyResources(File root, Resources resources, File folder) throws IOException {
25 | String hrefRoot = root.getAbsolutePath() + "/";
26 | for (File file : folder.listFiles()) {
27 | if (file.isDirectory()) {
28 | readLazyResources(root, resources, file);
29 | continue;
30 | }
31 | if (file.getName().equals(".ready")) {
32 | continue;
33 | }
34 |
35 | String path = file.getAbsolutePath();
36 | String href = path.replace(hrefRoot, "");
37 | Resource resource = new LazyResource(path, 0, href);
38 | resources.add(resource);
39 | }
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/view/JsApi.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.view
2 |
3 | import com.smartmobilefactory.epubreader.display.WebViewHelper
4 |
5 | internal class JsApi(
6 | private val webViewHelper: WebViewHelper
7 | ) {
8 |
9 | fun scrollToElementById(id: String) {
10 | webViewHelper.callJavaScriptMethod("scrollToElementById", id)
11 | }
12 |
13 | fun scrollToElementByXPath(xPath: String) {
14 | webViewHelper.callJavaScriptMethod("scrollToElementByXPath", xPath)
15 | }
16 |
17 | fun scrollToRangeStart(start: Int) {
18 | webViewHelper.callJavaScriptMethod("scrollToRangeStart", start)
19 | }
20 |
21 | fun setFontFamily(name: String) {
22 | webViewHelper.callJavaScriptMethod("setFontFamily", name)
23 | }
24 |
25 | fun setFont(name: String?, uri: String?) {
26 | webViewHelper.callJavaScriptMethod("setFont", name, uri)
27 | }
28 |
29 | fun updateFirstVisibleElement() {
30 | webViewHelper.callJavaScriptMethod("updateFirstVisibleElement")
31 | }
32 |
33 | fun updateFirstVisibleElementByTopPosition(value: Float) {
34 | webViewHelper.callJavaScriptMethod("updateFirstVisibleElementByTopPosition", value)
35 | }
36 |
37 | fun getYPositionOfElementWithId(id: String) {
38 | webViewHelper.callJavaScriptMethod("getYPositionOfElementWithId", id)
39 | }
40 |
41 | fun getYPositionOfElementWithXPath(xPath: String) {
42 | webViewHelper.callJavaScriptMethod("getYPositionOfElementWithXPath", xPath)
43 | }
44 |
45 | fun getYPositionOfElementFromRangeStart(start: Int) {
46 | webViewHelper.callJavaScriptMethod("getYPositionOfElementFromRangeStart", start)
47 | }
48 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/model/EpubLocation.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.model;
2 |
3 | import android.os.Parcelable;
4 | import android.support.annotation.NonNull;
5 |
6 | import com.google.auto.value.AutoValue;
7 |
8 | public abstract class EpubLocation implements Parcelable {
9 |
10 | EpubLocation() {
11 | }
12 |
13 | public static EpubLocation fromID(@NonNull int chapter, @NonNull String id) {
14 | return new AutoValue_EpubLocation_IdLocation(chapter, id);
15 | }
16 |
17 | public static EpubLocation fromRange(@NonNull int chapter, int start, int end) {
18 | return new AutoValue_EpubLocation_RangeLocation(chapter, start, end);
19 | }
20 |
21 | public static ChapterLocation fromChapter(@NonNull int chapter) {
22 | return new AutoValue_EpubLocation_ChapterLocationImpl(chapter);
23 | }
24 |
25 | public static XPathLocation fromXPath(int chapter, String xPath) {
26 | return new AutoValue_EpubLocation_XPathLocation(chapter, xPath);
27 | }
28 |
29 | public abstract static class ChapterLocation extends EpubLocation {
30 | public abstract int chapter();
31 | }
32 |
33 | @AutoValue
34 | public abstract static class ChapterLocationImpl extends ChapterLocation {
35 | }
36 |
37 | @AutoValue
38 | public abstract static class IdLocation extends ChapterLocation {
39 | public abstract String id();
40 | }
41 |
42 | @AutoValue
43 | public abstract static class XPathLocation extends ChapterLocation {
44 | public abstract String xPath();
45 | }
46 |
47 | @AutoValue
48 | public abstract static class RangeLocation extends ChapterLocation {
49 | public abstract int start();
50 |
51 | public abstract int end();
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | apply plugin: 'kotlin-android'
4 | apply plugin: 'com.github.dcendents.android-maven'
5 |
6 | group='com.github.smartmobilefactory'
7 |
8 | android {
9 | compileSdkVersion 25
10 |
11 | defaultConfig {
12 | minSdkVersion 16
13 | targetSdkVersion 25
14 | versionCode 1
15 | versionName "0.2.2"
16 |
17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
18 |
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 |
28 | compileOptions {
29 | sourceCompatibility JavaVersion.VERSION_1_8
30 | targetCompatibility JavaVersion.VERSION_1_8
31 | }
32 |
33 | }
34 |
35 | dependencies {
36 | compile fileTree(dir: 'libs', include: ['*.jar'])
37 |
38 | api "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
39 |
40 | def supportLibVersion = "25.2.0"
41 | compile "com.android.support:appcompat-v7:${supportLibVersion}"
42 | compile "com.android.support:recyclerview-v7:${supportLibVersion}"
43 |
44 | api 'io.reactivex.rxjava2:rxjava:2.1.6'
45 | api 'io.reactivex.rxjava2:rxandroid:2.0.1'
46 | implementation 'com.h6ah4i.android.widget.verticalseekbar:verticalseekbar:0.7.1'
47 |
48 | annotationProcessor 'com.google.auto.value:auto-value:1.5'
49 | compileOnly 'com.jakewharton.auto.value:auto-value-annotations:1.5'
50 | annotationProcessor 'com.ryanharter.auto.value:auto-value-parcel:0.2.5'
51 |
52 | api 'com.google.code.gson:gson:2.8.2'
53 |
54 | api ('com.github.timfreiheit.epublib:epublib-core:645ea52ef8') {
55 | exclude group: 'xmlpull'
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EpubReaderAndroid
2 |
3 | ePub-Reader for Android to make it easy integrate and customize epub-Reader functionality in any Android App
4 |
5 |
6 | ## Simple Integrate
7 |
8 | ```xml
9 |
10 |
14 |
15 | ```
16 |
17 | ```java
18 |
19 | EpubView epubView = findViewById(R.id.epubView);
20 | epubView.setEpub(epub);
21 |
22 | ```
23 |
24 |
25 | ## Display Modes
26 |
27 | The Epub can be displayed in different modes:
28 |
29 | - horizontal chapters + vertical content
30 | ```java
31 |
32 | epubView.setScrollDirection(EpubScrollDirection.HORIZONTAL_WITH_VERTICAL_CONTENT);
33 |
34 | ```
35 | - vertical chapters + vertical content
36 | ```java
37 |
38 | epubView.setScrollDirection(EpubScrollDirection.VERTICAL_WITH_VERTICAL_CONTENT);
39 |
40 | ```
41 | - single chapters + vertical content
42 | ```java
43 |
44 | epubView.setScrollDirection(EpubScrollDirection.SINGLE_CHAPTER_VERTICAL);
45 |
46 | ```
47 |
48 | More modes may be implemented later.
49 |
50 | ## Customization Settings
51 |
52 | ```java
53 |
54 | EpubViewSettings settings = epubView.getSettings();
55 | settings.setFont(EpubFont.fromFontFamiliy("Monospace"));
56 | settings.setFontSizeSp(30);
57 |
58 | // inject code into chapters
59 | settings.setJavascriptBridge(bridge);
60 | settings.setCustomChapterScript(...);
61 | settings.setCustomChapterCss(...);
62 |
63 | ```
64 |
65 | ## Observe current status
66 |
67 | observation is implemented using RxJava2
68 |
69 | - Current Chapter
70 | - Current Location
71 |
72 |
73 | ## Installation
74 |
75 |
76 | ```groovy
77 |
78 | repositories {
79 | // ...
80 | maven { url "https://jitpack.io" }
81 | }
82 |
83 | dependencies {
84 | compile 'com.github.smartmobilefactory:EpubReaderAndroid:XXX'
85 | }
86 |
87 | ```
88 |
89 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/SavedState.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import android.support.v4.os.ParcelableCompat
6 | import android.support.v4.os.ParcelableCompatCreatorCallbacks
7 | import android.support.v4.view.AbsSavedState
8 | import com.smartmobilefactory.epubreader.model.EpubLocation
9 |
10 | internal class SavedState : AbsSavedState {
11 |
12 | var epubUri: String? = null
13 | var location: EpubLocation? = null
14 | var loader: ClassLoader? = null
15 |
16 | internal constructor(superState: Parcelable) : super(superState) {}
17 |
18 | override fun writeToParcel(out: Parcel, flags: Int) {
19 | super.writeToParcel(out, flags)
20 | out.writeString(epubUri)
21 | out.writeParcelable(location, flags)
22 | }
23 |
24 | override fun toString(): String {
25 | return ("EpubView.SavedState{"
26 | + Integer.toHexString(System.identityHashCode(this))
27 | + " location=" + location + "}")
28 | }
29 |
30 | internal constructor(inParcel: Parcel, loader: ClassLoader?) : super(inParcel, loader) {
31 | this.loader = loader
32 | if (loader == null) {
33 | this.loader = javaClass.classLoader
34 | }
35 | epubUri = inParcel.readString()
36 | location = inParcel.readParcelable(loader)
37 | }
38 |
39 | companion object {
40 |
41 | @JvmField
42 | val CREATOR: Parcelable.Creator = ParcelableCompat.newCreator(
43 | object : ParcelableCompatCreatorCallbacks {
44 | override fun createFromParcel(`in`: Parcel, loader: ClassLoader): SavedState {
45 | return SavedState(`in`, loader)
46 | }
47 |
48 | override fun newArray(size: Int): Array {
49 | return arrayOfNulls(size)
50 | }
51 | })
52 | }
53 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/EpubDisplayStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display
2 |
3 | import android.support.annotation.CallSuper
4 | import android.view.ViewGroup
5 |
6 | import com.smartmobilefactory.epubreader.EpubView
7 | import com.smartmobilefactory.epubreader.model.Epub
8 | import com.smartmobilefactory.epubreader.model.EpubLocation
9 | import io.reactivex.Observable
10 | import io.reactivex.disposables.CompositeDisposable
11 | import io.reactivex.subjects.BehaviorSubject
12 |
13 | internal abstract class EpubDisplayStrategy {
14 |
15 | protected val compositeDisposable = CompositeDisposable()
16 |
17 | private val currentChapterSubject = BehaviorSubject.createDefault(0)
18 |
19 | private val currentLocationSubject = BehaviorSubject.create()
20 |
21 | var currentChapter: Int
22 | get() = currentChapterSubject.value
23 | set(position) = currentChapterSubject.onNext(position)
24 |
25 | var currentLocation: EpubLocation
26 | get() = currentLocationSubject.value
27 | set(location) = currentLocationSubject.onNext(location)
28 |
29 | abstract fun bind(epubView: EpubView, parent: ViewGroup)
30 |
31 | @CallSuper
32 | fun unbind() {
33 | compositeDisposable.clear()
34 | }
35 |
36 | abstract fun displayEpub(epub: Epub, location: EpubLocation)
37 |
38 | abstract fun gotoLocation(location: EpubLocation)
39 |
40 | fun onChapterChanged(): Observable {
41 | return currentChapterSubject
42 | }
43 |
44 | fun currentLocation(): Observable {
45 | return currentLocationSubject
46 | }
47 |
48 | /**
49 | * calls a javascript method on all visible chapters
50 | * this depends on the selected display strategy
51 | */
52 | open fun callChapterJavascriptMethod(name: String, vararg args: Any) {
53 | callChapterJavascriptMethod(currentChapter, name, args)
54 | }
55 |
56 | /**
57 | * calls a javascript method on the selected chapter if visible
58 | * this depends on the selected display strategy
59 | */
60 | abstract fun callChapterJavascriptMethod(chapter: Int, name: String, vararg args: Any)
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/vertical_content/VerticalEpubWebView.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.vertical_content
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 |
6 | import com.google.auto.value.AutoValue
7 | import com.smartmobilefactory.epubreader.display.view.EpubWebView
8 | import com.smartmobilefactory.epubreader.utils.BaseDisposableObserver
9 |
10 | import java.util.concurrent.TimeUnit
11 |
12 | import io.reactivex.Observable
13 | import io.reactivex.android.schedulers.AndroidSchedulers
14 | import io.reactivex.disposables.CompositeDisposable
15 | import io.reactivex.subjects.BehaviorSubject
16 |
17 | internal class VerticalEpubWebView @JvmOverloads constructor(
18 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
19 | ) : EpubWebView(context, attrs, defStyleAttr) {
20 |
21 | private val compositeDisposable = CompositeDisposable()
22 |
23 | private val currentScrollState = BehaviorSubject.createDefault(ScrollState(0, 100))
24 |
25 | data class ScrollState(
26 | val top: Int,
27 | val maxTop: Int
28 | )
29 |
30 | init {
31 | verticalScrollState()
32 | .sample(200, TimeUnit.MILLISECONDS)
33 | .observeOn(AndroidSchedulers.mainThread())
34 | .doOnNext {
35 | if (progress == 100) {
36 | js.updateFirstVisibleElement()
37 | }
38 | }
39 | .subscribeWith(BaseDisposableObserver())
40 | .addTo(compositeDisposable)
41 | }
42 |
43 | override fun onDetachedFromWindow() {
44 | super.onDetachedFromWindow()
45 | compositeDisposable.clear()
46 | }
47 |
48 | override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
49 |
50 | val height = Math.floor((contentHeight * scale).toDouble()).toInt()
51 | val webViewHeight = measuredHeight
52 |
53 | currentScrollState.onNext(ScrollState(t, height - webViewHeight))
54 | super.onScrollChanged(l, t, oldl, oldt)
55 | }
56 |
57 | fun verticalScrollState(): Observable {
58 | return currentScrollState
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/com/smartmobilefactory/epubreader/sample/ChapterJavaScriptBridge.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.sample;
2 |
3 | import android.util.Log;
4 | import android.util.SparseArray;
5 | import android.webkit.JavascriptInterface;
6 |
7 | import org.json.JSONException;
8 | import org.json.JSONObject;
9 |
10 | public class ChapterJavaScriptBridge {
11 |
12 | private static final String TAG = ChapterJavaScriptBridge.class.getSimpleName();
13 |
14 | // in memory storage of all highlights
15 | private SparseArray highlights = new SparseArray<>();
16 |
17 | /**
18 | * @return
19 | * [
20 | * {
21 | * id : ...,
22 | * chapterId: ...,
23 | * start: ...,
24 | * end: ...,
25 | * color: ...
26 | * }
27 | * ]
28 | */
29 | @JavascriptInterface
30 | public String getHighlights(int chapter) {
31 | String value = highlights.get(chapter);
32 | if (value == null) {
33 | return "[]";
34 | }
35 | return value;
36 | }
37 |
38 | @JavascriptInterface
39 | public void onHighlightAdded(int chapter, String data) {
40 | try {
41 | JSONObject object = new JSONObject(data);
42 | highlights.put(chapter, object.getJSONArray("highlights").toString());
43 | } catch (JSONException e) {
44 | e.printStackTrace();
45 | }
46 | Log.d(TAG, "onHighlightAdded() called with " + "data = [" + data + "]");
47 | }
48 |
49 | @JavascriptInterface
50 | public void onHighlightClicked(String json) {
51 | Log.d(TAG, "onHighlightClicked() called with " + "json = [" + json + "]");
52 | }
53 |
54 | @JavascriptInterface
55 | public void onSelectionChanged(int length) {
56 | Log.d(TAG, "onSelectionChanged() called with " + "length = [" + length + "]");
57 | }
58 |
59 | public String[] getCustomChapterScripts() {
60 | return new String[]{
61 | "file:///android_asset/books/javascript/rangy/rangy-core.min.js",
62 | "file:///android_asset/books/javascript/rangy/rangy-classapplier.min.js",
63 | "file:///android_asset/books/javascript/rangy/rangy-highlighter.min.js",
64 | "file:///android_asset/books/javascript/rangy/rangy-serializer.min.js",
65 | "file:///android_asset/books/javascript/highlight.js"
66 | };
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/scripts/dumpapp:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import sys
4 | import os
5 | import io
6 |
7 | from stetho_open import *
8 |
9 | def main():
10 | # Manually parse out -p , all other option handling occurs inside
11 | # the hosting process.
12 |
13 | # Connect to the process passed in via -p. If that is not supplied fallback
14 | # the process defined in STETHO_PROCESS. If neither are defined throw.
15 | process = os.environ.get('STETHO_PROCESS')
16 |
17 | args = sys.argv[1:]
18 | if len(args) > 0 and (args[0] == '-p' or args[0] == '--process'):
19 | if len(args) < 2:
20 | sys.exit('Missing ')
21 | else:
22 | process = args[1]
23 | args = args[2:]
24 |
25 | # Connect to ANDROID_SERIAL if supplied, otherwise fallback to any
26 | # transport.
27 | device = os.environ.get('ANDROID_SERIAL')
28 |
29 | try:
30 | sock = stetho_open(device, process)
31 |
32 | # Send dumpapp hello (DUMP + version=1)
33 | sock.send(b'DUMP' + struct.pack('!L', 1))
34 |
35 | enter_frame = b'!' + struct.pack('!L', len(args))
36 | for arg in args:
37 | argAsUTF8 = arg.encode('utf-8')
38 | enter_frame += struct.pack(
39 | '!H' + str(len(argAsUTF8)) + 's',
40 | len(argAsUTF8),
41 | argAsUTF8)
42 | sock.send(enter_frame)
43 |
44 | read_frames(sock)
45 | except HumanReadableError as e:
46 | sys.exit(e)
47 | except BrokenPipeError as e:
48 | sys.exit(0)
49 | except KeyboardInterrupt:
50 | sys.exit(1)
51 |
52 | def read_frames(sock):
53 | while True:
54 | # All frames have a single character code followed by a big-endian int
55 | code = read_input(sock, 1, 'code')
56 | n = struct.unpack('!L', read_input(sock, 4, 'int4'))[0]
57 |
58 | if code == b'1':
59 | if n > 0:
60 | sys.stdout.buffer.write(read_input(sock, n, 'stdout blob'))
61 | sys.stdout.buffer.flush()
62 | elif code == b'2':
63 | if n > 0:
64 | sys.stderr.buffer.write(read_input(sock, n, 'stderr blob'))
65 | sys.stderr.buffer.flush()
66 | elif code == b'_':
67 | if n > 0:
68 | data = sys.stdin.buffer.read(n)
69 | if len(data) == 0:
70 | sock.send(b'-' + struct.pack('!L', -1))
71 | else:
72 | sock.send(b'-' + struct.pack('!L', len(data)) + data)
73 | elif code == b'x':
74 | sys.exit(n)
75 | else:
76 | if raise_on_eof:
77 | raise IOError('Unexpected header: %s' % code)
78 | break
79 |
80 | if __name__ == '__main__':
81 | main()
82 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/library/src/main/assets/epubreaderandroid/script.js:
--------------------------------------------------------------------------------
1 |
2 | var customFonts = {};
3 |
4 | function setFont(name, fontUri) {
5 | var fontName = "custom_font" + name;
6 |
7 | if (customFonts[fontName]) {
8 | setFontFamily(fontName);
9 | return;
10 | }
11 |
12 | var newStyle = document.createElement('style');
13 | newStyle.appendChild(document.createTextNode("\
14 | @font-face {\
15 | font-family: '"+ fontName + "';\
16 | src: url('" + fontUri + "');\
17 | }\
18 | "));
19 |
20 | document.head.appendChild(newStyle);
21 | customFonts[fontName] = true;
22 | setFontFamily(fontName);
23 | }
24 |
25 | function setFontFamily(font_family) {
26 | document.body.style.fontFamily = font_family;
27 | }
28 |
29 | function scrollToElementById(element){
30 | var element = document.getElementById(element);
31 | scrollElementIntoView(element);
32 | }
33 |
34 | function scrollToElementByXPath(xpath){
35 | var element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
36 | scrollElementIntoView(element);
37 | }
38 |
39 | function scrollToRangeStart(start) {
40 | var element = getElementFromRangeStart(start);
41 | scrollElementIntoView(element);
42 | }
43 |
44 | function scrollElementIntoView(element) {
45 | if (element.children.length > 0) {
46 | // in some cases it is more accurate to scroll to the first children
47 | element = element.children[0];
48 | }
49 | // scrollIntoView(TOP)
50 | element.scrollIntoView(true);
51 | }
52 |
53 | function updateFirstVisibleElementByTopPosition(top) {
54 | var element = document.elementFromPoint(100, top);
55 | if (!element) {
56 | return;
57 | }
58 | var xpath = getXPathTo(element);
59 | internalBridge.onLocationChanged(xpath);
60 | }
61 |
62 | function updateFirstVisibleElement() {
63 | var element = getFirstVisibleElement(0);
64 | if (!element) {
65 | return;
66 | }
67 | var xpath = getXPathTo(element);
68 | internalBridge.onLocationChanged(xpath);
69 | }
70 |
71 | function getYPositionOfElementWithId(id) {
72 | var element = document.getElementById(element);
73 | publishResultGetYPositionOfElement(element);
74 | }
75 |
76 | function getYPositionOfElementWithXPath(xpath) {
77 | var element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
78 | publishResultGetYPositionOfElement(element);
79 | }
80 |
81 | function getYPositionOfElementFromRangeStart(start) {
82 | var element = getElementFromRangeStart(start);
83 | publishResultGetYPositionOfElement(element);
84 | }
85 |
86 | function publishResultGetYPositionOfElement(element) {
87 | if (!element) {
88 | console.log("element not found");
89 | return;
90 | }
91 | var rect = element.getBoundingClientRect();
92 | if (!rect) {
93 | console.log("element position not found");
94 | return;
95 | }
96 | internalBridge.resultGetYPositionOfElement(rect.top);
97 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/vertical_content/SingleChapterVerticalEpubDisplayStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.vertical_content
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 |
6 | import com.smartmobilefactory.epubreader.EpubView
7 | import com.smartmobilefactory.epubreader.EpubViewSettings
8 | import com.smartmobilefactory.epubreader.InternalEpubViewSettings
9 | import com.smartmobilefactory.epubreader.display.EpubDisplayStrategy
10 | import com.smartmobilefactory.epubreader.display.binding.ItemEpubVerticalContentBinding
11 | import com.smartmobilefactory.epubreader.display.view.InternalEpubBridge
12 | import com.smartmobilefactory.epubreader.model.Epub
13 | import com.smartmobilefactory.epubreader.model.EpubLocation
14 | import nl.siegmann.epublib.domain.SpineReference
15 |
16 | internal class SingleChapterVerticalEpubDisplayStrategy : EpubDisplayStrategy() {
17 | private lateinit var epubView: EpubView
18 | private lateinit var binding: ItemEpubVerticalContentBinding
19 |
20 | private var settings: InternalEpubViewSettings? = null
21 |
22 | override fun bind(epubView: EpubView, parent: ViewGroup) {
23 | this.epubView = epubView
24 | val inflater = LayoutInflater.from(parent.context)
25 | binding = ItemEpubVerticalContentBinding.inflate(inflater, parent, true)
26 | settings = epubView.internalSettings
27 | }
28 |
29 | override fun displayEpub(epub: Epub, location: EpubLocation) {
30 | if (location is EpubLocation.ChapterLocation) {
31 | val chapter = location.chapter()
32 | val spineReference = epub.book.spine.spineReferences[chapter]
33 | binding.webview.loadEpubPage(epub, spineReference, epubView.internalSettings)
34 | currentChapter = chapter
35 | }
36 |
37 | binding.webview.gotoLocation(location)
38 | binding.webview.setUrlInterceptor { url -> epubView.shouldOverrideUrlLoading(url) }
39 | binding.webview.bindToSettings(settings)
40 |
41 | val bridge = InternalEpubBridge()
42 | binding.webview.setInternalBridge(bridge)
43 |
44 | bridge.xPath().subscribe { xPath -> currentLocation = EpubLocation.fromXPath(currentChapter, xPath) }
45 |
46 | VerticalContentBinderHelper.bind(binding)
47 | }
48 |
49 | override fun gotoLocation(location: EpubLocation) {
50 | if (location is EpubLocation.ChapterLocation) {
51 | if (currentChapter == location.chapter()) {
52 | binding.webview.gotoLocation(location)
53 | } else {
54 | epubView.getEpub()?.let { displayEpub(it, location) }
55 | }
56 | currentLocation = location
57 | }
58 | }
59 |
60 | override fun callChapterJavascriptMethod(chapter: Int, name: String, vararg args: Any) {
61 | if (chapter == currentChapter) {
62 | callChapterJavascriptMethod(name, *args)
63 | }
64 | }
65 |
66 | override fun callChapterJavascriptMethod(name: String, vararg args: Any) {
67 | binding.webview.callJavascriptMethod(name, *args)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/InternalEpubViewSettings.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import com.smartmobilefactory.epubreader.model.EpubFont
6 | import io.reactivex.Observable
7 | import io.reactivex.disposables.Disposable
8 | import io.reactivex.subjects.PublishSubject
9 |
10 | internal class InternalEpubViewSettings(
11 | private val settings: EpubViewSettings
12 | ) {
13 |
14 | private val settingsChangedSubject = PublishSubject.create()
15 | private val mainThreadHandler = Handler(Looper.getMainLooper())
16 | private val disposables = mutableMapOf()
17 |
18 | private val plugins = mutableListOf()
19 |
20 | val fontSizeSp: Int
21 | get() = settings.fontSizeSp
22 |
23 | val font: EpubFont
24 | get() = settings.font
25 |
26 | val customChapterCss: List
27 | get() = mutableSetOf().apply {
28 | addAll(settings.customChapterCss)
29 | plugins.forEach { plugin ->
30 | addAll(plugin.customChapterCss)
31 | }
32 | }.toList()
33 |
34 | val customChapterScripts: List
35 | get() = mutableSetOf().apply {
36 | addAll(settings.customChapterScripts)
37 | plugins.forEach { plugin ->
38 | addAll(plugin.customChapterScripts)
39 | }
40 | }.toList()
41 |
42 | val javascriptBridges: List
43 | get() = mutableListOf().apply {
44 | if (settings.javascriptBridge != null) {
45 | add(EpubJavaScriptBridge("bridge", settings.javascriptBridge))
46 | }
47 | plugins.forEach { plugin ->
48 | plugin.javascriptBridge?.let { add(it) }
49 | }
50 | }
51 |
52 | fun addPlugin(epubPlugin: EpubViewPlugin) {
53 | if (!plugins.contains(epubPlugin)) {
54 | plugins.add(epubPlugin)
55 | val disposable = epubPlugin
56 | .dataChanged()
57 | .subscribe {
58 | onSettingHasChanged(setting = EpubViewSettings.Setting.CUSTOM_FILES)
59 | }
60 | disposables[epubPlugin] = disposable
61 | }
62 | }
63 |
64 | fun removePlugin(epubPlugin: EpubViewPlugin) {
65 | plugins.remove(epubPlugin)
66 | disposables[epubPlugin]?.dispose()
67 | disposables.remove(epubPlugin)
68 | }
69 |
70 | private fun onSettingHasChanged(setting: EpubViewSettings.Setting) {
71 | // make sure the values only updates on the main thread but avoid delaying events
72 | if (Looper.getMainLooper().thread === Thread.currentThread()) {
73 | settingsChangedSubject.onNext(setting)
74 | } else {
75 | mainThreadHandler.post({ settingsChangedSubject.onNext(setting) })
76 | }
77 | }
78 |
79 | fun anySettingHasChanged(): Observable {
80 | return settingsChangedSubject.mergeWith(settings.anySettingHasChanged())
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/EpubViewSettings.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader;
2 |
3 | import android.os.Handler;
4 | import android.os.Looper;
5 | import android.support.annotation.NonNull;
6 |
7 | import com.smartmobilefactory.epubreader.model.EpubFont;
8 |
9 | import io.reactivex.Observable;
10 | import io.reactivex.subjects.PublishSubject;
11 |
12 | public class EpubViewSettings {
13 |
14 | public enum Setting {
15 | FONT,
16 | FONT_SIZE,
17 | JAVASCRIPT_BRIDGE,
18 | // custom scripts, custom css
19 | CUSTOM_FILES
20 | }
21 |
22 | private EpubFont font = EpubFont.fromUri(null, null);
23 | private int fontSizeSp = 18;
24 |
25 | private Object javascriptBridge = new Object();
26 | private String[] customChapterScripts = new String[0];
27 | private String[] customChapterCss = new String[0];
28 |
29 | private Handler mainThreadHandler = new Handler(Looper.getMainLooper());
30 | private PublishSubject settingsChangedSubject = PublishSubject.create();
31 |
32 | EpubViewSettings() {
33 | // only the EpubView should construct settings
34 | }
35 |
36 | public void setFont(EpubFont font) {
37 | if (font == null) {
38 | font = EpubFont.fromUri(null, null);
39 | }
40 | if (this.font.equals(font)) {
41 | return;
42 | }
43 | this.font = font;
44 | onSettingHasChanged(Setting.FONT);
45 | }
46 |
47 | public EpubFont getFont() {
48 | return font;
49 | }
50 |
51 | public void setFontSizeSp(int sp) {
52 | if (sp == fontSizeSp) {
53 | return;
54 | }
55 | this.fontSizeSp = sp;
56 | onSettingHasChanged(Setting.FONT_SIZE);
57 | }
58 |
59 | public int getFontSizeSp() {
60 | return fontSizeSp;
61 | }
62 |
63 | public void setCustomChapterScript(String... customChapterScripts) {
64 | this.customChapterScripts = customChapterScripts;
65 | onSettingHasChanged(Setting.CUSTOM_FILES);
66 | }
67 |
68 | public String[] getCustomChapterScripts() {
69 | return customChapterScripts;
70 | }
71 |
72 | public void setJavascriptBridge(Object javascriptBridge) {
73 | if (javascriptBridge == this.javascriptBridge) {
74 | return;
75 | }
76 | this.javascriptBridge = javascriptBridge;
77 | onSettingHasChanged(Setting.JAVASCRIPT_BRIDGE);
78 | }
79 |
80 | public Object getJavascriptBridge() {
81 | return javascriptBridge;
82 | }
83 |
84 | public void setCustomChapterCss(String[] css) {
85 | this.customChapterCss = css;
86 | onSettingHasChanged(Setting.CUSTOM_FILES);
87 | }
88 |
89 | public String[] getCustomChapterCss() {
90 | return customChapterCss;
91 | }
92 |
93 | private void onSettingHasChanged(@NonNull Setting setting) {
94 | // make sure the values only updates on the main thread but avoid delaying events
95 | if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
96 | settingsChangedSubject.onNext(setting);
97 | } else {
98 | mainThreadHandler.post(() -> settingsChangedSubject.onNext(setting));
99 | }
100 | }
101 |
102 | public Observable anySettingHasChanged() {
103 | return settingsChangedSubject;
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/vertical_content/horizontal_chapters/HorizontalWithVerticalContentEpubDisplayStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.vertical_content.horizontal_chapters
2 |
3 | import android.support.v4.view.ViewPager
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import com.smartmobilefactory.epubreader.EpubView
7 | import com.smartmobilefactory.epubreader.display.EpubDisplayStrategy
8 | import com.smartmobilefactory.epubreader.display.binding.EpubHorizontalVerticalContentBinding
9 | import com.smartmobilefactory.epubreader.display.view.EpubWebView
10 | import com.smartmobilefactory.epubreader.model.Epub
11 | import com.smartmobilefactory.epubreader.model.EpubLocation
12 |
13 | internal class HorizontalWithVerticalContentEpubDisplayStrategy : EpubDisplayStrategy() {
14 |
15 | private lateinit var binding: EpubHorizontalVerticalContentBinding
16 |
17 | private lateinit var epubView: EpubView
18 | private lateinit var pagerAdapter: PagerAdapter
19 |
20 | internal val urlInterceptor: (String) -> Boolean = { url: String -> epubView.shouldOverrideUrlLoading(url) }
21 |
22 | override fun bind(epubView: EpubView, parent: ViewGroup) {
23 | this.epubView = epubView
24 | val inflater = LayoutInflater.from(parent.context)
25 | binding = EpubHorizontalVerticalContentBinding.inflate(inflater, parent, true)
26 |
27 | binding.pager.offscreenPageLimit = 1
28 | binding.pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
29 | override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
30 |
31 | override fun onPageSelected(position: Int) {
32 | currentChapter = position
33 | val location = pagerAdapter.getChapterLocation(position)
34 | ?: EpubLocation.fromChapter(position)
35 | currentLocation = location
36 | }
37 |
38 | override fun onPageScrollStateChanged(state: Int) {}
39 | })
40 | pagerAdapter = PagerAdapter(this, epubView)
41 | binding.pager.adapter = pagerAdapter
42 | }
43 |
44 | override fun displayEpub(epub: Epub, location: EpubLocation) {
45 | pagerAdapter.displayEpub(epub, location)
46 |
47 | if (location is EpubLocation.ChapterLocation) {
48 | binding.pager.currentItem = location.chapter()
49 | }
50 |
51 | }
52 |
53 | override fun gotoLocation(location: EpubLocation) {
54 | if (location is EpubLocation.ChapterLocation) {
55 |
56 | val binding = pagerAdapter.getViewBindingIfAttached(location.chapter())
57 | if (binding != null) {
58 | binding.webview.gotoLocation(location)
59 | } else {
60 | displayEpub(epubView.getEpub()!!, location)
61 | }
62 |
63 | this.binding.pager.currentItem = location.chapter()
64 | currentLocation = location
65 | }
66 | }
67 |
68 | override fun callChapterJavascriptMethod(chapter: Int, name: String, vararg args: Any) {
69 | val binding = pagerAdapter.getViewBindingIfAttached(chapter)
70 | binding?.webview?.callJavascriptMethod(name, *args)
71 | }
72 |
73 | override fun callChapterJavascriptMethod(name: String, vararg args: Any) {
74 | for (binding in pagerAdapter.attachedViewBindings) {
75 | binding.webview.callJavascriptMethod(name, *args)
76 | }
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/model/FileUtils.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.model;
2 |
3 | import java.io.File;
4 | import java.io.FileNotFoundException;
5 | import java.io.IOException;
6 |
7 | /**
8 | * http://grepcode.com/file_/repo1.maven.org/maven2/commons-io/commons-io/1.1/org/apache/commons/io/FileUtils.java/?v=source
9 | */
10 | class FileUtils {
11 |
12 | public static void tryDeleteDirectory(File directory)
13 | throws IOException {
14 | try {
15 | deleteDirectory(directory);
16 | } catch (Exception e){}
17 | }
18 |
19 | /**
20 | * Recursively delete a directory.
21 | * @param directory directory to delete
22 | * @throws IOException in case deletion is unsuccessful
23 | */
24 | public static void deleteDirectory(File directory)
25 | throws IOException {
26 | if (!directory.exists()) {
27 | return;
28 | }
29 |
30 | cleanDirectory(directory);
31 | if (!directory.delete()) {
32 | String message =
33 | "Unable to delete directory " + directory + ".";
34 | throw new IOException(message);
35 | }
36 | }
37 |
38 | /**
39 | * Clean a directory without deleting it.
40 | * @param directory directory to clean
41 | * @throws IOException in case cleaning is unsuccessful
42 | */
43 | public static void cleanDirectory(File directory) throws IOException {
44 | if (!directory.exists()) {
45 | String message = directory + " does not exist";
46 | throw new IllegalArgumentException(message);
47 | }
48 |
49 | if (!directory.isDirectory()) {
50 | String message = directory + " is not a directory";
51 | throw new IllegalArgumentException(message);
52 | }
53 |
54 | File[] files = directory.listFiles();
55 | if (files == null) { // null if security restricted
56 | throw new IOException("Failed to list contents of " + directory);
57 | }
58 |
59 | IOException exception = null;
60 | for (int i = 0; i < files.length; i++) {
61 | File file = files[i];
62 | try {
63 | forceDelete(file);
64 | } catch (IOException ioe) {
65 | exception = ioe;
66 | }
67 | }
68 |
69 | if (null != exception) {
70 | throw exception;
71 | }
72 | }
73 |
74 | /**
75 | *
76 | * Delete a file. If file is a directory, delete it and all sub-directories.
77 | *
78 | *
79 | * The difference between File.delete() and this method are:
80 | *
81 | *
82 | * - A directory to be deleted does not have to be empty.
83 | * - You get exceptions when a file or directory cannot be deleted.
84 | * (java.io.File methods returns a boolean)
85 | *
86 | * @param file file or directory to delete.
87 | * @throws IOException in case deletion is unsuccessful
88 | */
89 | public static void forceDelete(File file) throws IOException {
90 | if (file.isDirectory()) {
91 | deleteDirectory(file);
92 | } else {
93 | if (!file.exists()) {
94 | throw new FileNotFoundException("File does not exist: " + file);
95 | }
96 | if (!file.delete()) {
97 | String message =
98 | "Unable to delete file: " + file;
99 | throw new IOException(message);
100 | }
101 | }
102 | }
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/java/com/smartmobilefactory/epubreader/sample/TableOfContentsAdapter.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.sample;
2 |
3 | import android.graphics.Color;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.LayoutInflater;
6 | import android.view.ViewGroup;
7 |
8 | import com.smartmobilefactory.epubreader.EpubView;
9 | import com.smartmobilefactory.epubreader.model.Epub;
10 | import com.smartmobilefactory.epubreader.sample.databinding.TableOfContentsItemBinding;
11 | import com.smartmobilefactory.epubreader.utils.BaseDisposableObserver;
12 |
13 | import java.util.ArrayList;
14 | import java.util.List;
15 |
16 | import io.reactivex.Observable;
17 | import io.reactivex.subjects.PublishSubject;
18 | import nl.siegmann.epublib.domain.TOCReference;
19 |
20 | public class TableOfContentsAdapter extends RecyclerView.Adapter {
21 |
22 | private int currentTocPosition = -1;
23 |
24 | private final List tableOfContents = new ArrayList<>();
25 | private Epub epub;
26 |
27 | private PublishSubject jumpToChapter = PublishSubject.create();
28 |
29 | public void bindToEpubView(EpubView epubView) {
30 | epubView.currentChapter()
31 | .filter(integer -> epub != null)
32 | .filter(integer -> epub.equals(epubView.getEpub()))
33 | .doOnNext(chapter -> {
34 | currentTocPosition = epub.getTocPositionForSpinePosition(chapter);
35 | notifyDataSetChanged();
36 | })
37 | .subscribe(new BaseDisposableObserver<>());
38 | }
39 |
40 | public void setEpub(Epub epub) {
41 | this.epub = epub;
42 | tableOfContents.clear();
43 | fillToc(epub.getBook().getTableOfContents().getTocReferences());
44 |
45 | notifyDataSetChanged();
46 | }
47 |
48 | private void fillToc(List tocReferences) {
49 | for (TOCReference tocReference : tocReferences) {
50 | tableOfContents.add(tocReference);
51 | fillToc(tocReference.getChildren());
52 | }
53 | }
54 |
55 | @Override
56 | public VH onCreateViewHolder(ViewGroup parent, int viewType) {
57 | return new VH(TableOfContentsItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
58 | }
59 |
60 | @Override
61 | public void onBindViewHolder(VH holder, int position) {
62 | TOCReference tocReference = tableOfContents.get(position);
63 | String title = tocReference.getTitle();
64 | if (title == null) {
65 | title = "Chapter " + (position + 1);
66 | }
67 | holder.binding.chapterTitle.setText(title);
68 |
69 | if (position == currentTocPosition) {
70 | holder.binding.chapterTitle.setBackgroundColor(Color.LTGRAY);
71 | } else {
72 | holder.binding.chapterTitle.setBackgroundColor(Color.TRANSPARENT);
73 | }
74 |
75 | holder.binding.getRoot().setOnClickListener(v -> {
76 | // search correct chapter (spine position) for toc entry
77 | int spinePosition = epub.getSpinePositionForTocReference(tocReference);
78 | if (spinePosition >= 0) {
79 | jumpToChapter.onNext(spinePosition);
80 | }
81 | });
82 |
83 | }
84 |
85 | @Override
86 | public int getItemCount() {
87 | if (epub == null) {
88 | return 0;
89 | }
90 | return tableOfContents.size();
91 | }
92 |
93 | public Observable jumpToChapter() {
94 | return jumpToChapter;
95 | }
96 |
97 | static class VH extends RecyclerView.ViewHolder {
98 |
99 | TableOfContentsItemBinding binding;
100 |
101 | public VH(TableOfContentsItemBinding binding) {
102 | super(binding.getRoot());
103 | this.binding = binding;
104 | }
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/model/EpubStorageHelper.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.model;
2 |
3 | import android.content.Context;
4 | import android.net.Uri;
5 |
6 | import java.io.BufferedReader;
7 | import java.io.File;
8 | import java.io.FileInputStream;
9 | import java.io.IOException;
10 | import java.io.InputStream;
11 | import java.io.InputStreamReader;
12 |
13 | import nl.siegmann.epublib.domain.Book;
14 |
15 | class EpubStorageHelper {
16 |
17 | static final String ANDROID_ASSETS = "file:///android_asset/";
18 |
19 | static File getEpubReaderCacheDir(Context context) {
20 | File cacheDir = new File(context.getCacheDir(), "epubreader_cache");
21 | cacheDir.mkdirs();
22 | return cacheDir;
23 | }
24 |
25 | static File getOpfPath(Epub epub) {
26 | return getOpfPath(epub.getLocation());
27 | }
28 |
29 | static File getOpfPath(File folder) {
30 | String relativeOpfPath = "";
31 | try {
32 | // get the OPF path, directly from container.xml
33 |
34 | BufferedReader br
35 | = new BufferedReader(new InputStreamReader(new FileInputStream(folder
36 | + "/META-INF/container.xml"), "UTF-8"));
37 |
38 | String line;
39 | while ((line = br.readLine()) != null) {
40 | //if (line.indexOf(getS(R.string.full_path)) > -1)
41 | if (line.contains("full-path")) {
42 | int start = line.indexOf("full-path");
43 | //int start2 = line.indexOf("\"", start);
44 | int start2 = line.indexOf('\"', start);
45 | int stop2 = line.indexOf('\"', start2 + 1);
46 | if (start2 > -1 && stop2 > start2) {
47 | relativeOpfPath = line.substring(start2 + 1, stop2).trim();
48 | break;
49 | }
50 | }
51 | }
52 | br.close();
53 |
54 | // in case the OPF file is in the root directory
55 | if (!relativeOpfPath.contains("/")) {
56 | return folder;
57 | }
58 |
59 | // remove the OPF file name and the preceding '/'
60 | int last = relativeOpfPath.lastIndexOf('/');
61 | if (last > -1) {
62 | relativeOpfPath = relativeOpfPath.substring(0, last);
63 | }
64 |
65 | return new File(folder, relativeOpfPath);
66 | } catch (NullPointerException | IOException e) {
67 | e.printStackTrace();
68 | }
69 | return folder;
70 | }
71 |
72 | static Epub fromUri(Context context, String uri) throws IOException {
73 | File cacheDir = getEpubReaderCacheDir(context);
74 | File unzippedEpubLocation = Unzipper.unzipEpubIfNeeded(context, uri, cacheDir);
75 | try {
76 | Book book = UncompressedEpubReader.readUncompressedBook(unzippedEpubLocation);
77 | return new Epub(book, unzippedEpubLocation);
78 | } finally {
79 | System.gc();
80 | }
81 | }
82 |
83 | static Epub fromFolder(Context context, File folder) throws IOException {
84 | try {
85 | Book book = UncompressedEpubReader.readUncompressedBook(folder);
86 | return new Epub(book, folder);
87 | } finally {
88 | System.gc();
89 | }
90 | }
91 |
92 | static InputStream openFromUri(Context context, String uriString) throws IOException {
93 |
94 | Uri uri = Uri.parse(uriString);
95 | InputStream inputStream;
96 | if (uriString.startsWith(ANDROID_ASSETS)) {
97 | inputStream = context.getAssets().open(uriString.replace(ANDROID_ASSETS, ""));
98 | } else {
99 | inputStream = context.getContentResolver().openInputStream(uri);
100 | }
101 | return inputStream;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/vertical_content/VerticalContentBinderHelper.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.vertical_content
2 |
3 | import android.animation.Animator
4 | import android.view.View
5 | import android.widget.SeekBar
6 |
7 | import java.util.concurrent.TimeUnit
8 |
9 | import com.smartmobilefactory.epubreader.display.binding.ItemEpubVerticalContentBinding
10 | import com.smartmobilefactory.epubreader.utils.BaseDisposableObserver
11 | import io.reactivex.android.schedulers.AndroidSchedulers
12 | import io.reactivex.disposables.CompositeDisposable
13 | import io.reactivex.disposables.Disposable
14 |
15 | internal object VerticalContentBinderHelper {
16 |
17 | fun bind(binding: ItemEpubVerticalContentBinding): Disposable {
18 | val compositeDisposable = CompositeDisposable()
19 |
20 | compositeDisposable.add(bindSeekbar(binding))
21 | compositeDisposable.add(bindProgressBar(binding))
22 |
23 | return compositeDisposable
24 | }
25 |
26 | private fun bindProgressBar(binding: ItemEpubVerticalContentBinding): Disposable {
27 | return binding.webview.isReady()
28 | .doOnNext { isReady ->
29 | if (isReady) {
30 | binding.seekbar.visibility = View.INVISIBLE
31 | binding.progressBar.visibility = View.GONE
32 | } else {
33 | binding.seekbar.visibility = View.GONE
34 | binding.progressBar.visibility = View.VISIBLE
35 | }
36 | }.subscribeWith(BaseDisposableObserver())
37 | }
38 |
39 | private fun bindSeekbar(binding: ItemEpubVerticalContentBinding): Disposable {
40 | binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
41 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
42 | if (fromUser) {
43 | if (binding.webview.progress == 100) {
44 | binding.webview.scrollTo(0, progress)
45 | }
46 | }
47 | }
48 |
49 | override fun onStartTrackingTouch(seekBar: SeekBar) {
50 |
51 | }
52 |
53 | override fun onStopTrackingTouch(seekBar: SeekBar) {
54 |
55 | }
56 | })
57 |
58 | return binding.webview.verticalScrollState()
59 | .doOnDispose { binding.seekbar.setOnSeekBarChangeListener(null) }
60 | .doOnNext { scrollState ->
61 | if (binding.seekbar.visibility == View.INVISIBLE) {
62 | animateSeekbar(binding.seekbar, true)
63 | }
64 | binding.seekbar.max = scrollState.maxTop
65 | binding.seekbar.progress = scrollState.top
66 | }
67 | .debounce(3, TimeUnit.SECONDS)
68 | .observeOn(AndroidSchedulers.mainThread())
69 | .doOnNext {
70 | if (binding.seekbar.visibility == View.VISIBLE) {
71 | animateSeekbar(binding.seekbar, false)
72 | }
73 | }
74 | .subscribeWith(BaseDisposableObserver())
75 | }
76 |
77 | private fun animateSeekbar(seekbar: View, fadeIn: Boolean) {
78 | if (fadeIn) {
79 | seekbar.alpha = 0f
80 | seekbar.visibility = View.VISIBLE
81 | seekbar.animate().alpha(1f).setListener(null).start()
82 | } else {
83 | seekbar.animate().alpha(0f).setListener(object : Animator.AnimatorListener {
84 | override fun onAnimationStart(animation: Animator) {
85 |
86 | }
87 |
88 | override fun onAnimationEnd(animation: Animator) {
89 | seekbar.visibility = View.INVISIBLE
90 | }
91 |
92 | override fun onAnimationCancel(animation: Animator) {
93 |
94 | }
95 |
96 | override fun onAnimationRepeat(animation: Animator) {
97 |
98 | }
99 | })
100 | }
101 | }
102 |
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/assets/books/javascript/rangy/rangy-serializer.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Serializer module for Rangy.
3 | * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
4 | * cookie or local storage and restore it on the user's next visit to the same page.
5 | *
6 | * Part of Rangy, a cross-browser JavaScript range and selection library
7 | * https://github.com/timdown/rangy
8 | *
9 | * Depends on Rangy core.
10 | *
11 | * Copyright 2015, Tim Down
12 | * Licensed under the MIT license.
13 | * Version: 1.3.0
14 | * Build date: 10 May 2015
15 | */
16 | !function(e,n){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(n.rangy)}(function(e){return e.createModule("Serializer",["WrappedSelection"],function(e,n){function t(e){return e.replace(//g,">")}function o(e,n){n=n||[];var r=e.nodeType,i=e.childNodes,c=i.length,a=[r,e.nodeName,c].join(":"),d="",u="";switch(r){case 3:d=t(e.nodeValue);break;case 8:d="";break;default:d="<"+a+">",u=">"}d&&n.push(d);for(var s=0;c>s;++s)o(i[s],n);return u&&n.push(u),n}function r(e){var n=o(e).join("");return w(n).toString(16)}function i(e,n,t){var o=[],r=e;for(t=t||R.getDocument(e).documentElement;r&&r!=t;)o.push(R.getNodeIndex(r,!0)),r=r.parentNode;return o.join("/")+":"+n}function c(e,t,o){t||(t=(o||document).documentElement);for(var r,i=e.split(":"),c=t,a=i[0]?i[0].split("/"):[],d=a.length;d--;){if(r=parseInt(a[d],10),!(rc;++c)i[c]=a(r[c],t,o);return i.join("|")}function l(n,t,o){t?o=o||R.getWindow(t):(o=o||window,t=o.document.documentElement);for(var r=n.split("|"),i=e.getSelection(o),c=[],a=0,u=r.length;u>a;++a)c[a]=d(r[a],t,o.document);return i.setRanges(c),i}function f(e,n,t){var o;n?o=t?t.document:R.getDocument(n):(t=t||window,n=t.document.documentElement);for(var r=e.split("|"),i=0,c=r.length;c>i;++i)if(!u(r[i],n,o))return!1;return!0}function m(e){for(var n,t,o=e.split(/[;,]/),r=0,i=o.length;i>r;++r)if(n=o[r].split("="),n[0].replace(/^\s+/,"")==C&&(t=n[1]))return decodeURIComponent(t.replace(/\s+$/,""));return null}function p(e){e=e||window;var n=m(e.document.cookie);n&&l(n,e.doc)}function g(n,t){n=n||window,t="object"==typeof t?t:{};var o=t.expires?";expires="+t.expires.toUTCString():"",r=t.path?";path="+t.path:"",i=t.domain?";domain="+t.domain:"",c=t.secure?";secure":"",a=s(e.getSelection(n));n.document.cookie=encodeURIComponent(C)+"="+encodeURIComponent(a)+o+r+i+c}var h="undefined",v=e.util;(typeof encodeURIComponent==h||typeof decodeURIComponent==h)&&n.fail("encodeURIComponent and/or decodeURIComponent method is missing");var w=function(){function e(e){for(var n,t=[],o=0,r=e.length;r>o;++o)n=e.charCodeAt(o),128>n?t.push(n):2048>n?t.push(n>>6|192,63&n|128):t.push(n>>12|224,n>>6&63|128,63&n|128);return t}function n(){for(var e,n,t=[],o=0;256>o;++o){for(n=o,e=8;e--;)1==(1&n)?n=n>>>1^3988292384:n>>>=1;t[o]=n>>>0}return t}function t(){return o||(o=n()),o}var o=null;return function(n){for(var o,r=e(n),i=-1,c=t(),a=0,d=r.length;d>a;++a)o=255&(i^r[a]),i=i>>>8^c[o];return(-1^i)>>>0}}(),R=e.dom,S=/^([^,]+),([^,\{]+)(\{([^}]+)\})?$/,C="rangySerializedSelection";v.extend(e,{serializePosition:i,deserializePosition:c,serializeRange:a,deserializeRange:d,canDeserializeRange:u,serializeSelection:s,deserializeSelection:l,canDeserializeSelection:f,restoreSelectionFromCookie:p,saveSelectionCookie:g,getElementChecksum:r,nodeToInfoString:o}),v.crc32=w}),e},this);
--------------------------------------------------------------------------------
/library/src/main/assets/epubreaderandroid/helper_functions.js:
--------------------------------------------------------------------------------
1 | if (!document.caretRangeFromPoint) {
2 | // implementation when not supported in this webView version
3 | document.caretRangeFromPoint = function(x, y) {
4 | var log = "";
5 |
6 | function inRect(x, y, rect) {
7 | return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
8 | }
9 |
10 | function inObject(x, y, object) {
11 | var rects = object.getClientRects();
12 | for (var i = rects.length; i--;)
13 | if (inRect(x, y, rects[i]))
14 | return true;
15 | return false;
16 | }
17 |
18 | function getTextNodes(node, x, y) {
19 | if (!inObject(x, y, node))
20 | return [];
21 |
22 | var result = [];
23 | node = node.firstChild;
24 | while (node) {
25 | if (node.nodeType == 3)
26 | result.push(node);
27 | if (node.nodeType == 1)
28 | result = result.concat(getTextNodes(node, x, y));
29 |
30 | node = node.nextSibling;
31 | }
32 |
33 | return result;
34 | }
35 |
36 | var element = document.elementFromPoint(x, y);
37 | var nodes = getTextNodes(element, x, y);
38 | if (!nodes.length)
39 | return null;
40 | var node = nodes[0];
41 |
42 | var range = document.createRange();
43 | range.setStart(node, 0);
44 | range.setEnd(node, 1);
45 |
46 | for (var i = nodes.length; i--;) {
47 | var node = nodes[i],
48 | text = node.nodeValue;
49 |
50 |
51 | range = document.createRange();
52 | range.setStart(node, 0);
53 | range.setEnd(node, text.length);
54 |
55 | if (!inObject(x, y, range))
56 | continue;
57 |
58 | for (var j = text.length; j--;) {
59 | if (text.charCodeAt(j) <= 32)
60 | continue;
61 |
62 | range = document.createRange();
63 | range.setStart(node, j);
64 | range.setEnd(node, j + 1);
65 |
66 | if (inObject(x, y, range)) {
67 | range.setEnd(node, j);
68 | return range;
69 | }
70 | }
71 | }
72 |
73 | return range;
74 | };
75 | }
76 |
77 | function getFirstVisibleElement(y) {
78 | // TODO check if this works on all webview versions
79 | var range = document.caretRangeFromPoint(0, y);
80 | if (!range) {
81 | return null;
82 | }
83 | return element = range.startContainer.parentNode;
84 | }
85 |
86 | function getXPathTo(element) {
87 |
88 | if (element.id!=='') {
89 | return 'id("'+element.id+'")';
90 | }
91 |
92 | if (element===document.body) {
93 | return element.tagName;
94 | }
95 |
96 | var ix= 0;
97 | var siblings= element.parentNode.childNodes;
98 | for (var i= 0; i start) {
134 | console.log("range found");
135 |
136 | var element = node.parentElement.parentElement;
137 | while(element.children[0]) {
138 | element = element.children[0];
139 | }
140 | foundElement = element;
141 | return false;
142 | }
143 | counter = counter + len;
144 | return true;
145 | });
146 | return foundElement;
147 | }
148 |
--------------------------------------------------------------------------------
/app/src/main/assets/books/javascript/highlight.js:
--------------------------------------------------------------------------------
1 | var highlighter;
2 | var initialDoc;
3 |
4 | var highlighter;
5 |
6 | function init() {
7 | if (!highlighter) {
8 | rangy.init();
9 | highlighter = rangy.createHighlighter();
10 | }
11 |
12 | document.addEventListener("selectionchange", function(e) {
13 | bridge.onSelectionChanged(window.getSelection().toString().length);
14 | }, false);
15 |
16 | }
17 | init();
18 |
19 | /**
20 | * deserialize and format the highlight from database to usable once for rangy
21 | */
22 | function reloadHighlights() {
23 | var json = bridge.getHighlights(epubChapter.index);
24 | var data = JSON.parse(json);
25 |
26 | // remove all highlights before adding new once
27 | highlighter.removeAllHighlights();
28 |
29 | var serializedHighlights = ["type:textContent"]
30 | for (var i=0; i < data.length; i++){
31 | var highlight = data[i];
32 |
33 | if (highlight.chapterId != epubChapter.index) {
34 | continue;
35 | }
36 |
37 | var parts = [
38 | highlight.start,
39 | highlight.end,
40 | highlight.id,
41 | getColorClass(highlight.color),
42 | ''
43 | ];
44 |
45 | serializedHighlights.push( parts.join("$") );
46 | }
47 |
48 | var serializedData = serializedHighlights.join("|");
49 | highlighter.deserialize(serializedData);
50 | }
51 |
52 | /**
53 | * highlight the current selected text
54 | * @param color hex formatted rgb color
55 | */
56 | function highlightSelectedText(color) {
57 | init();
58 |
59 | var colorClass = getColorClass(color);
60 | var newHighlight = highlighter.highlightSelection(colorClass)[0];
61 |
62 | var node = newHighlight.getHighlightElements()[0];
63 |
64 | var x = 0;
65 | var y = 0;
66 | if (node && node.getBoundingClientRect) {
67 | var position = node.getBoundingClientRect();
68 | x = position.left;
69 | y = position.top;
70 | }
71 | var data = {
72 | x: x,
73 | y: y,
74 | highlights: getAllHighlightsSerialized()
75 | };
76 |
77 | bridge.onHighlightAdded(epubChapter.index, JSON.stringify(data));
78 | }
79 |
80 | function getAllHighlightsSerialized() {
81 |
82 | var serializedHighlights = [];
83 |
84 | var highlights = highlighter.highlights;
85 | for (i = 0; i < highlights.length; i++) {
86 | var highlight = highlights[i];
87 | serializedHighlights.push(serializeHighlight(highlight));
88 | }
89 |
90 | return serializedHighlights;
91 | }
92 |
93 | function serializeHighlight(highlight) {
94 | var color = "#" + highlight.classApplier.className.replace("highlight_", "");
95 |
96 | var serialized = {
97 | id : highlight.id,
98 | chapterId: epubChapter.index,
99 | start: highlight.characterRange.start,
100 | end: highlight.characterRange.end,
101 | text: highlight.getText(),
102 | color: color
103 | }
104 | return serialized;
105 | }
106 |
107 | function getColorClass(color) {
108 | var className = "highlight_" + color.replace("#", "");
109 |
110 | // check if class already exists
111 | for (var i=0; i < document.styleSheets.length; i++){
112 | var styleSheet = document.styleSheets[i];
113 | var rules = styleSheet.rules || styleSheet.cssRules;
114 | for(var x in rules) {
115 | if(rules[x].selectorText == className) {
116 | return className;
117 | }
118 | }
119 | }
120 |
121 | // class does not exists
122 |
123 | var style = document.createElement('style');
124 | style.type = 'text/css';
125 | style.innerHTML = '.' + className + ' { background-color: ' + color +'; }';
126 | document.getElementsByTagName('head')[0].appendChild(style);
127 |
128 | highlighter.addClassApplier(rangy.createClassApplier(className, {
129 | ignoreWhiteSpace: true,
130 | tagNames: ["span", "a"],
131 | elementProperties: {
132 | href: "#",
133 | onclick: function(event) {
134 | var highlight = highlighter.getHighlightForElement(this);
135 | var position = event.target.getBoundingClientRect();
136 | var data = {
137 | x: position.left,
138 | y: position.top,
139 | highlight: serializeHighlight(highlight)
140 | };
141 | bridge.onHighlightClicked(JSON.stringify(data));
142 | return false;
143 | }
144 | }
145 | }));
146 |
147 | return className;
148 | }
149 |
150 | reloadHighlights();
151 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/vertical_content/horizontal_chapters/PagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.vertical_content.horizontal_chapters
2 |
3 | import android.util.SparseArray
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import com.smartmobilefactory.epubreader.EpubView
8 | import com.smartmobilefactory.epubreader.display.binding.ItemEpubVerticalContentBinding
9 | import com.smartmobilefactory.epubreader.display.vertical_content.VerticalContentBinderHelper
10 | import com.smartmobilefactory.epubreader.display.view.BaseViewPagerAdapter
11 | import com.smartmobilefactory.epubreader.display.view.EpubWebView
12 | import com.smartmobilefactory.epubreader.display.view.InternalEpubBridge
13 | import com.smartmobilefactory.epubreader.model.Epub
14 | import com.smartmobilefactory.epubreader.model.EpubLocation
15 | import com.smartmobilefactory.epubreader.utils.BaseDisposableObserver
16 | import io.reactivex.disposables.CompositeDisposable
17 |
18 | internal class PagerAdapter(private val strategy: HorizontalWithVerticalContentEpubDisplayStrategy, private val epubView: EpubView) : BaseViewPagerAdapter() {
19 |
20 | private val chapterDisposables = SparseArray()
21 | private val chapterLocations = SparseArray()
22 |
23 | private var epub: Epub? = null
24 | private var location: EpubLocation? = null
25 |
26 | val attachedViewBindings: List
27 | get() = attachedViews.map { ItemEpubVerticalContentBinding.bind(it) }
28 |
29 | fun displayEpub(epub: Epub, location: EpubLocation) {
30 | this.epub = epub
31 | this.location = location
32 | notifyDataSetChanged()
33 | }
34 |
35 | override fun getView(position: Int, parent: ViewGroup): View {
36 |
37 | val compositeDisposable = chapterDisposables.get(position) ?: run {
38 | chapterDisposables.append(position, CompositeDisposable())
39 | chapterDisposables.get(position)
40 | }
41 |
42 | val binding = ItemEpubVerticalContentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
43 |
44 | val epub = epub ?: return binding.root
45 |
46 | binding.webview.setUrlInterceptor { strategy.urlInterceptor(it) }
47 |
48 | val spineReference = epub.book.spine.spineReferences[position]
49 | binding.webview.loadEpubPage(epub, spineReference, epubView.internalSettings)
50 |
51 | handleLocation(position, binding.webview)
52 |
53 | binding.webview.bindToSettings(epubView.internalSettings)
54 |
55 | val bridge = InternalEpubBridge()
56 | binding.webview.setInternalBridge(bridge)
57 |
58 | bridge.xPath()
59 | .doOnNext { xPath ->
60 | val location = EpubLocation.fromXPath(strategy.currentChapter, xPath)
61 | chapterLocations.append(position, location)
62 | strategy.currentLocation = location
63 | }
64 | .subscribeWith(BaseDisposableObserver())
65 | .addTo(compositeDisposable)
66 |
67 | compositeDisposable.add(VerticalContentBinderHelper.bind(binding))
68 |
69 | return binding.root
70 | }
71 |
72 | fun getChapterLocation(position: Int): EpubLocation? {
73 | return chapterLocations.get(position)
74 | }
75 |
76 | fun getViewBindingIfAttached(position: Int): ItemEpubVerticalContentBinding? {
77 | val view = getViewIfAttached(position) ?: return null
78 | return ItemEpubVerticalContentBinding.bind(view)
79 | }
80 |
81 | private fun handleLocation(position: Int, webView: EpubWebView) {
82 |
83 | if (location == null) {
84 | return
85 | }
86 |
87 | val chapter: Int = if (location is EpubLocation.ChapterLocation) {
88 | (location as EpubLocation.ChapterLocation).chapter()
89 | } else {
90 | 0
91 | }
92 |
93 | if (position == chapter) {
94 | location?.let { webView.gotoLocation(it) }
95 | location = null
96 | }
97 |
98 | }
99 |
100 | override fun onItemDestroyed(position: Int, view: View) {
101 | super.onItemDestroyed(position, view)
102 | chapterLocations.delete(position)
103 | val compositeDisposable = chapterDisposables.get(position)
104 | compositeDisposable?.clear()
105 | }
106 |
107 | override fun getCount(): Int {
108 | return epub?.book?.spine?.spineReferences?.size ?: 0
109 | }
110 |
111 | override fun getItemPosition(any: Any?): Int {
112 | return POSITION_NONE
113 | }
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/app/src/main/assets/fonts/Diplomata_SC/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011, Eduardo Tunni (http://www.tipo.net.ar),
2 | with Reserved Font Name "Diplomata"
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/scripts/stetho_open.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | ###############################################################################
3 | ##
4 | ## Simple utility class to create a forwarded socket connection to an
5 | ## application's stetho domain socket.
6 | ##
7 | ## Usage:
8 | ##
9 | ## sock = stetho_open(
10 | ## device='',
11 | ## process='com.facebook.stetho.sample')
12 | ## doHttp(sock)
13 | ##
14 | ###############################################################################
15 |
16 | import socket
17 | import struct
18 | import re
19 |
20 | def stetho_open(device=None, process=None):
21 | adb = _connect_to_device(device)
22 |
23 | socket_name = None
24 | if process is None:
25 | socket_name = _find_only_stetho_socket(device)
26 | else:
27 | socket_name = _format_process_as_stetho_socket(process)
28 |
29 | try:
30 | adb.select_service('localabstract:%s' % (socket_name))
31 | except SelectServiceError as e:
32 | raise HumanReadableError(
33 | 'Failure to target process %s: %s (is it running?)' % (
34 | process, e.reason))
35 |
36 | return adb.sock
37 |
38 | def read_input(sock, n, tag):
39 | data = b'';
40 | while len(data) < n:
41 | incoming_data = sock.recv(n - len(data))
42 | if len(incoming_data) == 0:
43 | break
44 | data += incoming_data
45 | if len(data) != n:
46 | raise IOError('Unexpected end of stream while reading %s.' % tag)
47 | return data
48 |
49 | def _find_only_stetho_socket(device):
50 | adb = _connect_to_device(device)
51 | try:
52 | adb.select_service('shell:cat /proc/net/unix')
53 | last_stetho_socket_name = None
54 | process_names = []
55 | for line in adb.sock.makefile():
56 | row = line.rstrip().split(' ')
57 | if len(row) < 8:
58 | continue
59 | socket_name = row[7]
60 | if not socket_name.startswith('@stetho_'):
61 | continue
62 | # Filter out entries that are not server sockets
63 | if int(row[3], 16) != 0x10000 or int(row[5]) != 1:
64 | continue
65 | last_stetho_socket_name = socket_name[1:]
66 | process_names.append(
67 | _parse_process_from_stetho_socket(socket_name))
68 | if len(process_names) > 1:
69 | raise HumanReadableError(
70 | 'Multiple stetho-enabled processes available:%s\n' % (
71 | '\n\t'.join([''] + list(set(process_names)))) +
72 | 'Use -p or the environment variable STETHO_PROCESS to ' +
73 | 'select one')
74 | elif last_stetho_socket_name == None:
75 | raise HumanReadableError('No stetho-enabled processes running')
76 | else:
77 | return last_stetho_socket_name
78 | finally:
79 | adb.sock.close()
80 |
81 | def _connect_to_device(device=None):
82 | adb = AdbSmartSocketClient()
83 | adb.connect()
84 |
85 | try:
86 | if device is None:
87 | adb.select_service('host:transport-any')
88 | else:
89 | adb.select_service('host:transport:%s' % (device))
90 |
91 | return adb
92 | except SelectServiceError as e:
93 | raise HumanReadableError(
94 | 'Failure to target device %s: %s' % (device, e.reason))
95 |
96 | def _parse_process_from_stetho_socket(socket_name):
97 | m = re.match("^\@stetho_(.+)_devtools_remote$", socket_name)
98 | if m is None:
99 | raise Exception('Unexpected Stetho socket formatting: %s' % (socket_name))
100 | return m.group(1)
101 |
102 | def _format_process_as_stetho_socket(process):
103 | return 'stetho_%s_devtools_remote' % (process)
104 |
105 | class AdbSmartSocketClient(object):
106 | """Implements the smartsockets system defined by:
107 | https://android.googlesource.com/platform/system/core/+/master/adb/protocol.txt
108 | """
109 |
110 | def __init__(self):
111 | pass
112 |
113 | def connect(self, port=5037):
114 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
115 | sock.connect(('127.0.0.1', port))
116 | self.sock = sock
117 |
118 | def select_service(self, service):
119 | message = '%04x%s' % (len(service), service)
120 | self.sock.send(message.encode('ascii'))
121 | status = read_input(self.sock, 4, "status")
122 | if status == b'OKAY':
123 | # All good...
124 | pass
125 | elif status == b'FAIL':
126 | reason_len = int(read_input(self.sock, 4, "fail reason"), 16)
127 | reason = read_input(self.sock, reason_len, "fail reason lean").decode('ascii')
128 | raise SelectServiceError(reason)
129 | else:
130 | raise Exception('Unrecognized status=%s' % (status))
131 |
132 | class SelectServiceError(Exception):
133 | def __init__(self, reason):
134 | self.reason = reason
135 |
136 | def __str__(self):
137 | return repr(self.reason)
138 |
139 | class HumanReadableError(Exception):
140 | def __init__(self, reason):
141 | self.reason = reason
142 |
143 | def __str__(self):
144 | return self.reason
145 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/vertical_content/vertical_chapters/VerticalWithVerticalContentEpubDisplayStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.vertical_content.vertical_chapters
2 |
3 | import android.support.v7.widget.LinearLayoutManager
4 | import android.support.v7.widget.RecyclerView
5 | import android.util.Pair
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 |
10 | import com.smartmobilefactory.epubreader.EpubView
11 | import com.smartmobilefactory.epubreader.UrlInterceptor
12 | import com.smartmobilefactory.epubreader.display.EpubDisplayStrategy
13 | import com.smartmobilefactory.epubreader.display.binding.EpubVerticalVerticalContentBinding
14 | import com.smartmobilefactory.epubreader.display.view.EpubWebView
15 | import com.smartmobilefactory.epubreader.model.Epub
16 | import com.smartmobilefactory.epubreader.model.EpubLocation
17 | import com.smartmobilefactory.epubreader.utils.BaseDisposableObserver
18 |
19 | import java.util.concurrent.TimeUnit
20 |
21 | import io.reactivex.android.schedulers.AndroidSchedulers
22 | import io.reactivex.subjects.PublishSubject
23 |
24 | internal class VerticalWithVerticalContentEpubDisplayStrategy : EpubDisplayStrategy() {
25 |
26 | private lateinit var binding: EpubVerticalVerticalContentBinding
27 |
28 | private lateinit var epubView: EpubView
29 |
30 | private lateinit var chapterAdapter: ChapterAdapter
31 |
32 | private val scrollPosition = PublishSubject.create>()
33 |
34 | internal val urlInterceptor: (String) -> Boolean = { url: String -> epubView.shouldOverrideUrlLoading(url) }
35 |
36 | override fun bind(epubView: EpubView, parent: ViewGroup) {
37 | this.epubView = epubView
38 | val inflater = LayoutInflater.from(parent.context)
39 | binding = EpubVerticalVerticalContentBinding.inflate(inflater, parent, true)
40 |
41 | val layoutManager = LinearLayoutManager(epubView.context)
42 | layoutManager.setInitialPrefetchItemCount(2)
43 | binding.recyclerview.layoutManager = layoutManager
44 | chapterAdapter = ChapterAdapter(this, epubView)
45 | binding.recyclerview.adapter = chapterAdapter
46 | binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
47 | override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
48 | super.onScrolled(recyclerView, dx, dy)
49 | val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
50 | val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
51 | scrollPosition.onNext(Pair(firstVisibleItemPosition, firstVisibleView.top))
52 | currentChapter = firstVisibleItemPosition
53 | }
54 | })
55 |
56 | scrollPosition
57 | .sample(200, TimeUnit.MILLISECONDS)
58 | .observeOn(AndroidSchedulers.mainThread())
59 | .doOnNext { positionTopPair ->
60 | val holder = binding.recyclerview.findViewHolderForAdapterPosition(positionTopPair.first) as? ChapterAdapter.ChapterViewHolder
61 | if (holder != null) {
62 | val density = epubView.context.resources.displayMetrics.density
63 | holder.binding.webview.js.updateFirstVisibleElementByTopPosition(-positionTopPair.second / density)
64 | }
65 | }
66 | .subscribe(BaseDisposableObserver())
67 | }
68 |
69 | override fun displayEpub(epub: Epub, location: EpubLocation) {
70 | chapterAdapter.displayEpub(epub, location)
71 |
72 | if (location is EpubLocation.ChapterLocation) {
73 | binding.recyclerview.scrollToPosition(location.chapter())
74 | }
75 | }
76 |
77 | override fun gotoLocation(location: EpubLocation) {
78 | if (location is EpubLocation.ChapterLocation) {
79 |
80 | this.binding.recyclerview.scrollToPosition(location.chapter())
81 | displayEpub(epubView.getEpub()!!, location)
82 | currentLocation = location
83 | }
84 | }
85 |
86 | override fun callChapterJavascriptMethod(chapter: Int, name: String, vararg args: Any) {
87 | if (chapterAdapter == null) {
88 | return
89 | }
90 | val holder = binding.recyclerview.findViewHolderForAdapterPosition(chapter) as ChapterAdapter.ChapterViewHolder
91 | holder.binding.webview.callJavascriptMethod(name, *args)
92 | }
93 |
94 | override fun callChapterJavascriptMethod(name: String, vararg args: Any) {
95 | callChapterJavascriptMethod(currentChapter, name, *args)
96 | }
97 |
98 | internal fun scrollTo(location: EpubLocation, chapter: Int, offsetY: Int) {
99 | binding.recyclerview.post {
100 | val linearLayoutManager = binding.recyclerview.layoutManager as LinearLayoutManager
101 | linearLayoutManager.scrollToPositionWithOffset(chapter, -offsetY)
102 | currentLocation = location
103 | }
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/vertical_content/vertical_chapters/ChapterAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.vertical_content.vertical_chapters
2 |
3 | import android.support.annotation.Keep
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.ViewGroup
7 | import android.webkit.JavascriptInterface
8 | import com.smartmobilefactory.epubreader.EpubView
9 | import com.smartmobilefactory.epubreader.display.binding.ItemVerticalVerticalContentBinding
10 | import com.smartmobilefactory.epubreader.display.view.EpubWebView
11 | import com.smartmobilefactory.epubreader.display.view.InternalEpubBridge
12 | import com.smartmobilefactory.epubreader.model.Epub
13 | import com.smartmobilefactory.epubreader.model.EpubLocation
14 | import com.smartmobilefactory.epubreader.utils.BaseDisposableObserver
15 | import io.reactivex.android.schedulers.AndroidSchedulers
16 | import io.reactivex.disposables.CompositeDisposable
17 | import java.util.concurrent.TimeUnit
18 |
19 | internal class ChapterAdapter(private val strategy: VerticalWithVerticalContentEpubDisplayStrategy, private val epubView: EpubView) : RecyclerView.Adapter() {
20 |
21 | private var epub: Epub? = null
22 |
23 | private var locationChapter: Int = 0
24 | private var tempLocation: EpubLocation? = null
25 | private var location: EpubLocation? = null
26 |
27 | fun displayEpub(epub: Epub, location: EpubLocation) {
28 | this.epub = epub
29 | this.location = location
30 | notifyDataSetChanged()
31 | }
32 |
33 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChapterViewHolder {
34 | val holder = ChapterViewHolder.create(parent)
35 | holder.binding.webview.bindToSettings(epubView.internalSettings)
36 | return holder
37 | }
38 |
39 | override fun onBindViewHolder(holder: ChapterViewHolder, position: Int) {
40 |
41 | val epub = epub ?: return
42 |
43 | val compositeDisposable = CompositeDisposable()
44 |
45 | holder.binding.webview.loadUrl(BLANK_URL)
46 | holder.binding.webview.setUrlInterceptor { url -> strategy.urlInterceptor(url) }
47 |
48 | val spineReference = epub.book.spine.spineReferences[position]
49 | holder.binding.webview.loadEpubPage(epub, spineReference, epubView.internalSettings)
50 |
51 | val bridge = Bridge()
52 | holder.binding.webview.setInternalBridge(bridge)
53 |
54 | handleLocation(position, holder.binding.webview)
55 |
56 | bridge.xPath()
57 | .doOnNext { xPath ->
58 | val location = EpubLocation.fromXPath(strategy.currentChapter, xPath)
59 | strategy.currentLocation = location
60 | }
61 | .subscribeWith(BaseDisposableObserver())
62 | .addTo(compositeDisposable)
63 | }
64 |
65 |
66 | private fun handleLocation(position: Int, webView: EpubWebView) {
67 |
68 | if (location == null) {
69 | return
70 | }
71 |
72 | val chapter: Int
73 | if (location is EpubLocation.ChapterLocation) {
74 | chapter = (location as EpubLocation.ChapterLocation).chapter()
75 | } else {
76 | chapter = 0
77 | }
78 |
79 | if (position == chapter) {
80 | locationChapter = chapter
81 | tempLocation = location
82 | location = null
83 | webView.isReady()
84 | .filter { isReady -> isReady && webView.url != BLANK_URL }
85 | .take(1)
86 | .delay(1000, TimeUnit.MILLISECONDS)
87 | .observeOn(AndroidSchedulers.mainThread())
88 | .doOnNext {
89 | when (tempLocation) {
90 | is EpubLocation.IdLocation -> webView.js.getYPositionOfElementWithId((tempLocation as EpubLocation.IdLocation).id())
91 | is EpubLocation.XPathLocation -> webView.js.getYPositionOfElementWithXPath((tempLocation as EpubLocation.XPathLocation).xPath())
92 | is EpubLocation.RangeLocation -> webView.js.getYPositionOfElementFromRangeStart((tempLocation as EpubLocation.RangeLocation).start())
93 | }
94 | }
95 | .subscribe(BaseDisposableObserver())
96 | }
97 |
98 | }
99 |
100 | override fun getItemCount(): Int {
101 | return epub?.book?.spine?.spineReferences?.size ?: 0
102 | }
103 |
104 | private inner class Bridge : InternalEpubBridge() {
105 |
106 | @Keep
107 | @JavascriptInterface
108 | fun resultGetYPositionOfElement(top: Int) {
109 | val tempLocation = tempLocation ?: return
110 | val density = epubView.context.resources.displayMetrics.density
111 | strategy.scrollTo(tempLocation, locationChapter, (top * density).toInt())
112 | }
113 |
114 | }
115 |
116 | internal class ChapterViewHolder(var binding: ItemVerticalVerticalContentBinding) : RecyclerView.ViewHolder(binding.root) {
117 | companion object {
118 |
119 | fun create(parent: ViewGroup): ChapterViewHolder {
120 | return ChapterViewHolder(ItemVerticalVerticalContentBinding.inflate(LayoutInflater.from(parent.context), parent, false))
121 | }
122 | }
123 | }
124 |
125 | companion object {
126 |
127 | private val BLANK_URL = "about:blank"
128 | }
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/model/Unzipper.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.model;
2 |
3 | import android.content.Context;
4 |
5 | import java.io.Closeable;
6 | import java.io.File;
7 | import java.io.FileOutputStream;
8 | import java.io.IOException;
9 | import java.io.InputStream;
10 | import java.io.OutputStream;
11 | import java.security.MessageDigest;
12 | import java.security.NoSuchAlgorithmException;
13 | import java.util.zip.ZipEntry;
14 | import java.util.zip.ZipInputStream;
15 |
16 | class Unzipper {
17 |
18 | static File getEpubCacheFolder(File destDir, String uri) {
19 | return new File(destDir, md5(uri));
20 | }
21 |
22 | /**
23 | * @return (unzipped location, epubfile)
24 | * @throws IOException
25 | */
26 | static File unzipEpubIfNeeded(Context context, String uri, File destDir) throws IOException {
27 | InputStream inputStream = EpubStorageHelper.openFromUri(context, uri);
28 | File destination = getEpubCacheFolder(destDir, uri);
29 |
30 | if (destination.exists()) {
31 | File ready = new File(destination, ".ready");
32 | if (ready.exists()) {
33 | return destination;
34 | }
35 | }
36 |
37 | try {
38 | unzip(inputStream, destination);
39 | } catch (IOException e) {
40 | //noinspection ResultOfMethodCallIgnored
41 | try {
42 | FileUtils.deleteDirectory(destination);
43 | } catch (Exception ignore) {}
44 |
45 | throw e;
46 | }
47 |
48 | return destination;
49 | }
50 |
51 | private static File copyAsset(Context context, String uri, File destDir) throws IOException {
52 | File file = new File(uri);
53 | File localFile = new File(destDir, file.getName());
54 | if (localFile.exists()) {
55 | return localFile;
56 | }
57 | localFile.createNewFile();
58 | try {
59 | InputStream steam = context.getAssets().open(uri.replace("file:///android_asset/", ""));
60 | copyFile(steam, new FileOutputStream(localFile));
61 | return localFile;
62 | } catch (IOException e) {
63 | e.printStackTrace();
64 | localFile.delete();
65 | }
66 | return file;
67 | }
68 |
69 | private static void copyFile(InputStream in, OutputStream out) throws IOException {
70 | byte[] buffer = new byte[1024];
71 | int read;
72 | while ((read = in.read(buffer)) != -1) {
73 | out.write(buffer, 0, read);
74 | }
75 | out.close();
76 | }
77 |
78 | private static void unzip(InputStream inputStream, File folder) throws IOException {
79 |
80 | if (!folder.exists()) {
81 | folder.mkdirs();
82 | } else {
83 | FileUtils.deleteDirectory(folder);
84 | }
85 |
86 | ZipInputStream zis;
87 |
88 | byte[] buffer = new byte[2048];
89 |
90 | try {
91 | String filename;
92 | zis = new ZipInputStream(inputStream);
93 |
94 | ZipEntry ze;
95 | int count;
96 | while ((ze = zis.getNextEntry()) != null) {
97 | filename = ze.getName();
98 | File file = new File(folder, filename);
99 |
100 | // make directory if necessary
101 | new File(file.getParent()).mkdirs();
102 |
103 | if (!ze.isDirectory() && !file.isDirectory()) {
104 | FileOutputStream fout = new FileOutputStream(file);
105 | while ((count = zis.read(buffer)) != -1) {
106 | fout.write(buffer, 0, count);
107 | }
108 | fout.close();
109 | }
110 | zis.closeEntry();
111 | }
112 |
113 | inputStream.close();
114 | zis.close();
115 |
116 | //file to show that everything is fully unzipped
117 | File ready = new File(folder, ".ready");
118 | if (!ready.exists()) {
119 | ready.createNewFile();
120 | }
121 |
122 | } catch (IOException e) {
123 | FileUtils.tryDeleteDirectory(folder);
124 | throw e;
125 | }
126 | }
127 |
128 | private static void createDir(File dir) {
129 | if (dir.exists()) {
130 | return;
131 | }
132 | if (!dir.mkdirs()) {
133 | throw new RuntimeException("Can not create dir " + dir);
134 | }
135 | }
136 |
137 | private static String md5(final String s) {
138 | final String MD5 = "MD5";
139 | try {
140 | // Create MD5 Hash
141 | MessageDigest digest = java.security.MessageDigest
142 | .getInstance(MD5);
143 | digest.update(s.getBytes());
144 | byte messageDigest[] = digest.digest();
145 |
146 | // Create Hex String
147 | StringBuilder hexString = new StringBuilder();
148 | for (byte aMessageDigest : messageDigest) {
149 | String h = Integer.toHexString(0xFF & aMessageDigest);
150 | while (h.length() < 2)
151 | h = "0" + h;
152 | hexString.append(h);
153 | }
154 | return hexString.toString();
155 |
156 | } catch (NoSuchAlgorithmException e) {
157 | e.printStackTrace();
158 | }
159 | return "";
160 | }
161 |
162 | private static void closeSilent(Closeable closeable) {
163 | try {
164 | closeable.close();
165 | } catch (Exception e) {
166 | // ignore
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/EpubDisplayHelper.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display
2 |
3 | import android.net.Uri
4 | import android.os.Build
5 | import android.support.annotation.CheckResult
6 | import android.webkit.WebView
7 | import com.smartmobilefactory.epubreader.InternalEpubViewSettings
8 | import com.smartmobilefactory.epubreader.model.Epub
9 | import io.reactivex.Completable
10 | import io.reactivex.Single
11 | import io.reactivex.android.schedulers.AndroidSchedulers
12 | import io.reactivex.schedulers.Schedulers
13 | import nl.siegmann.epublib.domain.SpineReference
14 | import nl.siegmann.epublib.util.IOUtil
15 | import java.io.IOException
16 | import java.lang.ref.WeakReference
17 | import java.util.*
18 |
19 | internal object EpubDisplayHelper {
20 |
21 | private val INJECT_CSS_FORMAT = "\n"
22 | private val INJECT_JAVASCRIPT_FORMAT = "\n"
23 |
24 | @CheckResult
25 | fun loadHtmlData(webView: WebView, epub: Epub, spineReference: SpineReference, settings: InternalEpubViewSettings): Completable {
26 |
27 | val webViewWeakReference = WeakReference(webView)
28 |
29 | return Single.fromCallable { EpubDisplayHelper.getHtml(epub, spineReference, settings) }
30 | .doOnSubscribe { webViewWeakReference.get()?.tag = spineReference }
31 | .subscribeOn(Schedulers.io())
32 | .observeOn(AndroidSchedulers.mainThread())
33 | .doOnSuccess { html ->
34 | val webViewReference = webViewWeakReference.get() ?: return@doOnSuccess
35 |
36 | var isAttachedToWindow = true
37 | if (Build.VERSION.SDK_INT >= 19) {
38 | isAttachedToWindow = webViewReference.isAttachedToWindow
39 | }
40 | if (webViewReference.tag === spineReference && isAttachedToWindow) {
41 | val basePath = Uri.fromFile(epub.opfPath).toString() + "//"
42 | webViewReference.loadDataWithBaseURL(
43 | basePath,
44 | html,
45 | "text/html",
46 | "UTF-8",
47 | "about:chapter"
48 | )
49 | }
50 | }
51 | .toCompletable()
52 | }
53 |
54 | @Throws(IOException::class)
55 | private fun getHtml(epub: Epub, reference: SpineReference, settings: InternalEpubViewSettings): String {
56 |
57 | val inputStream = epub.getResourceContent(reference.resource)
58 |
59 | var rawHtml = inputStream.use {
60 | String(IOUtil.toByteArray(inputStream), Charsets.UTF_8)
61 | }
62 |
63 | val injectBeforeBody = (buildLibraryInternalInjections()
64 | + injectJavascriptConstants(epub, reference)
65 | + buildCustomCssString(settings)
66 | + injectJavascriptStartCode(settings))
67 |
68 | val injectAfterBody = buildCustomScripsString(settings)
69 |
70 | // add custom scripts at the end of the body
71 | // this makes sure the dom tree is already present when the scripts are executed
72 | rawHtml = rawHtml.replaceFirst("".toRegex(), "" + injectBeforeBody)
73 | rawHtml = rawHtml.replaceFirst("".toRegex(), injectAfterBody + "")
74 |
75 | return rawHtml
76 | }
77 |
78 | private fun injectJavascriptConstants(epub: Epub, reference: SpineReference): String {
79 | val chapterPosition = getChapterPosition(epub, reference)
80 |
81 | return ("\n")
89 | }
90 |
91 | private fun injectJavascriptStartCode(settings: InternalEpubViewSettings): String {
92 |
93 | val builder = StringBuilder()
94 | builder.append("\n")
111 |
112 | return builder.toString()
113 | }
114 |
115 | private fun getChapterPosition(epub: Epub, reference: SpineReference): Int {
116 | val spineReferences = epub.book.spine.spineReferences
117 | for (i in spineReferences.indices) {
118 | val spineReference = spineReferences[i]
119 |
120 | if (spineReference.resourceId === reference.resourceId) {
121 | return i
122 | }
123 | if (spineReference.resourceId == reference.resourceId) {
124 | return i
125 | }
126 | }
127 | return 0
128 | }
129 |
130 | private fun buildLibraryInternalInjections(): String {
131 | return String.format(Locale.US, INJECT_JAVASCRIPT_FORMAT, "file:///android_asset/epubreaderandroid/helper_functions.js") +
132 | String.format(Locale.US, INJECT_JAVASCRIPT_FORMAT, "file:///android_asset/epubreaderandroid/script.js") +
133 | String.format(Locale.US, INJECT_CSS_FORMAT, "file:///android_asset/epubreaderandroid/style.css")
134 | }
135 |
136 | private fun buildCustomCssString(settings: InternalEpubViewSettings): String {
137 | val builder = StringBuilder()
138 | for (script in settings.customChapterCss) {
139 | builder.append(String.format(Locale.US, INJECT_CSS_FORMAT, script))
140 | }
141 | return builder.toString()
142 | }
143 |
144 | private fun buildCustomScripsString(settings: InternalEpubViewSettings): String {
145 | val builder = StringBuilder()
146 |
147 | for (script in settings.customChapterScripts) {
148 | builder.append(String.format(Locale.US, INJECT_JAVASCRIPT_FORMAT, script))
149 | }
150 | return builder.toString()
151 | }
152 |
153 | }
154 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/model/Epub.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.model;
2 |
3 | import android.content.Context;
4 | import android.support.annotation.WorkerThread;
5 | import android.support.v4.util.Pair;
6 |
7 | import com.smartmobilefactory.epubreader.EpubView;
8 |
9 | import java.io.File;
10 | import java.io.FileInputStream;
11 | import java.io.FileNotFoundException;
12 | import java.io.IOException;
13 | import java.io.InputStream;
14 | import java.util.List;
15 | import java.util.zip.ZipInputStream;
16 |
17 | import nl.siegmann.epublib.domain.Book;
18 | import nl.siegmann.epublib.domain.Resource;
19 | import nl.siegmann.epublib.domain.SpineReference;
20 | import nl.siegmann.epublib.domain.TOCReference;
21 | import nl.siegmann.epublib.epub.EpubReader;
22 |
23 | public class Epub {
24 |
25 | private File opfPath = null;
26 | private final File location;
27 | private final Book book;
28 |
29 | /**
30 | * create a new instance by calling {@link #fromUri(Context, String)}
31 | */
32 | Epub(Book book, File location) {
33 | this.location = location;
34 | this.book = book;
35 | }
36 |
37 | public Book getBook() {
38 | return book;
39 | }
40 |
41 | /**
42 | * @return -1 if toc position not found
43 | */
44 | public int getSpinePositionForTocReference(TOCReference tocReference) {
45 | List spineReferences = getBook().getSpine().getSpineReferences();
46 | for (int i = 0; i < spineReferences.size(); i++) {
47 | SpineReference spineReference = spineReferences.get(i);
48 | if (tocReference.getResourceId().equals(spineReference.getResourceId())) {
49 | return i;
50 | }
51 | }
52 | return -1;
53 | }
54 |
55 | /**
56 | * @return -1 if spine position not found
57 | */
58 | public int getTocPositionForSpineReference(SpineReference spineReference) {
59 | List spineReferences = getBook().getSpine().getSpineReferences();
60 | for (int i = 0; i < spineReferences.size(); i++) {
61 | SpineReference spineReference2 = spineReferences.get(i);
62 | if (spineReference2.getResourceId().equals(spineReference.getResourceId())) {
63 | return getTocPositionForSpinePosition(i);
64 | }
65 | }
66 | return -1;
67 | }
68 |
69 | /**
70 | * @return -1 if spine position not found
71 | */
72 | public int getTocPositionForSpinePosition(int spinePosition) {
73 | List tocReferences = getBook().getTableOfContents().getTocReferences();
74 | for (int i = 0; i < tocReferences.size(); i++) {
75 | TOCReference tocReference = tocReferences.get(i);
76 | int spinePositionForTocReference = getSpinePositionForTocReference(tocReference);
77 | if (spinePositionForTocReference == spinePosition) {
78 | return i;
79 | }
80 | if (spinePositionForTocReference > spinePosition) {
81 | return i - 1;
82 | }
83 | }
84 | return -1;
85 | }
86 |
87 | public File getOpfPath() {
88 | if (opfPath != null) {
89 | return opfPath;
90 | }
91 | opfPath = EpubStorageHelper.getOpfPath(this);
92 | return opfPath;
93 | }
94 |
95 | public File getLocation() {
96 | return location;
97 | }
98 |
99 | /**
100 | * returns the file input stream of a resources located in an epub
101 | * in difference to {@link Resource#getData()} the stream is not cached in memory
102 | */
103 | public InputStream getResourceContent(Resource resource) throws FileNotFoundException {
104 | File file = new File(getOpfPath(), resource.getHref());
105 | return new FileInputStream(file);
106 | }
107 |
108 | /**
109 | * Accepts the following URI schemes:
110 | *
111 | * - content
112 | * - android.resource
113 | * - file
114 | * - file/android_assets
115 | *
116 | *
117 | * @param context
118 | * @param uri
119 | * @throws IOException
120 | */
121 | @WorkerThread
122 | public static Epub fromUri(Context context, String uri) throws IOException {
123 | return EpubStorageHelper.fromUri(context, uri);
124 | }
125 |
126 | /**
127 | * @param context
128 | * @param folder uncompressed epub directory
129 | * @throws IOException
130 | */
131 | @WorkerThread
132 | public static Epub fromFolder(Context context, File folder) throws IOException {
133 | return EpubStorageHelper.fromFolder(context, folder);
134 | }
135 |
136 | /**
137 | * removes all cached extracted data for this epub
138 | * the original epub file will not be removed
139 | *
140 | * !!! it is not usable after calling this function !!!
141 | * make sure the epub is not currently displayed in any {@link EpubView}
142 | *
143 | * you need to recreate the epub with {@link #fromUri(Context, String)} again
144 | */
145 | @WorkerThread
146 | public void destroy() throws IOException {
147 | FileUtils.deleteDirectory(getLocation());
148 | }
149 |
150 |
151 | /**
152 | * @see #destroy()
153 | * destroys the cache for an epub without the need of creating an {@link Epub} instance before
154 | *
155 | * @param uri epub uri
156 | */
157 | @WorkerThread
158 | public static void destroyForUri(Context context, String uri) throws IOException {
159 | FileUtils.deleteDirectory(Unzipper.getEpubCacheFolder(EpubStorageHelper.getEpubReaderCacheDir(context), uri));
160 | }
161 |
162 | /**
163 | * checks if this epub is destroyed
164 | */
165 | public boolean isDestroyed() {
166 | return !getLocation().exists();
167 | }
168 |
169 | @Override
170 | public boolean equals(Object o) {
171 | if (this == o) return true;
172 | if (!(o instanceof Epub)) return false;
173 |
174 | Epub epub = (Epub) o;
175 |
176 | if (opfPath != null ? !opfPath.equals(epub.opfPath) : epub.opfPath != null) {
177 | return false;
178 | }
179 | if (location != null ? !location.equals(epub.location) : epub.location != null) {
180 | return false;
181 | }
182 | return book != null ? book.equals(epub.book) : epub.book == null;
183 |
184 | }
185 |
186 | @Override
187 | public int hashCode() {
188 | int result = opfPath != null ? opfPath.hashCode() : 0;
189 | result = 31 * result + (location != null ? location.hashCode() : 0);
190 | result = 31 * result + (book != null ? book.hashCode() : 0);
191 | return result;
192 | }
193 |
194 | }
195 |
--------------------------------------------------------------------------------
/app/src/main/java/com/smartmobilefactory/epubreader/sample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.sample;
2 |
3 | import android.app.Application;
4 | import android.os.Build;
5 | import android.os.Bundle;
6 | import android.os.StrictMode;
7 | import android.support.annotation.Nullable;
8 | import android.support.v7.app.AppCompatActivity;
9 | import android.support.v7.widget.LinearLayoutManager;
10 | import android.util.Log;
11 | import android.view.Gravity;
12 | import android.view.View;
13 | import android.webkit.WebView;
14 | import android.widget.SeekBar;
15 |
16 | import com.smartmobilefactory.epubreader.EpubScrollDirection;
17 | import com.smartmobilefactory.epubreader.model.Epub;
18 | import com.smartmobilefactory.epubreader.model.EpubFont;
19 | import com.smartmobilefactory.epubreader.model.EpubLocation;
20 | import com.smartmobilefactory.epubreader.sample.databinding.ActivityMainBinding;
21 |
22 | import io.reactivex.Single;
23 | import io.reactivex.android.schedulers.AndroidSchedulers;
24 | import io.reactivex.schedulers.Schedulers;
25 |
26 | public class MainActivity extends AppCompatActivity {
27 |
28 | private static Single epubSingle;
29 |
30 | private static final String TAG = MainActivity.class.getSimpleName();
31 |
32 | private NightmodePlugin nightmodePlugin;
33 | private TableOfContentsAdapter tableOfContentsAdapter;
34 | private ActivityMainBinding binding;
35 |
36 | @Override
37 | protected void onCreate(@Nullable Bundle savedInstanceState) {
38 | super.onCreate(savedInstanceState);
39 | enableStrictMode();
40 | binding = ActivityMainBinding.inflate(getLayoutInflater());
41 | setContentView(binding.getRoot());
42 |
43 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
44 | WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG);
45 | }
46 |
47 | initToolbar();
48 | initSettingsContainer();
49 |
50 | ChapterJavaScriptBridge bridge = new ChapterJavaScriptBridge();
51 | binding.epubView.getSettings().setJavascriptBridge(bridge);
52 | binding.epubView.getSettings().setCustomChapterScript(bridge.getCustomChapterScripts());
53 | binding.epubView.getSettings().setFont(EpubFont.fromFontFamily("Monospace"));
54 | binding.epubView.setScrollDirection(EpubScrollDirection.HORIZONTAL_WITH_VERTICAL_CONTENT);
55 |
56 | nightmodePlugin = new NightmodePlugin(binding.epubView);
57 | binding.epubView.addPlugin(nightmodePlugin);
58 |
59 | tableOfContentsAdapter = new TableOfContentsAdapter();
60 | tableOfContentsAdapter.bindToEpubView(binding.epubView);
61 | binding.contentsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
62 | binding.contentsRecyclerView.setAdapter(tableOfContentsAdapter);
63 |
64 | tableOfContentsAdapter.jumpToChapter()
65 | .doOnNext(chapter -> {
66 | binding.drawerLayout.closeDrawer(Gravity.START);
67 | binding.epubView.gotoLocation(EpubLocation.fromChapter(chapter));
68 | })
69 | .subscribe();
70 |
71 | loadEpub().doOnSuccess(epub -> {
72 | binding.epubView.setEpub(epub);
73 | tableOfContentsAdapter.setEpub(epub);
74 | if (savedInstanceState == null) {
75 | binding.epubView.gotoLocation(EpubLocation.fromChapter(10));
76 | }
77 | }).subscribe();
78 |
79 | observeEpub();
80 | }
81 |
82 | Single loadEpub() {
83 | if (epubSingle == null) {
84 | Application application = getApplication();
85 | epubSingle = Single.fromCallable(() -> Epub.fromUri(application, "file:///android_asset/The Silver Chair.epub"))
86 | .subscribeOn(Schedulers.io())
87 | .observeOn(AndroidSchedulers.mainThread())
88 | .cache();
89 | }
90 | return epubSingle;
91 | }
92 |
93 | private void initToolbar() {
94 | binding.toolbar.inflateMenu(R.menu.menu);
95 |
96 | binding.toolbar.setOnMenuItemClickListener(item -> {
97 | switch (item.getItemId()) {
98 | case R.id.menu_settings:
99 | if (binding.settings.getVisibility() == View.VISIBLE) {
100 | binding.settings.setVisibility(View.GONE);
101 | } else {
102 | binding.settings.setVisibility(View.VISIBLE);
103 | }
104 | return true;
105 | default:
106 | return false;
107 | }
108 | });
109 |
110 | binding.toolbar.setNavigationOnClickListener(v -> {
111 | binding.drawerLayout.openDrawer(Gravity.START);
112 | });
113 | }
114 |
115 | private void initSettingsContainer() {
116 |
117 | // TEXT SIZE
118 |
119 | binding.textSizeSeekbar.setMax(30);
120 | binding.textSizeSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
121 | @Override
122 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
123 | binding.epubView.getSettings().setFontSizeSp(progress + 10);
124 | }
125 |
126 | @Override
127 | public void onStartTrackingTouch(SeekBar seekBar) {
128 |
129 | }
130 |
131 | @Override
132 | public void onStopTrackingTouch(SeekBar seekBar) {
133 |
134 | }
135 | });
136 |
137 | // DISPLAY FONT
138 | binding.diplomata.setOnClickListener(v -> {
139 | binding.epubView.getSettings().setFont(EpubFont.fromUri("DiplomataSC", "file:///android_asset/fonts/Diplomata_SC/DiplomataSC-Regular.ttf"));
140 | });
141 |
142 | binding.monospace.setOnClickListener(v -> {
143 | binding.epubView.getSettings().setFont(EpubFont.fromFontFamily("Monospace"));
144 | });
145 |
146 | binding.serif.setOnClickListener(v -> {
147 | binding.epubView.getSettings().setFont(EpubFont.fromFontFamily("Serif"));
148 | });
149 |
150 | binding.sanSerif.setOnClickListener(v -> {
151 | binding.epubView.getSettings().setFont(EpubFont.fromFontFamily("Sans Serif"));
152 | });
153 |
154 | // DISPLAY STRATEGY
155 |
156 | binding.horizontalVerticalContent.setOnClickListener(v -> {
157 | binding.epubView.setScrollDirection(EpubScrollDirection.HORIZONTAL_WITH_VERTICAL_CONTENT);
158 | });
159 |
160 | binding.verticalVerticalContent.setOnClickListener(v -> {
161 | binding.epubView.setScrollDirection(EpubScrollDirection.VERTICAL_WITH_VERTICAL_CONTENT);
162 | });
163 |
164 | binding.singleChapterVertical.setOnClickListener(v -> {
165 | binding.epubView.setScrollDirection(EpubScrollDirection.SINGLE_CHAPTER_VERTICAL);
166 | });
167 |
168 | binding.nightmode.setOnCheckedChangeListener((buttonView, isChecked) -> {
169 | nightmodePlugin.setNightModeEnabled(isChecked);
170 | });
171 |
172 | }
173 |
174 | private void observeEpub() {
175 | binding.epubView.currentLocation()
176 | .doOnNext(xPathLocation -> {
177 | Log.d(TAG, "CurrentLocation: " + xPathLocation);
178 | }).subscribe();
179 |
180 | binding.epubView.currentChapter()
181 | .doOnNext(chapter -> {
182 | Log.d(TAG, "CurrentChapter: " + chapter);
183 | }).subscribe();
184 | }
185 |
186 | private void enableStrictMode() {
187 | StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
188 | .detectAll()
189 | .penaltyLog()
190 | .build());
191 | StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
192 | .detectAll()
193 | .penaltyLog()
194 | .build());
195 | }
196 |
197 | }
198 |
--------------------------------------------------------------------------------
/app/src/main/assets/books/javascript/rangy/rangy-highlighter.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Highlighter module for Rangy, a cross-browser JavaScript range and selection library
3 | * https://github.com/timdown/rangy
4 | *
5 | * Depends on Rangy core, ClassApplier and optionally TextRange modules.
6 | *
7 | * Copyright 2015, Tim Down
8 | * Licensed under the MIT license.
9 | * Version: 1.3.0
10 | * Build date: 10 May 2015
11 | */
12 | !function(e,t){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(t.rangy)}(function(e){return e.createModule("Highlighter",["ClassApplier"],function(e){function t(e,t){return e.characterRange.start-t.characterRange.start}function n(e,t){return t?e.getElementById(t):l(e)}function r(e,t){this.type=e,this.converterCreator=t}function i(e,t){f[e]=new r(e,t)}function a(e){var t=f[e];if(t instanceof r)return t.create();throw new Error("Highlighter type '"+e+"' is not valid")}function s(e,t){this.start=e,this.end=t}function h(e,t,n,r,i,a){i?(this.id=i,d=Math.max(d,i+1)):this.id=d++,this.characterRange=t,this.doc=e,this.classApplier=n,this.converter=r,this.containerElementId=a||null,this.applied=!1}function o(e,t){t=t||"textContent",this.doc=e||document,this.classAppliers={},this.highlights=[],this.converter=a(t)}var c=e.dom,g=c.arrayContains,l=c.getBody,u=e.util.createOptions,p=e.util.forEach,d=1,f={};r.prototype.create=function(){var e=this.converterCreator();return e.type=this.type,e},e.registerHighlighterType=i,s.prototype={intersects:function(e){return this.starte.start},isContiguousWith:function(e){return this.start==e.end||this.end==e.start},union:function(e){return new s(Math.min(this.start,e.start),Math.max(this.end,e.end))},intersection:function(e){return new s(Math.max(this.start,e.start),Math.min(this.end,e.end))},getComplements:function(e){var t=[];if(this.start>=e.start){if(this.end<=e.end)return[];t.push(new s(e.end,this.end))}else t.push(new s(this.start,Math.min(this.end,e.start))),this.end>e.end&&t.push(new s(e.end,this.end));return t},toString:function(){return"[CharacterRange("+this.start+", "+this.end+")]"}},s.fromCharacterRange=function(e){return new s(e.start,e.end)};var R={rangeToCharacterRange:function(e,t){var n=e.getBookmark(t);return new s(n.start,n.end)},characterRangeToRange:function(t,n,r){var i=e.createRange(t);return i.moveToBookmark({start:n.start,end:n.end,containerNode:r}),i},serializeSelection:function(e,t){for(var n=e.getAllRanges(),r=n.length,i=[],a=1==r&&e.isBackward(),s=0,h=n.length;h>s;++s)i[s]={characterRange:this.rangeToCharacterRange(n[s],t),backward:a};return i},restoreSelection:function(e,t,n){e.removeAllRanges();for(var r,i,a,s=e.win.document,h=0,o=t.length;o>h;++h)i=t[h],a=i.characterRange,r=this.characterRangeToRange(s,i.characterRange,n),e.addRange(r,i.backward)}};i("textContent",function(){return R}),i("TextRange",function(){var t;return function(){if(!t){var n=e.modules.TextRange;if(!n)throw new Error("TextRange module is missing.");if(!n.supported)throw new Error("TextRange module is present but not supported.");t={rangeToCharacterRange:function(e,t){return s.fromCharacterRange(e.toCharacterRange(t))},characterRangeToRange:function(t,n,r){var i=e.createRange(t);return i.selectCharacters(r,n.start,n.end),i},serializeSelection:function(e,t){return e.saveCharacterRanges(t)},restoreSelection:function(e,t,n){e.restoreCharacterRanges(n,t)}}}return t}}()),h.prototype={getContainerElement:function(){return n(this.doc,this.containerElementId)},getRange:function(){return this.converter.characterRangeToRange(this.doc,this.characterRange,this.getContainerElement())},fromRange:function(e){this.characterRange=this.converter.rangeToCharacterRange(e,this.getContainerElement())},getText:function(){return this.getRange().toString()},containsElement:function(e){return this.getRange().containsNodeContents(e.firstChild)},unapply:function(){this.classApplier.undoToRange(this.getRange()),this.applied=!1},apply:function(){this.classApplier.applyToRange(this.getRange()),this.applied=!0},getHighlightElements:function(){return this.classApplier.getElementsWithClassIntersectingRange(this.getRange())},toString:function(){return"[Highlight(ID: "+this.id+", class: "+this.classApplier.className+", character range: "+this.characterRange.start+" - "+this.characterRange.end+")]"}},o.prototype={addClassApplier:function(e){this.classAppliers[e.className]=e},getHighlightForElement:function(e){for(var t=this.highlights,n=0,r=t.length;r>n;++n)if(t[n].containsElement(e))return t[n];return null},removeHighlights:function(e){for(var t,n=0,r=this.highlights.length;r>n;++n)t=this.highlights[n],g(e,t)&&(t.unapply(),this.highlights.splice(n--,1))},removeAllHighlights:function(){this.removeHighlights(this.highlights)},getIntersectingHighlights:function(e){var t=[],n=this.highlights;return p(e,function(e){p(n,function(n){e.intersectsRange(n.getRange())&&!g(t,n)&&t.push(n)})}),t},highlightCharacterRanges:function(t,n,r){var i,a,o,c=this.highlights,g=this.converter,l=this.doc,d=[],f=t?this.classAppliers[t]:null;r=u(r,{containerElementId:null,exclusive:!0});var R,v,m,C=r.containerElementId,w=r.exclusive;C&&(R=this.doc.getElementById(C),R&&(v=e.createRange(this.doc),v.selectNodeContents(R),m=new s(0,v.toString().length)));var y,E,T,x,A,H;for(i=0,a=n.length;a>i;++i)if(y=n[i],A=[],m&&(y=y.intersection(m)),y.start!=y.end){for(o=0;o0},serialize:function(e){var n,r,i,s,h=this,o=h.highlights;return o.sort(t),e=u(e,{serializeHighlightText:!1,type:h.converter.type}),n=e.type,i=n!=h.converter.type,i&&(s=a(n)),r=["type:"+n],p(o,function(t){var n,a=t.characterRange;i&&(n=t.getContainerElement(),a=s.rangeToCharacterRange(h.converter.characterRangeToRange(h.doc,a,n),n));var o=[a.start,a.end,t.id,t.classApplier.className,t.containerElementId];e.serializeHighlightText&&o.push(t.getText()),r.push(o.join("$"))}),r.join("|")},deserialize:function(e){var t,r,i,o=e.split("|"),c=[],g=o[0],l=!1;if(!g||!(t=/^type:(\w+)$/.exec(g)))throw new Error("Serialized highlights are invalid.");r=t[1],r!=this.converter.type&&(i=a(r),l=!0),o.shift();for(var u,p,d,f,R,v,m=o.length;m-->0;){if(v=o[m].split("$"),d=new s(+v[0],+v[1]),f=v[4]||null,l&&(R=n(this.doc,f),d=this.converter.rangeToCharacterRange(i.characterRangeToRange(this.doc,d,R),R)),u=this.classAppliers[v[3]],!u)throw new Error("No class applier found for class '"+v[3]+"'");p=new h(this.doc,d,u,this.converter,parseInt(v[2]),f),p.apply(),c.push(p)}this.highlights=c}},e.Highlighter=o,e.createHighlighter=function(e,t){return new o(e,t)}}),e},this);
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/display/view/EpubWebView.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader.display.view
2 |
3 | import android.annotation.SuppressLint
4 | import android.annotation.TargetApi
5 | import android.content.Context
6 | import android.graphics.Bitmap
7 | import android.graphics.Color
8 | import android.os.Build
9 | import android.util.AttributeSet
10 | import android.webkit.WebResourceRequest
11 | import android.webkit.WebView
12 | import android.webkit.WebViewClient
13 | import com.smartmobilefactory.epubreader.EpubJavaScriptBridge
14 | import com.smartmobilefactory.epubreader.EpubViewSettings
15 | import com.smartmobilefactory.epubreader.InternalEpubViewSettings
16 | import com.smartmobilefactory.epubreader.display.EpubDisplayHelper
17 | import com.smartmobilefactory.epubreader.display.WebViewHelper
18 | import com.smartmobilefactory.epubreader.model.Epub
19 | import com.smartmobilefactory.epubreader.model.EpubFont
20 | import com.smartmobilefactory.epubreader.model.EpubLocation
21 | import com.smartmobilefactory.epubreader.utils.BaseDisposableObserver
22 | import io.reactivex.Observable
23 | import io.reactivex.android.schedulers.AndroidSchedulers
24 | import io.reactivex.disposables.CompositeDisposable
25 | import io.reactivex.subjects.BehaviorSubject
26 | import nl.siegmann.epublib.domain.SpineReference
27 | import java.lang.ref.WeakReference
28 | import java.util.concurrent.TimeUnit
29 |
30 | @Suppress("LeakingThis")
31 | @SuppressLint("SetJavaScriptEnabled")
32 | internal open class EpubWebView @JvmOverloads constructor(
33 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
34 | ) : WebView(context, attrs, defStyleAttr) {
35 |
36 | private val settingsCompositeDisposable = CompositeDisposable()
37 | private val loadEpubCompositeDisposable = CompositeDisposable()
38 |
39 | private var urlInterceptor: (url: String) -> Boolean = { true }
40 |
41 | val webViewHelper = WebViewHelper(this)
42 | val js = JsApi(webViewHelper)
43 |
44 | private val isReady = BehaviorSubject.createDefault(false)
45 |
46 | private var settingsWeakReference: WeakReference? = null
47 |
48 | private val client = object : WebViewClient() {
49 | override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
50 | super.onPageStarted(view, url, favicon)
51 | isReady.onNext(false)
52 | }
53 |
54 | override fun onPageFinished(view: WebView, url: String) {
55 | super.onPageFinished(view, url)
56 | isReady.onNext(true)
57 | }
58 |
59 | override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
60 | return urlInterceptor(url)
61 | }
62 |
63 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
64 | override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
65 | @Suppress("DEPRECATION")
66 | return shouldOverrideUrlLoading(view, request.url.toString())
67 | }
68 |
69 | }
70 |
71 | init {
72 | setWebViewClient(client)
73 | settings.javaScriptEnabled = true
74 | settings.allowUniversalAccessFromFileURLs = true
75 | settings.allowFileAccess = true
76 | isVerticalScrollBarEnabled = false
77 | isHorizontalScrollBarEnabled = false
78 | setBackgroundColor(Color.TRANSPARENT)
79 | }
80 |
81 | @SuppressLint("JavascriptInterface", "AddJavascriptInterface")
82 | fun setInternalBridge(bridge: InternalEpubBridge) {
83 | addJavascriptInterface(bridge, "internalBridge")
84 | }
85 |
86 | fun isReady(): Observable {
87 | return isReady
88 | }
89 |
90 | fun gotoLocation(location: EpubLocation) {
91 | isReady.filter { isReady -> isReady }
92 | .take(1)
93 | .doOnNext {
94 | when (location) {
95 | is EpubLocation.IdLocation -> js.scrollToElementById(location.id())
96 | is EpubLocation.XPathLocation -> js.scrollToElementByXPath(location.xPath())
97 | is EpubLocation.RangeLocation -> js.scrollToRangeStart(location.start())
98 | }
99 | }
100 | .subscribe(BaseDisposableObserver())
101 | }
102 |
103 | fun setUrlInterceptor(interceptor: (url: String) -> Boolean) {
104 | this.urlInterceptor = interceptor
105 | }
106 |
107 | fun bindToSettings(settings: InternalEpubViewSettings?) {
108 | if (settings == null) {
109 | return
110 | }
111 | settingsWeakReference = WeakReference(settings)
112 | settingsCompositeDisposable.clear()
113 |
114 | settings.anySettingHasChanged()
115 | .doOnNext { setting ->
116 | when (setting) {
117 | EpubViewSettings.Setting.FONT -> setFont(settings.font)
118 | EpubViewSettings.Setting.FONT_SIZE -> setFontSizeSp(settings.fontSizeSp)
119 | EpubViewSettings.Setting.JAVASCRIPT_BRIDGE -> setJavascriptBridge(settings.javascriptBridges)
120 | else -> {}
121 | }
122 | }
123 | .debounce(100, TimeUnit.MILLISECONDS)
124 | .observeOn(AndroidSchedulers.mainThread())
125 | .doOnNext { js.updateFirstVisibleElement() }
126 | .subscribeWith(BaseDisposableObserver())
127 | .addTo(settingsCompositeDisposable)
128 |
129 | setFontSizeSp(settings.fontSizeSp)
130 | setJavascriptBridge(settings.javascriptBridges)
131 | setFont(settings.font)
132 |
133 | }
134 |
135 | fun loadEpubPage(epub: Epub, spineReference: SpineReference, settings: InternalEpubViewSettings?) {
136 | if (settings == null) {
137 | return
138 | }
139 | loadEpubCompositeDisposable.clear()
140 | settings.anySettingHasChanged()
141 | // reload html when some settings changed
142 | .filter { setting -> setting == EpubViewSettings.Setting.CUSTOM_FILES }
143 | .startWith(EpubViewSettings.Setting.CUSTOM_FILES)
144 | .flatMap {
145 | EpubDisplayHelper.loadHtmlData(this, epub, spineReference, settings)
146 | .toObservable()
147 | }
148 | .subscribeWith(BaseDisposableObserver())
149 | .addTo(loadEpubCompositeDisposable)
150 | }
151 |
152 | override fun onAttachedToWindow() {
153 | super.onAttachedToWindow()
154 | if (settingsWeakReference != null && settingsWeakReference!!.get() != null) {
155 | bindToSettings(settingsWeakReference!!.get())
156 | }
157 | }
158 |
159 | override fun onDetachedFromWindow() {
160 | super.onDetachedFromWindow()
161 | settingsCompositeDisposable.clear()
162 | loadEpubCompositeDisposable.clear()
163 | }
164 |
165 | @SuppressLint("JavascriptInterface", "AddJavascriptInterface")
166 | private fun setJavascriptBridge(bridges: List) {
167 | for ((name, bridge) in bridges) {
168 | addJavascriptInterface(bridge, name)
169 | }
170 | }
171 |
172 | private fun setFont(font: EpubFont) {
173 | if (font.uri() == null && font.name() == null) {
174 | return
175 | }
176 | isReady.filter { isReady -> isReady }
177 | .take(1)
178 | .subscribe {
179 | if (font.uri() == null) {
180 | font.name()?.let { js.setFontFamily(it) }
181 | } else {
182 | js.setFont(font.name(), font.uri())
183 | }
184 | }
185 | }
186 |
187 | fun callJavascriptMethod(name: String, vararg args: Any) {
188 | webViewHelper.callJavaScriptMethod(name, *args)
189 | }
190 |
191 | private fun setFontSizeSp(fontSizeSp: Int) {
192 | settings.defaultFontSize = fontSizeSp
193 | settings.minimumFontSize = fontSizeSp
194 | settings.defaultFixedFontSize = fontSizeSp
195 | settings.minimumLogicalFontSize = fontSizeSp
196 | }
197 |
198 | }
199 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
16 |
17 |
25 |
26 |
30 |
31 |
35 |
36 |
46 |
47 |
53 |
54 |
59 |
60 |
66 |
67 |
70 |
71 |
74 |
75 |
81 |
82 |
88 |
89 |
95 |
96 |
102 |
103 |
104 |
105 |
106 |
112 |
113 |
116 |
117 |
120 |
121 |
127 |
128 |
134 |
135 |
141 |
142 |
143 |
144 |
145 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
164 |
165 |
173 |
174 |
178 |
179 |
183 |
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------
/library/src/main/java/com/smartmobilefactory/epubreader/EpubView.kt:
--------------------------------------------------------------------------------
1 | package com.smartmobilefactory.epubreader
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Parcelable
7 | import android.util.AttributeSet
8 | import android.widget.FrameLayout
9 | import com.smartmobilefactory.epubreader.display.EpubDisplayStrategy
10 | import com.smartmobilefactory.epubreader.display.vertical_content.SingleChapterVerticalEpubDisplayStrategy
11 | import com.smartmobilefactory.epubreader.display.vertical_content.horizontal_chapters.HorizontalWithVerticalContentEpubDisplayStrategy
12 | import com.smartmobilefactory.epubreader.display.vertical_content.vertical_chapters.VerticalWithVerticalContentEpubDisplayStrategy
13 | import com.smartmobilefactory.epubreader.model.Epub
14 | import com.smartmobilefactory.epubreader.model.EpubLocation
15 | import com.smartmobilefactory.epubreader.utils.BaseDisposableObserver
16 | import io.reactivex.Observable
17 | import io.reactivex.disposables.CompositeDisposable
18 | import io.reactivex.subjects.BehaviorSubject
19 |
20 | class EpubView @JvmOverloads constructor(
21 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
22 | ) : FrameLayout(context, attrs, defStyleAttr) {
23 |
24 | private var epub: Epub? = null
25 | val settings = EpubViewSettings()
26 |
27 | internal val internalSettings = InternalEpubViewSettings(settings)
28 |
29 | private val currentChapterSubject = BehaviorSubject.create()
30 | private val currentLocationSubject = BehaviorSubject.create()
31 |
32 | private val strategyDisposables = CompositeDisposable()
33 | private var strategy: EpubDisplayStrategy? = null
34 |
35 | var scrollDirection: EpubScrollDirection = EpubScrollDirection.HORIZONTAL_WITH_VERTICAL_CONTENT
36 | set(scrollDirection) {
37 | if (field == scrollDirection && strategy != null) {
38 | return
39 | }
40 | field = scrollDirection
41 | applyScrollDirection(scrollDirection)
42 | }
43 |
44 | private var savedState: SavedState? = null
45 |
46 | private var urlInterceptor: UrlInterceptor? = null
47 |
48 | val currentChapter: Int
49 | get() = currentChapterSubject.value
50 |
51 | val currentLocation: EpubLocation
52 | get() = currentLocationSubject.value ?: EpubLocation.fromChapter(currentChapter)
53 |
54 | init {
55 | scrollDirection = EpubScrollDirection.HORIZONTAL_WITH_VERTICAL_CONTENT
56 | }
57 |
58 | private fun applyScrollDirection(scrollDirection: EpubScrollDirection) {
59 | // make sure every case is handled
60 | @Suppress("UNUSED_VARIABLE")
61 | val unit = when (scrollDirection) {
62 | EpubScrollDirection.HORIZONTAL_WITH_VERTICAL_CONTENT -> applyDisplayStrategy(HorizontalWithVerticalContentEpubDisplayStrategy())
63 | EpubScrollDirection.SINGLE_CHAPTER_VERTICAL -> applyDisplayStrategy(SingleChapterVerticalEpubDisplayStrategy())
64 | EpubScrollDirection.VERTICAL_WITH_VERTICAL_CONTENT -> applyDisplayStrategy(VerticalWithVerticalContentEpubDisplayStrategy())
65 | }
66 | }
67 |
68 | private fun applyDisplayStrategy(newStrategy: EpubDisplayStrategy) {
69 | if (strategy != null) {
70 | unbindCurrentDisplayStrategy()
71 | }
72 | strategy = newStrategy
73 |
74 | newStrategy.bind(this, this)
75 |
76 | newStrategy.currentLocation()
77 | .doOnNext { location -> currentLocationSubject.onNext(location) }
78 | .subscribeWith(BaseDisposableObserver())
79 | .addTo(strategyDisposables)
80 |
81 | newStrategy.onChapterChanged()
82 | .doOnNext { chapter -> currentChapterSubject.onNext(chapter) }
83 | .subscribeWith(BaseDisposableObserver())
84 | .addTo(strategyDisposables)
85 |
86 | epub?.let { epub ->
87 | val location = currentLocation
88 | strategy?.displayEpub(epub, location)
89 | }
90 | }
91 |
92 | @JvmOverloads
93 | fun setEpub(epub: Epub?, location: EpubLocation? = null) {
94 | @Suppress("NAME_SHADOWING")
95 | var location = location
96 |
97 | if (epub == null) {
98 | unbindCurrentDisplayStrategy()
99 | return
100 | }
101 |
102 | if (epub.isDestroyed) {
103 | throw IllegalArgumentException("epub is already destroyed")
104 | }
105 |
106 | if (strategy == null) {
107 | applyScrollDirection(scrollDirection)
108 | }
109 |
110 | if (location == null) {
111 | location = if (savedState != null && Uri.fromFile(epub.location).toString() == savedState?.epubUri) {
112 | savedState?.location
113 | } else {
114 | EpubLocation.fromChapter(0)
115 | }
116 | savedState = null
117 | }
118 |
119 | location = location ?: EpubLocation.fromChapter(0) ?: return
120 |
121 | if (this.epub === epub) {
122 | gotoLocation(location)
123 | return
124 | }
125 |
126 | this.epub = epub
127 | strategy?.displayEpub(epub, location)
128 | }
129 |
130 | private fun unbindCurrentDisplayStrategy() {
131 | // unbind and remove current strategy
132 | strategyDisposables.clear()
133 | strategy?.unbind()
134 | strategy = null
135 | removeAllViews()
136 | }
137 |
138 | fun getEpub(): Epub? {
139 | return epub
140 | }
141 |
142 | fun setUrlInterceptor(interceptor: UrlInterceptor) {
143 | this.urlInterceptor = interceptor
144 | }
145 |
146 | internal fun shouldOverrideUrlLoading(url: String): Boolean {
147 |
148 | if (url.startsWith(Uri.fromFile(epub?.location).toString())) {
149 | epub?.let { epub ->
150 | // internal chapter change url
151 | val spineReferences = epub.book.spine.spineReferences
152 |
153 | for (i in spineReferences.indices) {
154 | val spineReference = spineReferences[i]
155 | if (url.endsWith(spineReference.resource.href)) {
156 | gotoLocation(EpubLocation.fromChapter(i))
157 | return true
158 | }
159 | }
160 | }
161 | // chapter not found
162 | // can not open the url
163 | return true
164 | }
165 |
166 | if (urlInterceptor?.shouldOverrideUrlLoading(url) == true) {
167 | return true
168 | }
169 |
170 | try {
171 | // try to open url with external app
172 | val intent = Intent(Intent.ACTION_VIEW)
173 | intent.data = Uri.parse(url)
174 | context.startActivity(intent)
175 | } catch (e: Exception) {
176 | // ignore
177 | }
178 |
179 | // we never want to load a new url in the same webview
180 | return true
181 | }
182 |
183 | fun gotoLocation(location: EpubLocation) {
184 | if (epub == null) {
185 | throw IllegalStateException("setEpub must be called first")
186 | }
187 | strategy?.gotoLocation(location)
188 | }
189 |
190 | fun currentChapter(): Observable {
191 | return currentChapterSubject.distinctUntilChanged()
192 | }
193 |
194 | fun currentLocation(): Observable {
195 | return currentLocationSubject.distinctUntilChanged()
196 | }
197 |
198 | /**
199 | * calls a javascript method on all visible chapters
200 | * this depends on the selected display strategy
201 | */
202 | fun callChapterJavascriptMethod(name: String, vararg args: Any) {
203 | strategy?.callChapterJavascriptMethod(name, *args)
204 | }
205 |
206 | /**
207 | * calls a javascript method on the selected chapter if visible
208 | * this depends on the selected display strategy
209 | */
210 | fun callChapterJavascriptMethod(chapter: Int, name: String, vararg args: Any) {
211 | strategy?.callChapterJavascriptMethod(chapter, name, *args)
212 | }
213 |
214 | fun addPlugin(epubPlugin: EpubViewPlugin) {
215 | internalSettings.addPlugin(epubPlugin)
216 | }
217 |
218 | fun removePlugin(epubPlugin: EpubViewPlugin) {
219 | internalSettings.removePlugin(epubPlugin)
220 | }
221 |
222 | public override fun onSaveInstanceState(): Parcelable? {
223 | val superState = super.onSaveInstanceState()
224 | val ss = SavedState(superState)
225 |
226 | if (epub != null) {
227 | ss.epubUri = Uri.fromFile(epub?.location).toString()
228 | }
229 |
230 | ss.location = strategy?.currentLocation ?: currentLocation
231 | if (savedState != null) {
232 | ss.location = savedState?.location
233 | }
234 |
235 | if (ss.location == null && strategy != null) {
236 | ss.location = EpubLocation.fromChapter(strategy?.currentChapter ?: currentChapter)
237 | }
238 | return ss
239 | }
240 |
241 | public override fun onRestoreInstanceState(state: Parcelable) {
242 | if (state !is SavedState) {
243 | super.onRestoreInstanceState(state)
244 | return
245 | }
246 |
247 | super.onRestoreInstanceState(state.superState)
248 |
249 | if (epub != null && Uri.fromFile(epub?.location).toString() == state.epubUri) {
250 | state.location?.let { gotoLocation(it) }
251 | } else {
252 | savedState = state
253 | }
254 |
255 | }
256 |
257 | }
258 |
--------------------------------------------------------------------------------
/app/src/main/assets/books/javascript/rangy/rangy-classapplier.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Class Applier module for Rangy.
3 | * Adds, removes and toggles classes on Ranges and Selections
4 | *
5 | * Part of Rangy, a cross-browser JavaScript range and selection library
6 | * https://github.com/timdown/rangy
7 | *
8 | * Depends on Rangy core.
9 | *
10 | * Copyright 2015, Tim Down
11 | * Licensed under the MIT license.
12 | * Version: 1.3.0
13 | * Build date: 10 May 2015
14 | */
15 | !function(e,t){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(t.rangy)}(function(e){return e.createModule("ClassApplier",["WrappedSelection"],function(e,t){function n(e,t){for(var n in e)if(e.hasOwnProperty(n)&&t(n,e[n])===!1)return!1;return!0}function s(e){return e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}function r(e,t){return!!e&&new RegExp("(?:^|\\s)"+t+"(?:\\s|$)").test(e)}function o(e,t){if("object"==typeof e.classList)return e.classList.contains(t);var n="string"==typeof e.className,s=n?e.className:e.getAttribute("class");return r(s,t)}function a(e,t){if("object"==typeof e.classList)e.classList.add(t);else{var n="string"==typeof e.className,s=n?e.className:e.getAttribute("class");s?r(s,t)||(s+=" "+t):s=t,n?e.className=s:e.setAttribute("class",s)}}function l(e){var t="string"==typeof e.className;return t?e.className:e.getAttribute("class")}function u(e){return e&&e.split(/\s+/).sort().join(" ")}function f(e){return u(l(e))}function c(e,t){return f(e)==f(t)}function p(e,t){for(var n=t.split(/\s+/),r=0,i=n.length;i>r;++r)if(!o(e,s(n[r])))return!1;return!0}function d(e){var t=e.parentNode;return t&&1==t.nodeType&&!/^(textarea|style|script|select|iframe)$/i.test(t.nodeName)}function h(e,t,n,s,r){var i=e.node,o=e.offset,a=i,l=o;i==s&&o>r&&++l,i!=t||o!=n&&o!=n+1||(a=s,l+=r-n),i==t&&o>n+1&&--l,e.node=a,e.offset=l}function m(e,t,n){e.node==t&&e.offset>n&&--e.offset}function g(e,t,n,s){-1==n&&(n=t.childNodes.length);var r=e.parentNode,i=$.getNodeIndex(e);U(s,function(e){h(e,r,i,t,n)}),t.childNodes.length==n?t.appendChild(e):t.insertBefore(e,t.childNodes[n])}function N(e,t){var n=e.parentNode,s=$.getNodeIndex(e);U(t,function(e){m(e,n,s)}),$.removeNode(e)}function v(e,t,n,s,r){for(var i,o=[];i=e.firstChild;)g(i,t,n++,r),o.push(i);return s&&N(e,r),o}function y(e,t){return v(e,e.parentNode,$.getNodeIndex(e),!0,t)}function C(e,t){var n=e.cloneRange();n.selectNodeContents(t);var s=n.intersection(e),r=s?s.toString():"";return""!=r}function T(e){for(var t,n=e.getNodes([3]),s=0;(t=n[s])&&!C(e,t);)++s;for(var r=n.length-1;(t=n[r])&&!C(e,t);)--r;return n.slice(s,r+1)}function E(e,t){if(e.attributes.length!=t.attributes.length)return!1;for(var n,s,r,i=0,o=e.attributes.length;o>i;++i)if(n=e.attributes[i],r=n.name,"class"!=r){if(s=t.attributes.getNamedItem(r),null===n!=(null===s))return!1;if(n.specified!=s.specified)return!1;if(n.specified&&n.nodeValue!==s.nodeValue)return!1}return!0}function b(e,t){for(var n,s=0,r=e.attributes.length;r>s;++s)if(n=e.attributes[s].name,(!t||!B(t,n))&&e.attributes[s].specified&&"class"!=n)return!0;return!1}function A(e){var t;return e&&1==e.nodeType&&((t=e.parentNode)&&9==t.nodeType&&"on"==t.designMode||G(e)&&!G(e.parentNode))}function S(e){return(G(e)||1!=e.nodeType&&G(e.parentNode))&&!A(e)}function x(e){return e&&1==e.nodeType&&!J.test(F(e,"display"))}function R(e){if(0==e.data.length)return!0;if(K.test(e.data))return!1;var t=F(e.parentNode,"whiteSpace");switch(t){case"pre":case"pre-wrap":case"-moz-pre-wrap":return!1;case"pre-line":if(/[\r\n]/.test(e.data))return!1}return x(e.previousSibling)||x(e.nextSibling)}function P(e){var t,n,s=[];for(t=0;n=e[t++];)s.push(new z(n.startContainer,n.startOffset),new z(n.endContainer,n.endOffset));return s}function w(e,t){for(var n,s,r,i=0,o=e.length;o>i;++i)n=e[i],s=t[2*i],r=t[2*i+1],n.setStartAndEnd(s.node,s.offset,r.node,r.offset)}function O(e,t){return $.isCharacterDataNode(e)?0==t?!!e.previousSibling:t==e.length?!!e.nextSibling:!0:t>0&&to;++o)"*"==r[o]?f.applyToAnyTagName=!0:f.tagNames.push(r[o].toLowerCase());else f.tagNames=[f.elementTagName]}function j(e,t,n){return new H(e,t,n)}var $=e.dom,z=$.DomPosition,B=$.arrayContains,D=e.util,U=D.forEach,V="span",k=D.isHostMethod(document,"createElementNS"),q=function(){function e(e,t,n){return t&&n?" ":""}return function(t,n){if("object"==typeof t.classList)t.classList.remove(n);else{var s="string"==typeof t.className,r=s?t.className:t.getAttribute("class");r=r.replace(new RegExp("(^|\\s)"+n+"(\\s|$)"),e),s?t.className=r:t.setAttribute("class",r)}}}(),F=$.getComputedStyleProperty,G=function(){var e=document.createElement("div");return"boolean"==typeof e.isContentEditable?function(e){return e&&1==e.nodeType&&e.isContentEditable}:function(e){return e&&1==e.nodeType&&"false"!=e.contentEditable?"true"==e.contentEditable||G(e.parentNode):!1}}(),J=/^inline(-block|-table)?$/i,K=/[^\r\n\t\f \u200B]/,Q=L(!1),X=L(!0);M.prototype={doMerge:function(e){var t=this.textNodes,n=t[0];if(t.length>1){var s,r=$.getNodeIndex(n),i=[],o=0;U(t,function(t,a){s=t.parentNode,a>0&&(s.removeChild(t),s.hasChildNodes()||$.removeNode(s),e&&U(e,function(e){e.node==t&&(e.node=n,e.offset+=o),e.node==s&&e.offset>r&&(--e.offset,e.offset==r+1&&len-1>a&&(e.node=n,e.offset=o))})),i[a]=t.data,o+=t.data.length}),n.data=i.join("")}return n.data},getLength:function(){for(var e=this.textNodes.length,t=0;e--;)t+=this.textNodes[e].length;return t},toString:function(){var e=[];return U(this.textNodes,function(t,n){e[n]="'"+t.data+"'"}),"[Merge("+e.join(",")+")]"}};var Y=["elementTagName","ignoreWhiteSpace","applyToEditableOnly","useExistingElements","removeEmptyElements","onElementCreate"],Z={};H.prototype={elementTagName:V,elementProperties:{},elementAttributes:{},ignoreWhiteSpace:!0,applyToEditableOnly:!1,useExistingElements:!0,removeEmptyElements:!0,onElementCreate:null,copyPropertiesToElement:function(e,t,n){var s,r,i,o,l,f,c={};for(var p in e)if(e.hasOwnProperty(p))if(o=e[p],l=t[p],"className"==p)a(t,o),a(t,this.className),t[p]=u(t[p]),n&&(c[p]=o);else if("style"==p){r=l,n&&(c[p]=i={});for(s in e[p])e[p].hasOwnProperty(s)&&(r[s]=o[s],n&&(i[s]=r[s]));this.attrExceptions.push(p)}else t[p]=o,n&&(c[p]=t[p],f=Z.hasOwnProperty(p)?Z[p]:p,this.attrExceptions.push(f));return n?c:""},copyAttributesToElement:function(e,t){for(var n in e)e.hasOwnProperty(n)&&!/^class(?:Name)?$/i.test(n)&&t.setAttribute(n,e[n])},appliesToElement:function(e){return B(this.tagNames,e.tagName.toLowerCase())},getEmptyElements:function(e){var t=this;return e.getNodes([1],function(e){return t.appliesToElement(e)&&!e.hasChildNodes()})},hasClass:function(e){return 1==e.nodeType&&(this.applyToAnyTagName||this.appliesToElement(e))&&o(e,this.className)},getSelfOrAncestorWithClass:function(e){for(;e;){if(this.hasClass(e))return e;e=e.parentNode}return null},isModifiable:function(e){return!this.applyToEditableOnly||S(e)},isIgnorableWhiteSpaceNode:function(e){return this.ignoreWhiteSpace&&e&&3==e.nodeType&&R(e)},postApply:function(e,t,n,s){var r,o,a=e[0],l=e[e.length-1],u=[],f=a,c=l,p=0,d=l.length;U(e,function(e){o=Q(e,!s),o?(r||(r=new M(o),u.push(r)),r.textNodes.push(e),e===a&&(f=r.textNodes[0],p=f.length),e===l&&(c=r.textNodes[0],d=r.getLength())):r=null});var h=X(l,!s);if(h&&(r||(r=new M(l),u.push(r)),r.textNodes.push(h)),u.length){for(i=0,len=u.length;len>i;++i)u[i].doMerge(n);t.setStartAndEnd(f,p,c,d)}},createContainer:function(e){var t,n=$.getDocument(e),s=k&&!$.isHtmlNamespace(e)&&(t=e.namespaceURI)?n.createElementNS(e.namespaceURI,this.elementTagName):n.createElement(this.elementTagName);return this.copyPropertiesToElement(this.elementProperties,s,!1),this.copyAttributesToElement(this.elementAttributes,s),a(s,this.className),this.onElementCreate&&this.onElementCreate(s,this),s},elementHasProperties:function(e,t){var s=this;return n(t,function(t,n){if("className"==t)return p(e,n);if("object"==typeof n){if(!s.elementHasProperties(e[t],n))return!1}else if(e[t]!==n)return!1})},elementHasAttributes:function(e,t){return n(t,function(t,n){return e.getAttribute(t)!==n?!1:void 0})},applyToTextNode:function(e){if(d(e)){var t=e.parentNode;if(1==t.childNodes.length&&this.useExistingElements&&this.appliesToElement(t)&&this.elementHasProperties(t,this.elementProperties)&&this.elementHasAttributes(t,this.elementAttributes))a(t,this.className);else{var n=e.parentNode,s=this.createContainer(n);n.insertBefore(s,e),s.appendChild(e)}}},isRemovable:function(e){return e.tagName.toLowerCase()==this.elementTagName&&f(e)==this.elementSortedClassName&&this.elementHasProperties(e,this.elementProperties)&&!b(e,this.attrExceptions)&&this.elementHasAttributes(e,this.elementAttributes)&&this.isModifiable(e)},isEmptyContainer:function(e){var t=e.childNodes.length;return 1==e.nodeType&&this.isRemovable(e)&&(0==t||1==t&&this.isEmptyContainer(e.firstChild))},removeEmptyContainers:function(e){var t=this,n=e.getNodes([1],function(e){return t.isEmptyContainer(e)}),s=[e],r=P(s);U(n,function(e){N(e,r)}),w(s,r)},undoToTextNode:function(e,t,n,s){if(!t.containsNode(n)){var r=t.cloneRange();r.selectNode(n),r.isPointInRange(t.endContainer,t.endOffset)&&(I(n,t.endContainer,t.endOffset,s),t.setEndAfter(n)),r.isPointInRange(t.startContainer,t.startOffset)&&(n=I(n,t.startContainer,t.startOffset,s))}this.isRemovable(n)?y(n,s):q(n,this.className)},splitAncestorWithClass:function(e,t,n){var s=this.getSelfOrAncestorWithClass(e);s&&I(s,e,t,n)},undoToAncestor:function(e,t){this.isRemovable(e)?y(e,t):q(e,this.className)},applyToRange:function(e,t){var n=this;t=t||[];var s=P(t||[]);e.splitBoundariesPreservingPositions(s),n.removeEmptyElements&&n.removeEmptyContainers(e);var r=T(e);if(r.length){U(r,function(e){n.isIgnorableWhiteSpaceNode(e)||n.getSelfOrAncestorWithClass(e)||!n.isModifiable(e)||n.applyToTextNode(e,s)});var i=r[r.length-1];e.setStartAndEnd(r[0],0,i,i.length),n.normalize&&n.postApply(r,e,s,!1),w(t,s)}var o=n.getEmptyElements(e);U(o,function(e){a(e,n.className)})},applyToRanges:function(e){for(var t=e.length;t--;)this.applyToRange(e[t],e);return e},applyToSelection:function(t){var n=e.getSelection(t);n.setRanges(this.applyToRanges(n.getAllRanges()))},undoToRange:function(e,t){var n=this;t=t||[];var s=P(t);e.splitBoundariesPreservingPositions(s),n.removeEmptyElements&&n.removeEmptyContainers(e,s);var r,i,o=T(e),a=o[o.length-1];if(o.length){n.splitAncestorWithClass(e.endContainer,e.endOffset,s),n.splitAncestorWithClass(e.startContainer,e.startOffset,s);for(var l=0,u=o.length;u>l;++l)r=o[l],i=n.getSelfOrAncestorWithClass(r),i&&n.isModifiable(r)&&n.undoToAncestor(i,s);e.setStartAndEnd(o[0],0,a,a.length),n.normalize&&n.postApply(o,e,s,!0),w(t,s)}var f=n.getEmptyElements(e);U(f,function(e){q(e,n.className)})},undoToRanges:function(e){for(var t=e.length;t--;)this.undoToRange(e[t],e);return e},undoToSelection:function(t){var n=e.getSelection(t),s=e.getSelection(t).getAllRanges();this.undoToRanges(s),n.setRanges(s)},isAppliedToRange:function(e){if(e.collapsed||""==e.toString())return!!this.getSelfOrAncestorWithClass(e.commonAncestorContainer);var t=e.getNodes([3]);if(t.length)for(var n,s=0;n=t[s++];)if(!this.isIgnorableWhiteSpaceNode(n)&&C(e,n)&&this.isModifiable(n)&&!this.getSelfOrAncestorWithClass(n))return!1;return!0},isAppliedToRanges:function(e){var t=e.length;if(0==t)return!1;for(;t--;)if(!this.isAppliedToRange(e[t]))return!1;return!0},isAppliedToSelection:function(t){var n=e.getSelection(t);return this.isAppliedToRanges(n.getAllRanges())},toggleRange:function(e){this.isAppliedToRange(e)?this.undoToRange(e):this.applyToRange(e)},toggleSelection:function(e){this.isAppliedToSelection(e)?this.undoToSelection(e):this.applyToSelection(e)},getElementsWithClassIntersectingRange:function(e){var t=[],n=this;return e.getNodes([3],function(e){var s=n.getSelfOrAncestorWithClass(e);s&&!B(t,s)&&t.push(s)}),t},detach:function(){}},H.util={hasClass:o,addClass:a,removeClass:q,getClass:l,hasSameClasses:c,hasAllClasses:p,replaceWithOwnChildren:y,elementsHaveSameNonClassAttributes:E,elementHasNonClassAttributes:b,splitNodeAt:I,isEditableElement:G,isEditingHost:A,isEditable:S},e.CssClassApplier=e.ClassApplier=H,e.createClassApplier=j,D.createAliasForDeprecatedMethod(e,"createCssClassApplier","createClassApplier",t)}),e},this);
--------------------------------------------------------------------------------