├── 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 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /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 |