├── .babelrc ├── .buckconfig ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .watchmanconfig ├── README.md ├── __tests__ ├── index.android.js └── index.ios.js ├── android ├── app │ ├── BUCK │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ ├── index.android.bundle │ │ └── index.android.bundle.meta │ │ ├── java │ │ └── com │ │ │ └── gitbookreader │ │ │ ├── MainActivity.java │ │ │ ├── MainApplication.java │ │ │ ├── MyNativeModule.java │ │ │ └── MyPackage.java │ │ └── res │ │ ├── drawable-mdpi │ │ ├── ic_logo.png │ │ ├── src_img_book.png │ │ ├── src_img_defaultcover.png │ │ ├── src_img_delete.png │ │ ├── src_img_download.png │ │ ├── src_img_feed.png │ │ ├── src_img_history.png │ │ ├── src_img_logo.png │ │ ├── src_img_more.png │ │ ├── src_img_scan.png │ │ ├── src_img_search.png │ │ ├── src_img_star.png │ │ └── src_img_toc.png │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystores │ ├── BUCK │ ├── debug.keystore.properties │ └── rn.jks └── settings.gradle ├── app.json ├── docs ├── qr.png ├── screen1.png ├── screen2.png └── screen3.png ├── index.android.js ├── index.ios.js ├── ios ├── MyNativeModule.h ├── MyNativeModule.m ├── Podfile ├── Podfile.lock ├── gitbookreader-tvOS │ └── Info.plist ├── gitbookreader-tvOSTests │ └── Info.plist ├── gitbookreader.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── gitbookreader-tvOS.xcscheme │ │ └── gitbookreader.xcscheme ├── gitbookreader.xcworkspace │ └── contents.xcworkspacedata ├── gitbookreader │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ └── main.m └── gitbookreaderTests │ ├── Info.plist │ └── gitbookreaderTests.m ├── jsconfig.json ├── package.json ├── src ├── App.js ├── components │ ├── Feeds.js │ ├── ImageWithPlaceHolder.js │ └── RowWithActions.js ├── data │ ├── bookFiles.js │ ├── dataBase.js │ ├── index.js │ └── search.js ├── img │ ├── book.png │ ├── default-cover.png │ ├── delete.png │ ├── download.png │ ├── feed.png │ ├── history.png │ ├── ic_share.png │ ├── logo.png │ ├── more.png │ ├── scan.png │ ├── search.png │ ├── star.png │ └── time.png ├── screens │ ├── BookDetail │ │ ├── Badge.js │ │ ├── CoverHeader.js │ │ ├── Readme.js │ │ └── index.js │ ├── Camera │ │ └── index.js │ ├── Download │ │ ├── Book.js │ │ └── index.js │ ├── Home │ │ ├── Book.js │ │ └── index.js │ ├── More │ │ └── index.js │ ├── OnlineReader │ │ └── index.js │ ├── Reader │ │ ├── TOC.js │ │ └── index.js │ ├── Search │ │ ├── ResultItem.js │ │ └── index.js │ └── index.js └── utils │ └── index.js └── target └── gitbookreader.apk /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | .*/Libraries/react-native/ReactNative.js 16 | 17 | [include] 18 | 19 | [libs] 20 | node_modules/react-native/Libraries/react-native/react-native-interface.js 21 | node_modules/react-native/flow 22 | flow/ 23 | 24 | [options] 25 | emoji=true 26 | 27 | module.system=haste 28 | 29 | experimental.strict_type_args=true 30 | 31 | munge_underscores=true 32 | 33 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 34 | 35 | suppress_type=$FlowIssue 36 | suppress_type=$FlowFixMe 37 | suppress_type=$FixMe 38 | 39 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-2]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 40 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-2]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 41 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 42 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 43 | 44 | unsafe.enable_getters_and_setters=true 45 | 46 | [version] 47 | ^0.42.0 48 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | Pods 25 | 26 | # Android/IntelliJ 27 | # 28 | build/ 29 | .idea 30 | .gradle 31 | local.properties 32 | *.iml 33 | 34 | # node.js 35 | # 36 | node_modules/ 37 | npm-debug.log 38 | yarn-error.log 39 | 40 | # BUCK 41 | buck-out/ 42 | \.buckd/ 43 | *.keystore 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 51 | 52 | fastlane/report.xml 53 | fastlane/Preview.html 54 | fastlane/screenshots 55 | 56 | target/gitbookreader_debug.apk -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GitBook阅读器 2 | 3 | 使用ReactNative开发的GitBook阅读器,可以查看在线的书籍信息,在线阅读和下载。 4 | 5 | 目前仅适配了Android端,iOS适配中。 6 | 7 | ### Android版本下载 8 | 9 | ![qr](./docs/qr.png) 10 | 11 | ### 截屏 12 | 13 | ![screen1](./docs/screen1.png) 14 | 15 | ![screen2](./docs/screen2.png) 16 | 17 | ![screen3](./docs/screen3.png) 18 | 19 | ### 主要功能 20 | 21 | - 展示流行的书籍列表 22 | - 搜索功能 23 | - 在线阅读 24 | - 下载书籍,本地阅读(保持和在线一样的样式) 25 | - 扫描gitbook在线网址的二维码,获得书籍信息 26 | 27 | ### 开发进度 28 | 29 | - [x] Explore页面,根据Star显示流行的书籍列表 30 | - [x] 搜索功能 31 | - [x] 下载功能 32 | - [x] 本地阅读器 33 | - [x] 扫描gitbook在线网址的二维码,获得书籍信息 34 | - [x] 本地阅读器功能丰富: 保存、恢复阅读滚动条进度 35 | - [ ] Explore页面过滤、排序功能 36 | - [ ] 搜索结果过滤、排序功能 37 | - [ ] 本地阅读器功能丰富(收藏、更改字体、night模式) 38 | - [ ] 使用Redux 39 | - [ ] test 40 | 41 | ### 数据来源 42 | 43 | - [Gitbook API](https://developer.gitbook.com) 44 | 45 | - [在线搜索页面](https://www.gitbook.com/search?q=react) 搜索功能没有API,采用了解析html结果的方式 46 | 47 | ### 使用的第三方依赖 48 | 49 | - [react-native-navigation](https://github.com/wix/react-native-navigation/) 50 | - [react-native-interactable](https://github.com/wix/react-native-interactable) 51 | - [react-native-fs](https://github.com/itinance/react-native-fs) 52 | - [realm](https://realm.io/news/introducing-realm-react-native/) 53 | -------------------------------------------------------------------------------- /__tests__/index.android.js: -------------------------------------------------------------------------------- 1 | import 'react-native'; 2 | import React from 'react'; 3 | import Index from '../index.android.js'; 4 | 5 | // Note: test renderer must be required after react-native. 6 | import renderer from 'react-test-renderer'; 7 | 8 | it('renders correctly', () => { 9 | const tree = renderer.create( 10 | 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/index.ios.js: -------------------------------------------------------------------------------- 1 | import 'react-native'; 2 | import React from 'react'; 3 | import Index from '../index.ios.js'; 4 | 5 | // Note: test renderer must be required after react-native. 6 | import renderer from 'react-test-renderer'; 7 | 8 | it('renders correctly', () => { 9 | const tree = renderer.create( 10 | 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | lib_deps = [] 12 | 13 | for jarfile in glob(['libs/*.jar']): 14 | name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')] 15 | lib_deps.append(':' + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | 21 | for aarfile in glob(['libs/*.aar']): 22 | name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')] 23 | lib_deps.append(':' + name) 24 | android_prebuilt_aar( 25 | name = name, 26 | aar = aarfile, 27 | ) 28 | 29 | android_library( 30 | name = "all-libs", 31 | exported_deps = lib_deps, 32 | ) 33 | 34 | android_library( 35 | name = "app-code", 36 | srcs = glob([ 37 | "src/main/java/**/*.java", 38 | ]), 39 | deps = [ 40 | ":all-libs", 41 | ":build_config", 42 | ":res", 43 | ], 44 | ) 45 | 46 | android_build_config( 47 | name = "build_config", 48 | package = "com.gitbookreader", 49 | ) 50 | 51 | android_resource( 52 | name = "res", 53 | package = "com.gitbookreader", 54 | res = "src/main/res", 55 | ) 56 | 57 | android_binary( 58 | name = "app", 59 | keystore = "//android/keystores:debug", 60 | manifest = "src/main/AndroidManifest.xml", 61 | package_type = "debug", 62 | deps = [ 63 | ":app-code", 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation 19 | * entryFile: "index.android.js", 20 | * 21 | * // whether to bundle JS and assets in debug mode 22 | * bundleInDebug: false, 23 | * 24 | * // whether to bundle JS and assets in release mode 25 | * bundleInRelease: true, 26 | * 27 | * // whether to bundle JS and assets in another build variant (if configured). 28 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 29 | * // The configuration property can be in the following formats 30 | * // 'bundleIn${productFlavor}${buildType}' 31 | * // 'bundleIn${buildType}' 32 | * // bundleInFreeDebug: true, 33 | * // bundleInPaidRelease: true, 34 | * // bundleInBeta: true, 35 | * 36 | * // the root of your project, i.e. where "package.json" lives 37 | * root: "../../", 38 | * 39 | * // where to put the JS bundle asset in debug mode 40 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 41 | * 42 | * // where to put the JS bundle asset in release mode 43 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 44 | * 45 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 46 | * // require('./image.png')), in debug mode 47 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 48 | * 49 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 50 | * // require('./image.png')), in release mode 51 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 52 | * 53 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 54 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 55 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 56 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 57 | * // for example, you might want to remove it from here. 58 | * inputExcludes: ["android/**", "ios/**"], 59 | * 60 | * // override which node gets called and with what additional arguments 61 | * nodeExecutableAndArgs: ["node"], 62 | * 63 | * // supply additional arguments to the packager 64 | * extraPackagerArgs: [] 65 | * ] 66 | */ 67 | 68 | apply from: "../../node_modules/react-native/react.gradle" 69 | 70 | /** 71 | * Set this to true to create two separate APKs instead of one: 72 | * - An APK that only works on ARM devices 73 | * - An APK that only works on x86 devices 74 | * The advantage is the size of the APK is reduced by about 4MB. 75 | * Upload all the APKs to the Play Store and people will download 76 | * the correct one based on the CPU architecture of their device. 77 | */ 78 | def enableSeparateBuildPerCPUArchitecture = false 79 | 80 | /** 81 | * Run Proguard to shrink the Java bytecode in release builds. 82 | */ 83 | def enableProguardInReleaseBuilds = false 84 | 85 | android { 86 | signingConfigs { 87 | rn { 88 | keyAlias 'videohistory' 89 | keyPassword '123456' 90 | storeFile file('../keystores/rn.jks') 91 | storePassword '123456' 92 | } 93 | } 94 | 95 | compileSdkVersion 24 96 | buildToolsVersion "24.0.1" 97 | 98 | defaultConfig { 99 | applicationId "com.gitbookreader" 100 | minSdkVersion 16 101 | targetSdkVersion 22 102 | versionCode 1 103 | versionName "1.0" 104 | ndk { 105 | abiFilters "armeabi-v7a", "x86" 106 | } 107 | } 108 | splits { 109 | abi { 110 | reset() 111 | enable enableSeparateBuildPerCPUArchitecture 112 | universalApk false // If true, also generate a universal APK 113 | include "armeabi-v7a", "x86" 114 | } 115 | } 116 | buildTypes { 117 | release { 118 | minifyEnabled enableProguardInReleaseBuilds 119 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 120 | signingConfig signingConfigs.rn 121 | applicationVariants.all { variant -> 122 | renameApk(variant) 123 | } 124 | } 125 | } 126 | // applicationVariants are e.g. debug, release 127 | applicationVariants.all { variant -> 128 | variant.outputs.each { output -> 129 | // For each separate APK per architecture, set a unique version code as described here: 130 | // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits 131 | def versionCodes = ["armeabi-v7a":1, "x86":2] 132 | def abi = output.getFilter(OutputFile.ABI) 133 | if (abi != null) { // null for the universal-debug, universal-release variants 134 | output.versionCodeOverride = 135 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 136 | } 137 | } 138 | } 139 | } 140 | 141 | dependencies { 142 | compile project(':react-native-camera') 143 | compile project(':react-native-interactable') 144 | compile project(':realm') 145 | compile project(':react-native-fs') 146 | compile fileTree(dir: "libs", include: ["*.jar"]) 147 | compile 'com.folioreader:folioreader:0.2.5' 148 | compile "com.android.support:appcompat-v7:24.3.1" 149 | compile "com.facebook.react:react-native:+" // From node_modules 150 | compile project(':react-native-navigation') 151 | } 152 | 153 | // Run this once to be able to run the application with BUCK 154 | // puts all compile dependencies into folder libs for BUCK to use 155 | task copyDownloadableDepsToLibs(type: Copy) { 156 | from configurations.compile 157 | into 'libs' 158 | } 159 | 160 | // 输出apk 161 | def renameApk(variant) { 162 | variant.outputs.each { output -> 163 | def fileName 164 | def outputPath 165 | def outputFile = output.packageApplication.outputFile 166 | 167 | if (outputFile != null && outputFile.name.endsWith('.apk')) { 168 | if (variant.buildType.name.equals('release')) { 169 | outputPath = "$rootDir/../target/" 170 | fileName = "gitbookreader.apk" 171 | } else if (variant.buildType.name.equals('debug')) { 172 | outputPath = "$rootDir/../target/" 173 | fileName = "gitbookreader_debug.apk" 174 | } 175 | output.packageApplication.outputFile = new File(outputPath, fileName) 176 | } 177 | } 178 | } -------------------------------------------------------------------------------- /android/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 /usr/local/Cellar/android-sdk/24.3.3/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 | # Disabling obfuscation is useful if you collect stack traces from production crashes 20 | # (unless you are using a system that supports de-obfuscate the stack traces). 21 | -dontobfuscate 22 | 23 | # React Native 24 | 25 | # Keep our interfaces so they can be used by other ProGuard rules. 26 | # See http://sourceforge.net/p/proguard/bugs/466/ 27 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip 28 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters 29 | -keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip 30 | 31 | # Do not strip any method/class that is annotated with @DoNotStrip 32 | -keep @com.facebook.proguard.annotations.DoNotStrip class * 33 | -keep @com.facebook.common.internal.DoNotStrip class * 34 | -keepclassmembers class * { 35 | @com.facebook.proguard.annotations.DoNotStrip *; 36 | @com.facebook.common.internal.DoNotStrip *; 37 | } 38 | 39 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { 40 | void set*(***); 41 | *** get*(); 42 | } 43 | 44 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } 45 | -keep class * extends com.facebook.react.bridge.NativeModule { *; } 46 | -keepclassmembers,includedescriptorclasses class * { native ; } 47 | -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } 48 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } 49 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } 50 | 51 | -dontwarn com.facebook.react.** 52 | 53 | # TextLayoutBuilder uses a non-public Android constructor within StaticLayout. 54 | # See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details. 55 | -dontwarn android.text.StaticLayout 56 | 57 | # okhttp 58 | 59 | -keepattributes Signature 60 | -keepattributes *Annotation* 61 | -keep class okhttp3.** { *; } 62 | -keep interface okhttp3.** { *; } 63 | -dontwarn okhttp3.** 64 | 65 | # okio 66 | 67 | -keep class sun.misc.Unsafe { *; } 68 | -dontwarn java.nio.file.* 69 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 70 | -dontwarn okio.** 71 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/assets/index.android.bundle.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/assets/index.android.bundle.meta -------------------------------------------------------------------------------- /android/app/src/main/java/com/gitbookreader/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.gitbookreader; 2 | 3 | import android.graphics.Color; 4 | import android.util.TypedValue; 5 | import android.view.Gravity; 6 | import android.widget.ImageView; 7 | import android.widget.LinearLayout; 8 | import android.widget.TextView; 9 | 10 | import com.reactnativenavigation.controllers.SplashActivity; 11 | 12 | public class MainActivity extends SplashActivity { 13 | @Override 14 | public LinearLayout createSplashLayout() { 15 | LinearLayout view = new LinearLayout(this); 16 | ImageView imageView = new ImageView(this); 17 | 18 | view.setBackgroundColor(Color.parseColor("#ffffff")); 19 | view.setGravity(Gravity.CENTER); 20 | 21 | imageView.setBackgroundResource(R.drawable.ic_logo); 22 | 23 | view.addView(imageView); 24 | return view; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/gitbookreader/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.gitbookreader; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.reactnativenavigation.NavigationApplication; 5 | import com.rnfs.RNFSPackage; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import com.wix.interactable.Interactable; 10 | import com.lwansbrough.RCTCamera.RCTCameraPackage; 11 | 12 | import io.realm.react.RealmReactPackage; 13 | 14 | public class MainApplication extends NavigationApplication { 15 | 16 | @Override 17 | public boolean isDebug() { 18 | return BuildConfig.DEBUG; 19 | } 20 | 21 | protected List getPackages() { 22 | return Arrays.asList( 23 | new MyPackage(), 24 | new RNFSPackage(), 25 | new RealmReactPackage(), 26 | new Interactable(), 27 | new RCTCameraPackage() 28 | ); 29 | } 30 | 31 | @Override 32 | public List createAdditionalReactPackages() { 33 | return getPackages(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/gitbookreader/MyNativeModule.java: -------------------------------------------------------------------------------- 1 | package com.gitbookreader; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.util.Log; 6 | 7 | import com.facebook.react.bridge.Promise; 8 | import com.facebook.react.bridge.ReactApplicationContext; 9 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 10 | import com.facebook.react.bridge.ReactMethod; 11 | import com.folioreader.activity.FolioActivity; 12 | 13 | import java.io.File; 14 | import java.io.FileInputStream; 15 | import java.io.FileOutputStream; 16 | import java.io.IOException; 17 | import java.util.zip.ZipEntry; 18 | import java.util.zip.ZipInputStream; 19 | 20 | public class MyNativeModule extends ReactContextBaseJavaModule { 21 | private Context mContext; 22 | 23 | public MyNativeModule(ReactApplicationContext reactContext) { 24 | super(reactContext); 25 | mContext = reactContext; 26 | } 27 | 28 | @Override 29 | public String getName() { 30 | return "MyNativeModule"; 31 | } 32 | 33 | @ReactMethod 34 | public void startFolioReader(String path, Promise promise) { 35 | Log.d("le0zh", "start read " + path); 36 | Intent intent = new Intent(mContext, FolioActivity.class); 37 | intent.putExtra(FolioActivity.INTENT_EPUB_SOURCE_TYPE, FolioActivity.EpubSourceType.SD_CARD); 38 | intent.putExtra(FolioActivity.INTENT_EPUB_SOURCE_PATH, path); 39 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 40 | mContext.startActivity(intent); 41 | } 42 | 43 | @ReactMethod 44 | public void unZipFile(String zipFileString, String outPathString, Promise promise){ 45 | try{ 46 | unzip(zipFileString, outPathString); 47 | promise.resolve(true); 48 | }catch (Exception e){ 49 | promise.reject(e.getMessage()); 50 | } 51 | } 52 | 53 | /** 54 | * unzip to dir 55 | */ 56 | private void unzip(String zipFileString, String outPathString) throws IOException { 57 | ZipInputStream inZip = null; 58 | try { 59 | inZip = new ZipInputStream(new FileInputStream(zipFileString)); 60 | ZipEntry zipEntry; 61 | String szName; 62 | while ((zipEntry = inZip.getNextEntry()) != null) { 63 | szName = zipEntry.getName().replaceAll(" ", ""); 64 | // MyLogger.d(TAG, "---szName>>" + szName); 65 | if (zipEntry.isDirectory()) { 66 | // MyLogger.d(TAG, " ---zipEntry.isDirectory()---"); 67 | szName = szName.substring(0, szName.length() - 1); 68 | File folder = new File(outPathString + File.separator + szName); 69 | boolean mkb = folder.mkdirs(); 70 | } else { 71 | String[] tmpNames = null; 72 | if (szName.lastIndexOf("/") > 0) { 73 | tmpNames = szName.split("/"); 74 | } 75 | if (tmpNames != null) { 76 | String tmpFolder = tmpNames[0]; 77 | File folder = new java.io.File(outPathString + File.separator + tmpFolder); 78 | if (!folder.exists()) { 79 | folder.mkdirs(); 80 | } 81 | } 82 | 83 | File file = new File(outPathString + File.separator + szName); 84 | 85 | boolean cf = file.createNewFile(); 86 | FileOutputStream out = null; 87 | try { 88 | out = new FileOutputStream(file); 89 | int len; 90 | byte[] buffer = new byte[1024]; 91 | while ((len = inZip.read(buffer)) != -1) { 92 | out.write(buffer, 0, len); 93 | out.flush(); 94 | } 95 | } catch (Exception e) { 96 | // MyLogger.e(TAG, e.getMessage()); 97 | } finally { 98 | if (out != null) { 99 | out.close(); 100 | } 101 | } 102 | } 103 | } 104 | } catch (Exception e) { 105 | // MyLogger.e(TAG, e.getMessage()); 106 | } finally { 107 | if (inZip != null) { 108 | inZip.close(); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/gitbookreader/MyPackage.java: -------------------------------------------------------------------------------- 1 | package com.gitbookreader; 2 | 3 | 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.bridge.JavaScriptModule; 6 | import com.facebook.react.bridge.NativeModule; 7 | import com.facebook.react.bridge.ReactApplicationContext; 8 | import com.facebook.react.uimanager.ViewManager; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | public class MyPackage implements ReactPackage { 15 | @Override 16 | public List createNativeModules(ReactApplicationContext reactContext) { 17 | List modules = new ArrayList<>(); 18 | modules.add(new MyNativeModule(reactContext)); 19 | return modules; 20 | } 21 | 22 | @Override 23 | public List> createJSModules() { 24 | return Collections.emptyList(); // 暂时不用,返回空的list 25 | } 26 | 27 | @Override 28 | public List createViewManagers(ReactApplicationContext reactContext) { 29 | return Collections.emptyList(); // 暂时不用,返回空的list 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/ic_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_book.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_defaultcover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_defaultcover.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_delete.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_download.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_feed.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_history.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_more.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_scan.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_search.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_star.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/src_img_toc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/drawable-mdpi/src_img_toc.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GitBook 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.3' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | mavenLocal() 18 | jcenter() 19 | maven { 20 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 21 | url "$rootDir/../node_modules/react-native/android" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/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 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useDeprecatedNdk=true 21 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 6 | -------------------------------------------------------------------------------- /android/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 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /android/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 | -------------------------------------------------------------------------------- /android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = 'debug', 3 | store = 'debug.keystore', 4 | properties = 'debug.keystore.properties', 5 | visibility = [ 6 | 'PUBLIC', 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /android/keystores/rn.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/android/keystores/rn.jks -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'gitbookreader' 2 | include ':react-native-camera' 3 | project(':react-native-camera').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-camera/android') 4 | include ':react-native-interactable' 5 | project(':react-native-interactable').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-interactable/android') 6 | include ':realm' 7 | project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android') 8 | include ':react-native-fs' 9 | project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android') 10 | 11 | include ':app' 12 | include ':react-native-navigation' 13 | project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/') 14 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitbookreader", 3 | "displayName": "gitbookreader" 4 | } -------------------------------------------------------------------------------- /docs/qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/docs/qr.png -------------------------------------------------------------------------------- /docs/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/docs/screen1.png -------------------------------------------------------------------------------- /docs/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/docs/screen2.png -------------------------------------------------------------------------------- /docs/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/docs/screen3.png -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | require('./src/App'); -------------------------------------------------------------------------------- /index.ios.js: -------------------------------------------------------------------------------- 1 | require('./src/App'); -------------------------------------------------------------------------------- /ios/MyNativeModule.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef MyNativeModule_h 3 | #define MyNativeModule_h 4 | 5 | #import 6 | 7 | @interface MyNativeModule : NSObject 8 | @end 9 | 10 | #endif /* MyNativeModule_h */ 11 | -------------------------------------------------------------------------------- /ios/MyNativeModule.m: -------------------------------------------------------------------------------- 1 | #import "MyNativeModule.h" 2 | #import 3 | #import "SSZipArchive.h" 4 | 5 | @implementation MyNativeModule 6 | 7 | RCT_EXPORT_MODULE(); 8 | 9 | RCT_EXPORT_METHOD(unZipFile:(NSString *)zipFileString outPathString:(NSString *)outPathString findEventsWithResolver:(RCTPromiseResolveBlock)resolve 10 | rejecter:(RCTPromiseRejectBlock)reject) 11 | { 12 | [SSZipArchive unzipFileAtPath:zipFileString toDestination: outPathString]; 13 | resolve(@"done"); 14 | } 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '9.0' 3 | 4 | target 'gitbookreader' do 5 | # Uncomment the next line if you're using Swift or would like to use dynamic frameworks 6 | # use_frameworks! 7 | 8 | # Pods for gitbookreader 9 | 10 | pod 'SSZipArchive', '~> 1.8' 11 | 12 | target 'gitbookreader-tvOSTests' do 13 | inherit! :search_paths 14 | # Pods for testing 15 | end 16 | 17 | target 'gitbookreaderTests' do 18 | inherit! :search_paths 19 | # Pods for testing 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SSZipArchive (1.8.1) 3 | 4 | DEPENDENCIES: 5 | - SSZipArchive (~> 1.8) 6 | 7 | SPEC CHECKSUMS: 8 | SSZipArchive: 04547dfa448be5ed7ecbaf7eaf8a6e9eb9b42997 9 | 10 | PODFILE CHECKSUM: 70d6477b5551a7e606771c12127ba7105c6aba2e 11 | 12 | COCOAPODS: 1.2.1 13 | -------------------------------------------------------------------------------- /ios/gitbookreader-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSLocationWhenInUseUsageDescription 40 | 41 | NSAppTransportSecurity 42 | 43 | 44 | NSExceptionDomains 45 | 46 | localhost 47 | 48 | NSExceptionAllowsInsecureHTTPLoads 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /ios/gitbookreader-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/gitbookreader.xcodeproj/xcshareddata/xcschemes/gitbookreader-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /ios/gitbookreader.xcodeproj/xcshareddata/xcschemes/gitbookreader.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /ios/gitbookreader.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/gitbookreader/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @interface AppDelegate : UIResponder 13 | 14 | @property (nonatomic, strong) UIWindow *window; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /ios/gitbookreader/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "AppDelegate.h" 11 | #import "RCCManager.h" 12 | 13 | #import 14 | #import 15 | 16 | @implementation AppDelegate 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 19 | { 20 | NSURL *jsCodeLocation; 21 | 22 | jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; 23 | 24 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 25 | self.window.backgroundColor = [UIColor whiteColor]; 26 | [[RCCManager sharedInstance] initBridgeWithBundleURL:jsCodeLocation launchOptions:launchOptions]; 27 | 28 | /*RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation 29 | moduleName:@"gitbookreader" 30 | initialProperties:nil 31 | launchOptions:launchOptions]; 32 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 33 | 34 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 35 | UIViewController *rootViewController = [UIViewController new]; 36 | rootViewController.view = rootView; 37 | self.window.rootViewController = rootViewController; 38 | [self.window makeKeyAndVisible];*/ 39 | 40 | return YES; 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /ios/gitbookreader/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ios/gitbookreader/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /ios/gitbookreader/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | gitbookreader 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | localhost 32 | 33 | NSExceptionAllowsInsecureHTTPLoads 34 | 35 | 36 | 37 | 38 | NSCameraUsageDescription 39 | 40 | NSLocationWhenInUseUsageDescription 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /ios/gitbookreader/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #import "AppDelegate.h" 13 | 14 | int main(int argc, char * argv[]) { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ios/gitbookreaderTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/gitbookreaderTests/gitbookreaderTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import 12 | 13 | #import 14 | #import 15 | 16 | #define TIMEOUT_SECONDS 600 17 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 18 | 19 | @interface gitbookreaderTests : XCTestCase 20 | 21 | @end 22 | 23 | @implementation gitbookreaderTests 24 | 25 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 26 | { 27 | if (test(view)) { 28 | return YES; 29 | } 30 | for (UIView *subview in [view subviews]) { 31 | if ([self findSubviewInView:subview matching:test]) { 32 | return YES; 33 | } 34 | } 35 | return NO; 36 | } 37 | 38 | - (void)testRendersWelcomeScreen 39 | { 40 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 41 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 42 | BOOL foundElement = NO; 43 | 44 | __block NSString *redboxError = nil; 45 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 46 | if (level >= RCTLogLevelError) { 47 | redboxError = message; 48 | } 49 | }); 50 | 51 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 52 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 53 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 54 | 55 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 56 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 57 | return YES; 58 | } 59 | return NO; 60 | }]; 61 | } 62 | 63 | RCTSetLogFunction(RCTDefaultLogFunction); 64 | 65 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 66 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 67 | } 68 | 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true 5 | }, 6 | "exclude": [ 7 | "node_modules" 8 | ] 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gank.io", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node node_modules/react-native/local-cli/cli.js start", 7 | "test": "jest", 8 | "bundle": "react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/" 9 | }, 10 | "dependencies": { 11 | "fast-xml-parser": "^2.4.4", 12 | "flow-bin": "^0.42.0", 13 | "moment": "^2.18.1", 14 | "react": "16.0.0-alpha.6", 15 | "react-native": "0.44.2", 16 | "react-native-camera": "git+https://github.com/lwansbrough/react-native-camera.git", 17 | "react-native-fs": "^2.3.3", 18 | "react-native-interactable": "0.0.9", 19 | "react-native-navigation": "^1.1.80", 20 | "realm": "^1.3.1" 21 | }, 22 | "devDependencies": { 23 | "babel-jest": "20.0.3", 24 | "babel-preset-react-native": "1.9.2", 25 | "jest": "20.0.4", 26 | "react-test-renderer": "16.0.0-alpha.6" 27 | }, 28 | "jest": { 29 | "preset": "react-native" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Navigation } from 'react-native-navigation'; 2 | import { NativeModules } from 'react-native'; 3 | 4 | import { registerScreens } from './screens'; 5 | 6 | registerScreens(); // this is where you register all of your app's screens 7 | 8 | // 内置对象扩展 9 | String.prototype.replaceAll = function(search, replacement) { 10 | var target = this; 11 | return target.replace(new RegExp(search, 'g'), replacement); 12 | }; 13 | 14 | // start the app 15 | Navigation.startTabBasedApp({ 16 | tabs: [ 17 | { 18 | label: 'Explore', 19 | screen: 'app.Home', // this is a registered name for a screen 20 | icon: require('./img/book.png'), 21 | selectedIcon: require('./img/book.png'), // iOS only 22 | title: 'Explore', 23 | }, 24 | { 25 | label: 'Search', 26 | screen: 'app.Search', // this is a registered name for a screen 27 | icon: require('./img/search.png'), 28 | selectedIcon: require('./img/search.png'), // iOS only 29 | title: 'Search', 30 | }, 31 | { 32 | label: 'Download', 33 | screen: 'app.Download', 34 | icon: require('./img/download.png'), 35 | selectedIcon: require('./img/download.png'), // iOS only 36 | title: 'Download', 37 | }, 38 | { 39 | label: 'More', 40 | screen: 'app.More', 41 | icon: require('./img/more.png'), 42 | selectedIcon: require('./img/more.png'), // iOS only 43 | title: 'More', 44 | }, 45 | ], 46 | appStyle: { 47 | tabBarSelectedButtonColor: '#3F51B5', // change the color of the selected tab icon and text (only selected) 48 | forceTitlesDisplay: true, // Android only. If true - Show all bottom tab labels. If false - only the selected tab's label is visible. 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/Feeds.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | ActivityIndicator, 5 | Text, 6 | StyleSheet, 7 | ScrollView, 8 | StatusBar, 9 | FlatList, 10 | InteractionManager, 11 | } from 'react-native'; 12 | 13 | import { px2dp, SCREEN_WIDTH } from '../utils/'; 14 | 15 | export default class Feeds extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = { 20 | data: [], 21 | loading: false, 22 | page: 0, 23 | seed: 1, 24 | error: null, 25 | refreshing: false, 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | this._fetchData(); 31 | } 32 | 33 | removeOne = predicate => { 34 | const newData = this.state.data.filter((item, index) => { 35 | return !predicate(item); 36 | }); 37 | 38 | this.setState({ 39 | data: newData, 40 | }); 41 | }; 42 | 43 | refresh = () => { 44 | // this.flatList && this.flatList.scrollToIndex({ viewPosition: 0.5, index: 0 }); 45 | this._handleRefresh(); 46 | }; 47 | 48 | _fetchData = () => { 49 | InteractionManager.runAfterInteractions(() => { 50 | this.setState({ loading: true }); 51 | 52 | const { page } = this.state; 53 | 54 | // fetchData(page: number): {list: []} 55 | // page start from 0 56 | this.props.fetchData(page).then(res => { 57 | this.setState({ 58 | data: page === 0 ? res.list : [...this.state.data, ...res.list], 59 | loading: false, 60 | refreshing: false, 61 | }); 62 | }); 63 | }); 64 | }; 65 | 66 | _renderFooter = () => { 67 | if (!this.state.loading) return null; 68 | 69 | return ( 70 | 77 | 78 | 79 | ); 80 | }; 81 | 82 | _handleRefresh = () => { 83 | this.setState( 84 | { 85 | page: 0, 86 | refreshing: true, 87 | }, 88 | () => { 89 | this._fetchData(); 90 | } 91 | ); 92 | }; 93 | 94 | _loadMore = () => { 95 | this.setState( 96 | { 97 | page: this.state.page + 1, 98 | }, 99 | () => { 100 | this._fetchData(); 101 | } 102 | ); 103 | }; 104 | 105 | _renderItemSeparator = () => { 106 | return ; 107 | }; 108 | 109 | render() { 110 | if (!this.state.loading && this.state.data.length === 0) { 111 | return ( 112 | 113 | NO DATA 114 | 115 | ); 116 | } 117 | 118 | return ( 119 | 120 | (this.flatList = view)} 122 | data={this.state.data} 123 | keyExtractor={this.props.keyExtractor} 124 | renderItem={this.props.renderItem} 125 | ListFooterComponent={this._renderFooter} 126 | onEndReachedThreshold={30} 127 | refreshing={this.state.refreshing} 128 | onEndReached={this._loadMore} 129 | onRefresh={this._handleRefresh} 130 | getItemLayout={this.props.getItemLayout} 131 | /> 132 | 133 | ); 134 | } 135 | } 136 | 137 | const styles = StyleSheet.create({ 138 | wrapper: { 139 | flex: 1, 140 | }, 141 | 142 | noContent: { 143 | width: SCREEN_WIDTH, 144 | height: 200, 145 | justifyContent: 'center', 146 | alignItems: 'center', 147 | }, 148 | 149 | header: { 150 | height: px2dp(335), 151 | backgroundColor: '#000', 152 | }, 153 | 154 | itemSeparator: { 155 | height: px2dp(35), 156 | width: SCREEN_WIDTH, 157 | backgroundColor: '#eee', 158 | borderBottomWidth: StyleSheet.hairlineWidth, 159 | borderBottomColor: '#e9e9e9', 160 | // elevation: 5, 161 | }, 162 | }); 163 | -------------------------------------------------------------------------------- /src/components/ImageWithPlaceHolder.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import { View, StyleSheet, Image } from 'react-native'; 5 | 6 | type Props = { 7 | source: {}, 8 | placeHolderSource: number, // 本地图片,require最后会变成资源id,数字类型 9 | style: { resizeMode?: string }, 10 | }; 11 | 12 | /** 13 | * 带有占位符(默认图片)的图片容器 14 | * 只有当图片加载成功后,才显示图片,否则都显示占位符(默认图片) 15 | */ 16 | export default class ImageWithPlaceHolder extends React.PureComponent { 17 | props: Props; 18 | 19 | state: { 20 | loading: boolean, 21 | }; 22 | 23 | constructor() { 24 | super(); 25 | 26 | this.state = { 27 | loading: true, 28 | }; 29 | } 30 | 31 | onImageLoad = () => { 32 | this.setState({ 33 | loading: false, 34 | }); 35 | }; 36 | 37 | render() { 38 | const { source, placeHolderSource, style } = this.props; 39 | 40 | const flattenStyle = StyleSheet.flatten(style); 41 | const { resizeMode, ...viewStyle } = StyleSheet.flatten(style); 42 | 43 | const { 44 | borderRadius, 45 | borderTopLeftRadius, 46 | borderTopRightRadius, 47 | borderBottomLeftRadius, 48 | borderBottomRightRadius, 49 | } = flattenStyle; 50 | 51 | const imageStyle = [ 52 | styles.placeHolder, 53 | { 54 | borderRadius, 55 | borderTopLeftRadius, 56 | borderTopRightRadius, 57 | borderBottomLeftRadius, 58 | borderBottomRightRadius, 59 | }, 60 | ]; 61 | 62 | return ( 63 | 64 | 65 | {this.state.loading ? : null} 66 | 67 | ); 68 | } 69 | } 70 | 71 | const styles = StyleSheet.create({ 72 | placeHolder: { 73 | position: 'absolute', 74 | top: 0, 75 | right: 0, 76 | left: 0, 77 | bottom: 0, 78 | height: null, 79 | width: null, 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /src/components/RowWithActions.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { StyleSheet, View, Image, Text, Animated, TouchableOpacity, Dimensions, Slider } from 'react-native'; 3 | import Interactable from 'react-native-interactable'; 4 | 5 | const Screen = Dimensions.get('window'); 6 | 7 | export default class Row extends Component { 8 | constructor(props) { 9 | super(props); 10 | this._deltaX = new Animated.Value(0); 11 | } 12 | 13 | _trash = () => { 14 | this.props.onTrash && this.props.onTrash(); 15 | }; 16 | 17 | render() { 18 | return ( 19 | 20 | 21 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | {this.props.children} 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | const styles = StyleSheet.create({ 56 | button: { 57 | width: 40, 58 | height: 40, 59 | }, 60 | trashHolder: { 61 | position: 'absolute', 62 | top: 0, 63 | left: Screen.width - 78, 64 | width: Screen.width, 65 | paddingLeft: 18, 66 | backgroundColor: '#E91E63', 67 | justifyContent: 'flex-start', 68 | alignItems: 'center', 69 | flexDirection: 'row', 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /src/data/bookFiles.js: -------------------------------------------------------------------------------- 1 | import { NativeModules } from 'react-native'; 2 | import fastXmlParser from 'fast-xml-parser'; 3 | import RNFS from 'react-native-fs'; 4 | 5 | import { saveDownloadItem } from './dataBase'; 6 | 7 | const MyNativeModule = NativeModules.MyNativeModule; 8 | 9 | // 保存下载列表 10 | const downloadingBooks = new Set(); 11 | 12 | /** 13 | * 确保目标文件夹存在 14 | */ 15 | function makeSureDirExist(dir) { 16 | return new Promise((resolve, reject) => { 17 | RNFS.exists(dir).then(dirExists => { 18 | if (dirExists) { 19 | resolve(); 20 | } else { 21 | RNFS.mkdir(dir).then(() => { 22 | resolve(); 23 | }); 24 | } 25 | }); 26 | }); 27 | } 28 | 29 | /** 30 | * 执行下载流程 31 | */ 32 | function executeDownloadFlow(DIR, book) { 33 | const downlosdFileOpt = { 34 | fromUrl: book.urls.download.epub, 35 | toFile: `${DIR}/book.epub`, 36 | background: true, 37 | progressDivider: 10, 38 | progress: res => { 39 | // 下载进度 40 | console.log(res); 41 | }, 42 | }; 43 | 44 | // 下载 45 | const downloadRes = RNFS.downloadFile(downlosdFileOpt); 46 | 47 | return downloadRes.promise 48 | .then(res => { 49 | // 解压并生成目录文件 50 | unzip(book.id); 51 | 52 | // 保存下载记录 53 | saveDownloadItem(book.id, book.title, book.cover.small, res.bytesWritten); 54 | 55 | // 从下载中列表移除 56 | downloadingBooks.delete(book.id); 57 | 58 | return downloadRes; 59 | }) 60 | .catch(err => { 61 | console.warn(err.message); 62 | }); 63 | } 64 | 65 | /** 66 | * 根据bookId获得文件目录 67 | */ 68 | export function getDirFromBookId(bookId) { 69 | return `${RNFS.DocumentDirectoryPath}/download/${bookId.replace('/', '-')}`; 70 | } 71 | 72 | /** 73 | * 下载 74 | */ 75 | export function downloadAsync(book) { 76 | const DIR = getDirFromBookId(book.id); 77 | 78 | downloadingBooks.add(book.id); 79 | 80 | return makeSureDirExist(DIR).then(() => { 81 | return executeDownloadFlow(DIR, book); 82 | }); 83 | } 84 | 85 | /** 86 | * 检测指定书籍是否在下载中 87 | * @param {string} bookId 88 | */ 89 | export function checkIsDownloadingOrNot(bookId) { 90 | return downloadingBooks.has(bookId); 91 | } 92 | 93 | /** 94 | * 解压缩 95 | */ 96 | function unzip(bookId) { 97 | const DIR = getDirFromBookId(bookId); 98 | const path = `${DIR}/book.epub`; 99 | 100 | const contentDir = `${DIR}/content`; 101 | 102 | const doUnzip = () => { 103 | // 解压缩 104 | MyNativeModule.unZipFile(path, contentDir) 105 | .then(res => { 106 | console.log('unzip done', res); 107 | 108 | // 生成目录文件 109 | RNFS.readFile(`${contentDir}/toc.ncx`).then(content => { 110 | const start = content.indexOf(''); 111 | const end = content.indexOf(''); 112 | 113 | const navContent = content.substr(start, end - start + 1); 114 | 115 | var xml = fastXmlParser.parse(navContent, { 116 | ignoreNonTextNodeAttr: false, 117 | ignoreTextNodeAttr: false, 118 | attrPrefix: '_', 119 | }); 120 | 121 | const toc = xml.navMap.navPoint; 122 | 123 | RNFS.writeFile(`${DIR}/toc.json`, JSON.stringify(toc), 'utf8').then(success => { 124 | console.log(`${bookId} toc FILE WRITTEN!`); 125 | }); 126 | }); 127 | }) 128 | .catch(e => { 129 | console.warn(e); 130 | }); 131 | }; 132 | 133 | makeSureDirExist(contentDir).then(doUnzip); 134 | } 135 | 136 | // 删除书籍文件 137 | export function deleteBoookFile(bookId) { 138 | const DIR = getDirFromBookId(bookId); 139 | 140 | RNFS.unlink(DIR) 141 | .then(() => { 142 | console.log(`${bookId} FILE DELETED`); 143 | }) 144 | .catch(err => { 145 | console.log(err.message); 146 | }); 147 | } 148 | -------------------------------------------------------------------------------- /src/data/dataBase.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Realm from 'realm'; 4 | import { deleteBoookFile } from './bookFiles'; 5 | 6 | /** 7 | * 阅读记录 8 | */ 9 | const HistorySchema = { 10 | name: 'History', 11 | primaryKey: 'bookId', 12 | properties: { 13 | bookId: 'string', // 书的id 14 | title: 'string', // 书的标题 15 | cover: 'string', // 书的封面 16 | lastReadAt: 'date', // 上次阅读时间 17 | }, 18 | }; 19 | 20 | /** 21 | * 下载记录 22 | */ 23 | const DownloadSchema = { 24 | name: 'Download', 25 | primaryKey: 'bookId', 26 | properties: { 27 | bookId: 'string', // 书的id 28 | title: 'string', // 书的标题 29 | cover: 'string', // 书的封面 30 | size: 'int', // 文件大小 kb 31 | downloadAt: 'date', // 下载时间 32 | }, 33 | }; 34 | 35 | /** 36 | * 阅读进度 37 | */ 38 | const ProgressSchema = { 39 | name: 'Progress', 40 | primaryKey: 'bookId', 41 | properties: { 42 | bookId: 'string', // 书的id 43 | src: 'string', // 阅读的文件 44 | readAt: 'date', // 阅读时间 45 | position: { type: 'int', default: 0, optional: true }, // 阅读位置 46 | }, 47 | }; 48 | 49 | const realm = new Realm({ 50 | schema: [HistorySchema, DownloadSchema, ProgressSchema], 51 | schemaVersion: 2, 52 | // migration: (oldRealm, newRealm) => { 53 | // if (oldRealm.schemaVersion < 2) { 54 | 55 | // } 56 | // }, 57 | }); 58 | 59 | /** 60 | * 保存下载记录 61 | */ 62 | export function saveDownloadItem(bookId: string, title: string, cover: string, size: number) { 63 | realm.write(() => { 64 | realm.create('Download', { bookId, title, cover, size, downloadAt: new Date() }); 65 | }); 66 | } 67 | 68 | /** 69 | * 获取下载列表的分页数据 70 | * @param {number} page 页码 71 | */ 72 | export function getDownloadItems(page: number = 0) { 73 | const pageSize = 10; 74 | page = page + 1; 75 | let items = realm.objects('Download').sorted('downloadAt', true).slice((page - 1) * pageSize, pageSize); 76 | 77 | return items; 78 | } 79 | 80 | /** 81 | * 检查指定bookId是否已经被下载 82 | * @param {string} bookId 83 | */ 84 | export function checkIsDownloadOrNot(bookId: string) { 85 | let books = realm.objects('Download').filtered(`bookId = "${bookId}"`); 86 | 87 | return books.length > 0; 88 | } 89 | 90 | /** 91 | * 获取所有的下载书籍的id 92 | */ 93 | export function getAllDownloadIds() { 94 | return realm.objects('Download').map(item => item.bookId); 95 | } 96 | 97 | export function getDownloadItemsAsync(page: number = 0) { 98 | return new Promise((resolve, reject) => { 99 | try { 100 | const items = getDownloadItems(page); 101 | resolve({ 102 | list: items, 103 | }); 104 | } catch (e) { 105 | reject(e); 106 | } 107 | }); 108 | } 109 | 110 | export function deleteOneDownloadItem(bookId: string) { 111 | let books = realm.objects('Download').filtered(`bookId = "${bookId}"`); 112 | 113 | if (books.length > 0) { 114 | realm.write(() => { 115 | realm.delete(books[0]); 116 | }); 117 | } 118 | 119 | deleteBoookFile(bookId); 120 | } 121 | 122 | /** 123 | * 保存阅读进度 124 | */ 125 | export function saveReadProgress(bookId: string, src: string, pos: number) { 126 | realm.write(() => { 127 | // 第三个参数true表示,当前主键已经存在时,直接更新, 否则创建 128 | realm.create('Progress', { bookId, src, readAt: new Date(), position: pos }, true); 129 | }); 130 | } 131 | 132 | /** 133 | * 获取阅读进度,当前只是文件级别 134 | * @param {string} bookId 135 | */ 136 | export function getReadProgress(bookId: string): { src: string, position: number } { 137 | let books = realm.objects('Progress').filtered(`bookId = "${bookId}"`); 138 | 139 | if (books.length > 0) { 140 | return { 141 | src: books[0].src, 142 | position: books[0].position, 143 | }; 144 | } 145 | 146 | // 没有阅读进度,返回首页 147 | return { 148 | src: 'index.html', 149 | position: -100, 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /src/data/index.js: -------------------------------------------------------------------------------- 1 | import { NativeModules, AsyncStorage } from 'react-native'; 2 | 3 | exports.get = function get(api, params) { 4 | const opt = { 5 | method: 'GET', 6 | }; 7 | 8 | return fetch(api, opt) 9 | .then(response => { 10 | // 如果请求的返回结果不是OK则返回空数组,UI显示无数据 11 | if (response.status !== 200) { 12 | return []; 13 | } 14 | 15 | return response.json(); 16 | }) 17 | .catch(e => { 18 | console.log(`[API][error] ${e}`); 19 | }); 20 | }; 21 | 22 | export function getAllBooks(page) { 23 | return fetch(`https://api.gitbook.com/books/all?limit=10&page=${page}`).then(res => res.json()); 24 | } 25 | 26 | export function getBook(id) { 27 | return fetch(`https://api.gitbook.com/book/${id}`).then(res => res.json()); 28 | } 29 | 30 | const historyKey = id => `@history#${id}`; 31 | /** 32 | * 保存阅读记录 33 | */ 34 | export function saveReadHistory(bookInfo) { 35 | // 记录额外的信息 36 | bookInfo.lastReadTime = Date.now(); 37 | 38 | console.log('save history', historyKey(bookInfo.id)); 39 | 40 | AsyncStorage.setItem(historyKey(bookInfo.id), JSON.stringify(bookInfo)); 41 | } 42 | 43 | /** 44 | * 读取阅读记录 45 | */ 46 | export function getReadHisotry() { 47 | console.log('get historys'); 48 | 49 | return AsyncStorage.getAllKeys().then(keys => { 50 | const historyKeys = []; 51 | keys.forEach(key => { 52 | if (key.indexOf('@history') === 0) { 53 | historyKeys.push(key); 54 | } 55 | }); 56 | 57 | return AsyncStorage.multiGet(historyKeys).then(res => { 58 | return res.map(item => JSON.parse(item[1])); 59 | }); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /src/data/search.js: -------------------------------------------------------------------------------- 1 | export function search(keyword, page = 0) { 2 | return fetch(`https://www.gitbook.com/search?q=${keyword}&page=${page}&type=books&sort=stars`).then(res => { 3 | const html = res._bodyInit; 4 | const books = parseHtml(html); 5 | 6 | return { 7 | list: books, 8 | }; 9 | }); 10 | } 11 | 12 | function parseHtml(html) { 13 | const reg = new RegExp('
', 'g'); 14 | 15 | const result = html.match(reg); 16 | 17 | if (result === null) { 18 | return []; 19 | } 20 | 21 | const books = result.map(div => parseBook(div)); 22 | return books; 23 | } 24 | 25 | function parseBook(fragment) { 26 | const reg = new RegExp('React 入门教 29 | 30 | const id = result.substring(0, result.indexOf('"')).replace('https://www.gitbook.com/book/', ''); 31 | const title = result.substring(result.indexOf('>') + 1); 32 | 33 | const infoReg = new RegExp('

', 'g'); 34 | const descResult = infoReg.exec(fragment); 35 | let desc = ''; 36 | 37 | if (descResult != null) { 38 | desc = descResult[1].substring(descResult[1].indexOf('>') + 1); 39 | } 40 | 41 | return { 42 | id, 43 | title, 44 | desc, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/img/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/book.png -------------------------------------------------------------------------------- /src/img/default-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/default-cover.png -------------------------------------------------------------------------------- /src/img/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/delete.png -------------------------------------------------------------------------------- /src/img/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/download.png -------------------------------------------------------------------------------- /src/img/feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/feed.png -------------------------------------------------------------------------------- /src/img/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/history.png -------------------------------------------------------------------------------- /src/img/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/ic_share.png -------------------------------------------------------------------------------- /src/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/logo.png -------------------------------------------------------------------------------- /src/img/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/more.png -------------------------------------------------------------------------------- /src/img/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/scan.png -------------------------------------------------------------------------------- /src/img/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/search.png -------------------------------------------------------------------------------- /src/img/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/star.png -------------------------------------------------------------------------------- /src/img/time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/src/img/time.png -------------------------------------------------------------------------------- /src/screens/BookDetail/Badge.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, ScrollView, View, Text, StyleSheet, TouchableNativeFeedback } from 'react-native'; 3 | import moment from 'moment'; 4 | 5 | import { px2dp, SCREEN_WIDTH } from '../../utils'; 6 | 7 | export default class BookDetail extends React.PureComponent { 8 | render() { 9 | const { icon, title, value } = this.props; 10 | 11 | return ( 12 | 13 | 14 | 15 | {title} 16 | 17 | 18 | 19 | {value} 20 | 21 | 22 | ); 23 | } 24 | } 25 | 26 | const styles = StyleSheet.create({ 27 | label: { 28 | height: px2dp(60), 29 | borderWidth: 1, 30 | borderColor: '#e8eaed', 31 | flexDirection: 'row', 32 | justifyContent: 'flex-start', 33 | alignItems: 'center', 34 | marginRight: 10, 35 | }, 36 | 37 | keyWrapper: { 38 | flexDirection: 'row', 39 | justifyContent: 'center', 40 | alignItems: 'center', 41 | backgroundColor: '#f6f7f8', 42 | paddingLeft: 5, 43 | paddingRight: 5, 44 | }, 45 | 46 | valueWrapper: { 47 | backgroundColor: '#fff', 48 | paddingLeft: 5, 49 | paddingRight: 5, 50 | }, 51 | 52 | icon: { 53 | height: 16, 54 | width: 16, 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /src/screens/BookDetail/CoverHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, ScrollView, View, Text, StyleSheet, TouchableNativeFeedback } from 'react-native'; 3 | 4 | import moment from 'moment'; 5 | import { Navigation } from 'react-native-navigation'; 6 | 7 | import { px2dp, SCREEN_WIDTH, getBookCover } from '../../utils'; 8 | import ImageWithPlaceHolder from '../../components/ImageWithPlaceHolder'; 9 | 10 | export default class CoverHeader extends React.PureComponent { 11 | render() { 12 | //const cover = '/cover/book/frontendmasters/front-end-handbook.jpg?build=1490223158266'; 13 | const cover = getBookCover(); 14 | 15 | return ( 16 | 17 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | const styles = StyleSheet.create({ 28 | coverWrapper: { 29 | width: SCREEN_WIDTH, 30 | height: px2dp(800), 31 | backgroundColor: '#fff', 32 | justifyContent: 'center', 33 | alignItems: 'center', 34 | }, 35 | 36 | coverImage: { 37 | width: px2dp(474), 38 | height: px2dp(622), 39 | borderWidth: 2, 40 | borderColor: '#eee', 41 | elevation: 10, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/screens/BookDetail/Readme.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, ScrollView, View, Text, StyleSheet, WebView, ActivityIndicator } from 'react-native'; 3 | import moment from 'moment'; 4 | 5 | import { px2dp, SCREEN_WIDTH } from '../../utils'; 6 | import { get } from '../../data'; 7 | 8 | const injectedScript = function() { 9 | var wrapper = document.createElement('div'); 10 | wrapper.id = 'height-wrapper'; 11 | while (document.body.firstChild) { 12 | wrapper.appendChild(document.body.firstChild); 13 | } 14 | document.body.appendChild(wrapper); 15 | 16 | function waitForBridge() { 17 | if (window.postMessage.length !== 1) { 18 | setTimeout(waitForBridge, 200); 19 | } else { 20 | let height = wrapper.clientHeight; 21 | postMessage(height); 22 | } 23 | } 24 | waitForBridge(); 25 | 26 | window.addEventListener('load', function() { 27 | waitForBridge(); 28 | setTimeout(waitForBridge, 1000); 29 | }); 30 | 31 | window.addEventListener('resize', waitForBridge); 32 | }; 33 | 34 | export default class ReadMe extends React.PureComponent { 35 | constructor(props) { 36 | super(props); 37 | 38 | this.state = { 39 | ready: false, 40 | content: '', 41 | webViewHeight: 300, 42 | }; 43 | 44 | this._isMounted = false; 45 | } 46 | 47 | componentDidMount() { 48 | get(`https://api.gitbook.com/book/${this.props.id}/contents/README.json`).then(res => { 49 | this.setState({ 50 | ready: true, 51 | content: res.sections[0].content.replaceAll('', '/span>'), 52 | }); 53 | }); 54 | 55 | this._isMounted = true; 56 | } 57 | 58 | componentWillUnmount() { 59 | this._isMounted = false; 60 | } 61 | 62 | _onMessage = e => { 63 | const newHeight = parseInt(e.nativeEvent.data, 10) + 100; 64 | if (this._isMounted && this.state.webViewHeight !== newHeight) { 65 | this.setState({ 66 | webViewHeight: newHeight, 67 | }); 68 | } 69 | }; 70 | 71 | // webView自适应高度参考: 72 | // https://github.com/scazzy/react-native-webview-autoheight 73 | // https://gist.github.com/epeli/10c77c1710dd137a1335 74 | 75 | render() { 76 | if (!this.state.ready) { 77 | return ; 78 | } 79 | 80 | const htmlStr = ` 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 100 | 101 | 102 | ${this.state.content} 103 | 104 | 105 | `; 106 | 107 | const webViewSource = { html: htmlStr }; 108 | 109 | return ( 110 | 111 | 124 | 125 | ); 126 | } 127 | } 128 | 129 | const styles = StyleSheet.create({ 130 | wrapper: { 131 | flex: 1, 132 | paddingLeft: 15, 133 | paddingRight: 15, 134 | backgroundColor: '#fff', 135 | }, 136 | }); 137 | -------------------------------------------------------------------------------- /src/screens/BookDetail/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AlertIOS, 4 | Image, 5 | ScrollView, 6 | View, 7 | Platform, 8 | Text, 9 | StyleSheet, 10 | TouchableHighlight, 11 | ActivityIndicator, 12 | } from 'react-native'; 13 | 14 | import moment from 'moment'; 15 | import RNFS from 'react-native-fs'; 16 | 17 | import { px2dp, SCREEN_WIDTH } from '../../utils'; 18 | import ImageWithPlaceHolder from '../../components/ImageWithPlaceHolder'; 19 | import Badge from './Badge'; 20 | import Readme from './Readme'; 21 | import { getBook } from '../../data'; 22 | import { checkIsDownloadOrNot } from '../../data/dataBase'; 23 | import { downloadAsync, checkIsDownloadingOrNot } from '../../data/bookFiles'; 24 | 25 | export default class BookDetail extends React.PureComponent { 26 | static navigatorStyle = { 27 | navBarHideOnScroll: true, 28 | navBarBackgroundColor: '#3F51B5', // This will be the TitleBars color when the react view is hidden and collapsed 29 | collapsingToolBarComponent: 'app.BookDetail.Cover', 30 | navBarTranslucent: true, // Optional, sets a translucent dark background to the TitleBar. Useful when displaying bright colored header to emphasize the title and buttons in the TitleBar 31 | showTitleWhenExpended: false, // default: true. Show the screens title only when the toolbar is collapsed 32 | navBarTextColor: '#fff', 33 | navBarTitleTextCentered: true, 34 | statusBarColor: '#3F51B5', 35 | navBarButtonColor: '#fff', 36 | tabBarHidden: false, 37 | topBarElevationShadowEnabled: true, 38 | }; 39 | 40 | constructor(props) { 41 | super(props); 42 | 43 | this.state = { 44 | downloading: false, 45 | downloaded: false, 46 | book: props.book, 47 | }; 48 | 49 | this._isMounted = false; 50 | } 51 | 52 | componentDidMount() { 53 | this._isMounted = true; 54 | 55 | if (this.props.fromSearch) { 56 | // 如果是从搜索页面来的,则只有book id,需要再获取一下书的详细信息 57 | getBook(this.props.id).then(res => { 58 | this.setState({ book: res }); 59 | this.props.navigator.setTitle({ 60 | title: res.title, 61 | }); 62 | }); 63 | } 64 | } 65 | 66 | componentWillUnmount() { 67 | this._isMounted = false; 68 | } 69 | 70 | _readOnline = () => { 71 | this.props.navigator.push({ 72 | screen: 'app.OnlineReader', // unique ID registered with Navigation.registerScreen 73 | // title: item.title, // navigation bar title of the pushed screen (optional) 74 | // titleImage: require('../../img/ic_news.png'), //navigation bar title image instead of the title text of the pushed screen (optional) 75 | passProps: { 76 | url: this.state.book.urls.read, 77 | }, // Object that will be passed as props to the pushed screen (optional) 78 | animated: true, // does the push have transition animation or does it happen immediately (optional) 79 | backButtonTitle: '返回', // override the back button title (optional) 80 | backButtonHidden: false, // hide the back button altogether (optional) 81 | }); 82 | 83 | // 保存阅读记录 84 | // saveReadHistory(this.props.book); 85 | }; 86 | 87 | _readLocal = () => { 88 | const { book } = this.state; 89 | 90 | this.props.navigator.push({ 91 | screen: 'app.Reader', // unique ID registered with Navigation.registerScreen 92 | title: book.title, 93 | passProps: { 94 | bookId: book.id, 95 | }, // Object that will be passed as props to the pushed screen (optional) 96 | animated: true, // does the push have transition animation or does it happen immediately (optional) 97 | }); 98 | 99 | // 调用FolioReader 100 | // MyNativeModule.startFolioReader(path); 101 | }; 102 | 103 | _download = () => { 104 | if (this.state.downloading) { 105 | if (Platform.OS === 'ios') { 106 | AlertIOS.alert('message', downloading); 107 | } else if (Platform.OS === 'android') { 108 | this.props.navigator.showSnackbar({ 109 | text: 'downloading', 110 | }); 111 | } 112 | 113 | return; 114 | } 115 | 116 | if (Platform.OS === 'android') { 117 | this.props.navigator.showSnackbar({ 118 | text: 'Start to download', 119 | }); 120 | } 121 | 122 | const { book } = this.state; 123 | 124 | this.setState({ 125 | downloading: true, 126 | }); 127 | 128 | downloadAsync(book).then(res => { 129 | if (this._isMounted) { 130 | this.setState({ 131 | downloading: false, 132 | downloaded: true, 133 | }); 134 | } 135 | }); 136 | }; 137 | 138 | _renderDownloadButton = (isDownloaded, isDownLoading) => { 139 | if (isDownloaded) { 140 | return ( 141 | 142 | 143 | Read Local 144 | 145 | 146 | ); 147 | } 148 | 149 | if (isDownLoading) { 150 | return ( 151 | 152 | Downloading 153 | 154 | ); 155 | } 156 | 157 | return ( 158 | 159 | 160 | Download 161 | 162 | 163 | ); 164 | }; 165 | 166 | render() { 167 | const { book } = this.state; 168 | 169 | if (book === null) { 170 | return ( 171 | 172 | 173 | 174 | ); 175 | } 176 | 177 | const isDownloaded = checkIsDownloadOrNot(book.id); 178 | const isDownLoading = checkIsDownloadingOrNot(book.id); 179 | 180 | return ( 181 | 182 | 183 | {book.title} 184 | Last updated {moment(book.dates.build).fromNow()} 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | Read Online 194 | 195 | 196 | {this._renderDownloadButton(isDownloaded, isDownLoading)} 197 | 198 | 199 | 200 | 201 | 202 | 203 | ); 204 | } 205 | } 206 | 207 | const styles = StyleSheet.create({ 208 | contentContainer: { 209 | flexDirection: 'column', 210 | }, 211 | 212 | coverWrapper: { 213 | width: SCREEN_WIDTH, 214 | height: px2dp(800), 215 | backgroundColor: '#fff', 216 | justifyContent: 'center', 217 | alignItems: 'center', 218 | }, 219 | 220 | coverImage: { 221 | width: px2dp(474), 222 | height: px2dp(622), 223 | borderWidth: 2, 224 | borderColor: '#eee', 225 | elevation: 10, 226 | }, 227 | 228 | title: { 229 | fontSize: px2dp(60), 230 | color: '#000', 231 | }, 232 | 233 | description: { 234 | fontSize: px2dp(40), 235 | }, 236 | 237 | infoWrapper: { 238 | flexDirection: 'row', 239 | alignItems: 'center', 240 | justifyContent: 'flex-start', 241 | paddingTop: 10, 242 | }, 243 | 244 | label: { 245 | height: px2dp(60), 246 | borderWidth: 1, 247 | borderColor: '#e8eaed', 248 | flexDirection: 'row', 249 | justifyContent: 'flex-start', 250 | alignItems: 'center', 251 | }, 252 | 253 | buttons: { 254 | flexDirection: 'row', 255 | justifyContent: 'flex-start', 256 | alignItems: 'center', 257 | }, 258 | 259 | button: { 260 | paddingTop: 10, 261 | paddingBottom: 10, 262 | paddingLeft: 20, 263 | paddingRight: 20, 264 | height: 40, 265 | alignItems: 'center', 266 | backgroundColor: '#f6f7f8', 267 | justifyContent: 'center', 268 | borderRadius: 2, 269 | borderWidth: 1, 270 | borderColor: '#e8eaed', 271 | marginRight: 10, 272 | marginTop: 10, 273 | }, 274 | 275 | buttonText: { 276 | fontSize: px2dp(40), 277 | color: '#000', 278 | }, 279 | }); 280 | -------------------------------------------------------------------------------- /src/screens/Camera/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { AppRegistry, Dimensions, StyleSheet, Text, TouchableHighlight, View } from 'react-native'; 3 | import Camera from 'react-native-camera'; 4 | 5 | export default class QRScanner extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); 10 | } 11 | 12 | onNavigatorEvent(event) { 13 | switch (event.id) { 14 | case 'willAppear': 15 | this.scanning = true; 16 | break; 17 | case 'didAppear': 18 | break; 19 | case 'willDisappear': 20 | break; 21 | case 'didDisappear': 22 | break; 23 | } 24 | } 25 | 26 | scanning = true; 27 | 28 | _onBarCodeRead = e => { 29 | if (this.scanning) { 30 | if (e.type === 'QR_CODE') { 31 | if (e.data.indexOf('www.gitbook.com') > 0) { 32 | const parts = e.data.split('/'); 33 | const author = parts[parts.length - 3]; 34 | const book = parts[parts.length - 2]; 35 | 36 | this._scanDone(author, book); 37 | } else if (e.data.indexOf('.gitbooks.io') > 0) { 38 | const parts = e.data.replace('https://', '').split('/'); 39 | const author = parts[0].replace('.gitbooks.io', ''); 40 | const book = parts[1]; 41 | 42 | this._scanDone(author, book); 43 | } 44 | } 45 | } 46 | }; 47 | 48 | _scanDone = (author, book) => { 49 | this.scanning = false; 50 | 51 | this.props.navigator.push({ 52 | screen: 'app.BookDetail', // unique ID registered with Navigation.registerScreen 53 | passProps: { 54 | id: `${author}/${book}`, 55 | book: null, 56 | fromSearch: true, 57 | }, 58 | animated: true, // does the push have transition animation or does it happen immediately (optional) 59 | }); 60 | }; 61 | 62 | static navigatorStyle = { 63 | tabBarHidden: true, 64 | navBarBackgroundColor: '#3F51B5', 65 | navBarTextColor: '#fff', 66 | statusBarColor: '#3F51B5', 67 | navBarTitleTextCentered: true, 68 | navBarButtonColor: '#fff', 69 | }; 70 | 71 | render() { 72 | return ( 73 | 74 | { 76 | this.camera = cam; 77 | }} 78 | onBarCodeRead={this._onBarCodeRead} 79 | style={styles.preview} 80 | aspect={Camera.constants.Aspect.fill} 81 | > 82 | Scan the QR code of gitbook's url to get the book 83 | 84 | 85 | ); 86 | } 87 | } 88 | 89 | const styles = StyleSheet.create({ 90 | container: { 91 | flex: 1, 92 | flexDirection: 'row', 93 | }, 94 | preview: { 95 | flex: 1, 96 | justifyContent: 'flex-end', 97 | alignItems: 'center', 98 | }, 99 | capture: { 100 | flex: 0, 101 | backgroundColor: '#ffffff80', 102 | borderRadius: 5, 103 | color: '#000', 104 | padding: 10, 105 | margin: 40, 106 | }, 107 | }); 108 | -------------------------------------------------------------------------------- /src/screens/Download/Book.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, View, Text, StyleSheet, TouchableHighlight } from 'react-native'; 3 | 4 | import moment from 'moment'; 5 | import Interactable from 'react-native-interactable'; 6 | 7 | import { px2dp, SCREEN_WIDTH } from '../../utils'; 8 | import ImageWithPlaceHolder from '../../components/ImageWithPlaceHolder'; 9 | import RowWithActions from '../../components/RowWithActions'; 10 | 11 | export default class NewsCard extends React.PureComponent { 12 | _showSize = size => { 13 | if (size > 1000) { 14 | const inKb = size / 1000; 15 | 16 | if (inKb > 1000) { 17 | const inMb = inKb / 1000; 18 | return `${inMb.toFixed(2)} MB`; 19 | } 20 | 21 | return `${inKb.toFixed(2)} kB`; 22 | } 23 | 24 | return `${size} bytes`; 25 | }; 26 | 27 | render() { 28 | return ( 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | {this.props.title} 41 | 42 | 43 | size: {this._showSize(this.props.size)} 44 | 45 | 46 | Downloaded at {moment(this.props.downloadAt).fromNow()} 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | const styles = StyleSheet.create({ 56 | row: { 57 | height: px2dp(380), 58 | flexDirection: 'row', 59 | justifyContent: 'flex-start', 60 | alignItems: 'center', 61 | backgroundColor: '#fff', 62 | paddingLeft: 10, 63 | paddingRight: 10, 64 | borderBottomWidth: StyleSheet.hairlineWidth, 65 | borderBottomColor: '#e9e9e9', 66 | }, 67 | 68 | cover: { 69 | width: px2dp(200), 70 | height: px2dp(380), 71 | marginRight: 10, 72 | justifyContent: 'center', 73 | alignItems: 'center', 74 | }, 75 | 76 | coverImage: { 77 | width: px2dp(200), 78 | height: px2dp(262), 79 | borderWidth: 2, 80 | borderColor: '#eee', 81 | elevation: 5, 82 | }, 83 | 84 | content: { 85 | flex: 1, 86 | height: px2dp(262), 87 | flexDirection: 'column', 88 | justifyContent: 'space-between', 89 | alignItems: 'flex-start', 90 | }, 91 | 92 | title: { 93 | fontSize: 18, 94 | color: '#000', 95 | }, 96 | 97 | desc: { 98 | fontSize: 16, 99 | }, 100 | 101 | time: { 102 | fontSize: 16, 103 | color: '#8e8e8e', 104 | }, 105 | }); 106 | -------------------------------------------------------------------------------- /src/screens/Download/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet } from 'react-native'; 3 | 4 | import { getDownloadItemsAsync, deleteOneDownloadItem } from '../../data/dataBase'; 5 | import Feeds from '../../components/Feeds'; 6 | import Book from './Book'; 7 | import { px2dp } from '../../utils'; 8 | 9 | const ITEM_HEIGHT = px2dp(380); 10 | 11 | export default class Download extends React.PureComponent { 12 | static navigatorStyle = { 13 | navBarHideOnScroll: true, 14 | navBarBackgroundColor: '#3F51B5', // This will be the TitleBars color when the react view is hidden and collapsed 15 | navBarTextColor: '#fff', 16 | statusBarColor: '#3F51B5', 17 | navBarTitleTextCentered: true, 18 | navBarTitleTextCentered: true, 19 | statusBarTextColorScheme: 'light', 20 | 21 | // iOS only 22 | statusBarHideWithNavBar: true, 23 | }; 24 | 25 | constructor(props) { 26 | super(props); 27 | 28 | this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); 29 | } 30 | 31 | onNavigatorEvent(event) { 32 | if (event.id == 'bottomTabSelected') { 33 | this.feeds.refresh(); 34 | } 35 | } 36 | 37 | _getItemLayout = (data, index) => { 38 | return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }; 39 | }; 40 | 41 | _readLocal = item => { 42 | this.props.navigator.push({ 43 | screen: 'app.Reader', // unique ID registered with Navigation.registerScreen 44 | title: item.title, 45 | passProps: { 46 | bookId: item.bookId, 47 | }, // Object that will be passed as props to the pushed screen (optional) 48 | animated: true, // does the push have transition animation or does it happen immediately (optional) 49 | }); 50 | }; 51 | 52 | _deleteOneItem = bookId => { 53 | this.feeds.removeOne(item => item.bookId === bookId); 54 | deleteOneDownloadItem(bookId); 55 | }; 56 | 57 | _renderItem = ({ item }) => { 58 | return ( 59 | this._readLocal(item)} 61 | remove={() => this._deleteOneItem(item.bookId)} 62 | {...item} 63 | key={item.bookId} 64 | /> 65 | ); 66 | }; 67 | 68 | render() { 69 | return ( 70 | (this.feeds = view)} 72 | getItemLayout={this._getItemLayout} 73 | navigator={this.props.navigator} 74 | renderItem={this._renderItem} 75 | fetchData={getDownloadItemsAsync} 76 | keyExtractor={item => item.bookId} 77 | /> 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/screens/Home/Book.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, View, Text, StyleSheet, TouchableNativeFeedback, TouchableHighlight, Platform } from 'react-native'; 3 | import moment from 'moment'; 4 | 5 | import { px2dp, SCREEN_WIDTH } from '../../utils'; 6 | import ImageWithPlaceHolder from '../../components/ImageWithPlaceHolder'; 7 | 8 | // var zh = require('moment/locale/zh-cn'); 9 | // moment.updateLocale('zh-cn', zh); 10 | // {this.props.language} 11 | 12 | export default class NewsCard extends React.PureComponent { 13 | renderContent = () => { 14 | return ( 15 | 16 | 17 | 22 | 23 | 24 | {this.props.title} 25 | 26 | 27 | {this.props.description === '' ? 'No description for now' : this.props.description} 28 | 29 | 30 | Last updated {moment(this.props.dates.build).fromNow()} 31 | 32 | 33 | 34 | ★ {this.props.counts.stars} 35 | 36 | {this.props.language} 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | render() { 44 | if (Platform.OS === 'ios') { 45 | return ( 46 | 47 | {this.renderContent()} 48 | 49 | ); 50 | } else if (Platform.OS === 'android') { 51 | return ( 52 | 53 | {this.renderContent()} 54 | 55 | ); 56 | } 57 | } 58 | } 59 | 60 | const styles = StyleSheet.create({ 61 | row: { 62 | height: px2dp(380), 63 | flexDirection: 'row', 64 | justifyContent: 'flex-start', 65 | alignItems: 'center', 66 | backgroundColor: '#fff', 67 | paddingLeft: 10, 68 | paddingRight: 10, 69 | borderBottomWidth: StyleSheet.hairlineWidth, 70 | borderBottomColor: '#e9e9e9', 71 | }, 72 | 73 | cover: { 74 | width: px2dp(200), 75 | height: px2dp(380), 76 | marginRight: 10, 77 | justifyContent: 'center', 78 | alignItems: 'center', 79 | }, 80 | 81 | coverImage: { 82 | width: px2dp(200), 83 | height: px2dp(262), 84 | borderWidth: 2, 85 | borderColor: '#eee', 86 | elevation: 5, 87 | }, 88 | 89 | content: { 90 | flex: 1, 91 | height: px2dp(262), 92 | flexDirection: 'column', 93 | justifyContent: 'space-between', 94 | alignItems: 'flex-start', 95 | }, 96 | 97 | title: { 98 | fontSize: 18, 99 | color: '#000', 100 | }, 101 | 102 | desc: { 103 | fontSize: 16, 104 | }, 105 | 106 | time: { 107 | fontSize: 16, 108 | color: '#8e8e8e', 109 | }, 110 | 111 | infoWrapper: { 112 | position: 'absolute', 113 | right: -10, 114 | bottom: 0, 115 | flexDirection: 'row', 116 | }, 117 | 118 | label: { 119 | height: px2dp(50), 120 | borderWidth: 1, 121 | borderColor: '#eee', 122 | padding: 5, 123 | backgroundColor: '#fff', 124 | marginRight: 5, 125 | justifyContent: 'center', 126 | alignItems: 'center', 127 | }, 128 | }); 129 | -------------------------------------------------------------------------------- /src/screens/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, ActivityIndicator, Text, StyleSheet, ScrollView, StatusBar, FlatList } from 'react-native'; 3 | 4 | import { px2dp, SCREEN_WIDTH, saveBookCover } from '../../utils'; 5 | 6 | import Feeds from '../../components/Feeds'; 7 | import Book from './Book'; 8 | import { getAllBooks } from '../../data'; 9 | 10 | const ITEM_HEIGHT = px2dp(380); 11 | 12 | export default class Home extends React.Component { 13 | static navigatorStyle = { 14 | navBarHideOnScroll: true, 15 | navBarBackgroundColor: '#3F51B5', // This will be the TitleBars color when the react view is hidden and collapsed 16 | navBarTextColor: '#fff', 17 | statusBarColor: '#3F51B5', 18 | navBarTitleTextCentered: true, 19 | statusBarTextColorScheme: 'light', 20 | 21 | // iOS only 22 | statusBarHideWithNavBar: true, 23 | }; 24 | 25 | constructor(props) { 26 | super(props); 27 | } 28 | 29 | _gotoDetail = item => { 30 | saveBookCover(item.cover.large); 31 | 32 | this.props.navigator.push({ 33 | screen: 'app.BookDetail', // unique ID registered with Navigation.registerScreen 34 | title: item.title, // navigation bar title of the pushed screen (optional) 35 | // titleImage: require('../../img/ic_news.png'), //navigation bar title image instead of the title text of the pushed screen (optional) 36 | passProps: { 37 | book: item, 38 | }, // Object that will be passed as props to the pushed screen (optional) 39 | animated: true, // does the push have transition animation or does it happen immediately (optional) 40 | backButtonTitle: '返回', // override the back button title (optional) 41 | backButtonHidden: false, // hide the back button altogether (optional) 42 | navigatorStyle: { 43 | // navBarHideOnScroll: true, 44 | // navBarTextColor: '#fff', // change the text color of the title (remembered across pushes) 45 | // // navBarTextFontFamily: 'font-name', // Changes the title font 46 | // navBarBackgroundColor: '#000', // change the background color of the nav bar (remembered across pushes) 47 | // drawUnderTabBar: true, // draw the screen content under the tab bar (the tab bar is always translucent) 48 | // statusBarBlur: true, // blur the area under the status bar, works best with navBarHidden:true 49 | // navBarBlur: true, // blur the entire nav bar, works best with drawUnderNavBar:true 50 | // navBarButtonColor: '#fff', 51 | }, // override the navigator style for the pushed screen (optional) 52 | navigatorButtons: {}, // override the nav buttons for the pushed screen (optional) 53 | }); 54 | }; 55 | 56 | _renderItem = ({ item }) => { 57 | return this._gotoDetail(item)} key={item.id} {...item} />; 58 | }; 59 | 60 | _getItemLayout = (data, index) => { 61 | return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }; 62 | }; 63 | 64 | render() { 65 | return ( 66 | item.id} 73 | /> 74 | ); 75 | } 76 | } 77 | 78 | const styles = StyleSheet.create({ 79 | subtitleWrapper: { 80 | height: px2dp(90), 81 | backgroundColor: '#9E9E9E', 82 | justifyContent: 'center', 83 | alignItems: 'flex-start', 84 | paddingLeft: 20, 85 | }, 86 | 87 | subtitle: { 88 | color: '#212121', 89 | fontSize: px2dp(38), 90 | fontWeight: 'bold', 91 | }, 92 | 93 | item: { 94 | padding: 25, 95 | borderBottomColor: '#BDBDBD', 96 | borderBottomWidth: px2dp(1), 97 | }, 98 | }); 99 | -------------------------------------------------------------------------------- /src/screens/More/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet, Image } from 'react-native'; 3 | 4 | import { getReadHisotry } from '../../data'; 5 | 6 | export default class Empty extends React.PureComponent { 7 | static navigatorStyle = { 8 | navBarHideOnScroll: false, 9 | navBarBackgroundColor: '#3F51B5', // This will be the TitleBars color when the react view is hidden and collapsed 10 | navBarTextColor: '#fff', 11 | statusBarColor: '#3F51B5', 12 | navBarTitleTextCentered: true, 13 | }; 14 | 15 | constructor(props) { 16 | super(props); 17 | } 18 | 19 | componentDidMount() {} 20 | 21 | render() { 22 | return ( 23 | 24 | 25 | Gitbook Reader v1.0.0 @le0zh 26 | 27 | ); 28 | } 29 | } 30 | 31 | const styles = StyleSheet.create({ 32 | wrapper: { 33 | flexDirection: 'column', 34 | justifyContent: 'center', 35 | alignItems: 'center', 36 | flex: 1, 37 | }, 38 | 39 | logo: { 40 | height: 200, 41 | width: 200, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/screens/OnlineReader/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet, WebView, Linking, ToastAndroid } from 'react-native'; 3 | 4 | export default class Reader extends React.PureComponent { 5 | // static navigatorButtons = { 6 | // rightButtons: [ 7 | // { 8 | // title: '使用浏览器打开', // for a textual button, provide the button title (label) 9 | // // icon: require('../img/ic_hero.png'), // if you want an image button 10 | // id: 'browser', // id for this button, given in onNavigatorEvent(event) to help understand which button was clicked 11 | // testID: 'e2e_rules', // optional, used to locate this view in end-to-end tests 12 | // disabled: false, // optional, used to disable the button (appears faded and doesn't interact) 13 | // showAsAction: 'ifRoom', // optional, Android only. Control how the button is displayed in the Toolbar. Accepted valued: 'ifRoom' (default) - Show this item as a button in an Action Bar if the system decides there is room for it. 'always' - Always show this item as a button in an Action Bar. 'withText' - When this item is in the action bar, always show it with a text label even if it also has an icon specified. 'never' - Never show this item as a button in an Action Bar. 14 | // buttonColor: 'blue', // Optional, iOS only. Set color for the button (can also be used in setButtons function to set different button style programatically) 15 | // buttonFontSize: 14, // Set font size for the button (can also be used in setButtons function to set different button style programatically) 16 | // buttonFontWeight: '600', // Set font weight for the button (can also be used in setButtons function to set different button style programatically) 17 | // }, 18 | // { 19 | // title: '分享', // for a textual button, provide the button title (label) 20 | // // icon: require('../img/ic_hero.png'), // if you want an image button 21 | // id: 'share', // id for this button, given in onNavigatorEvent(event) to help understand which button was clicked 22 | // disabled: false, // optional, used to disable the button (appears faded and doesn't interact) 23 | // showAsAction: 'ifRoom', // optional, Android only. Control how the button is displayed in the Toolbar. Accepted valued: 'ifRoom' (default) - Show this item as a button in an Action Bar if the system decides there is room for it. 'always' - Always show this item as a button in an Action Bar. 'withText' - When this item is in the action bar, always show it with a text label even if it also has an icon specified. 'never' - Never show this item as a button in an Action Bar. 24 | // buttonColor: 'blue', // Optional, iOS only. Set color for the button (can also be used in setButtons function to set different button style programatically) 25 | // buttonFontSize: 14, // Set font size for the button (can also be used in setButtons function to set different button style programatically) 26 | // buttonFontWeight: '600', // Set font weight for the button (can also be used in setButtons function to set different button style programatically) 27 | // }, 28 | // ], 29 | // }; 30 | 31 | static navigatorStyle = { 32 | tabBarHidden: true, 33 | navBarHideOnScroll: true, 34 | statusBarColor: '#3F51B5', 35 | }; 36 | 37 | constructor(props) { 38 | super(props); 39 | // if you want to listen on navigator events, set this up 40 | this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); 41 | } 42 | 43 | onNavigatorEvent(event) { 44 | // this is the onPress handler for the two buttons together 45 | if (event.type == 'NavBarButtonPress') { 46 | // this is the event type for button presses 47 | if (event.id == 'browser') { 48 | // this is the same id field from the static navigatorButtons definition 49 | Linking.openURL(this.props.url); 50 | } 51 | 52 | if (event.id == 'share') { 53 | ToastAndroid.show('即将到来', ToastAndroid.SHORT); 54 | } 55 | } 56 | } 57 | 58 | render() { 59 | return ; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/screens/Reader/TOC.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet, WebView, ScrollView, TouchableHighlight, BackAndroid } from 'react-native'; 3 | 4 | import RNFS from 'react-native-fs'; 5 | import fastXmlParser from 'fast-xml-parser'; 6 | 7 | import { saveReadProgress } from '../../data/dataBase'; 8 | import { px2dp } from '../../utils'; 9 | 10 | export default class TOC extends React.PureComponent { 11 | static navigatorStyle = { 12 | tabBarHidden: false, 13 | navBarHidden: false, 14 | statusBarColor: '#3F51B5', 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | selectedUrl: props.initSelectedSrc, 22 | }; 23 | } 24 | 25 | _onNavItemPress = src => { 26 | this.setState({ 27 | selectedUrl: src, 28 | }); 29 | 30 | this.props.onNavPress && this.props.onNavPress(src); 31 | }; 32 | 33 | _renderLevels = (item, level) => { 34 | let subItems = null; 35 | 36 | if (item.navPoint) { 37 | if (Array.isArray(item.navPoint)) { 38 | subItems = item.navPoint.map(subItem => { 39 | return this._renderLevels(subItem, level + 1); 40 | }); 41 | } else { 42 | subItems = this._renderLevels(item.navPoint, level + 1); 43 | } 44 | } 45 | 46 | const isSelected = this.state.selectedUrl === item.content._src; 47 | let textColor = '#000'; 48 | 49 | if (isSelected) { 50 | textColor = '#3F51B5'; 51 | } else if (level > 0) { 52 | textColor = '#000000B3'; 53 | } 54 | 55 | return ( 56 | this._onNavItemPress(item.content._src)}> 57 | 58 | 59 | 60 | {item.navLabel.text} 61 | 62 | 63 | {subItems} 64 | 65 | 66 | ); 67 | }; 68 | 69 | render() { 70 | return ( 71 | 72 | 73 | {this.props.toc.map(item => { 74 | return this._renderLevels(item, 0); 75 | })} 76 | 77 | 78 | ); 79 | } 80 | } 81 | 82 | const styles = StyleSheet.create({ 83 | tocItem: { 84 | height: px2dp(140), 85 | flexDirection: 'row', 86 | justifyContent: 'flex-start', 87 | alignItems: 'center', 88 | backgroundColor: '#fff', 89 | paddingLeft: 10, 90 | paddingRight: 10, 91 | borderBottomWidth: StyleSheet.hairlineWidth, 92 | borderBottomColor: '#0000004D', 93 | backgroundColor: '#eee', 94 | }, 95 | 96 | title1: { 97 | color: '#000', 98 | fontSize: px2dp(40), 99 | }, 100 | }); 101 | -------------------------------------------------------------------------------- /src/screens/Reader/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | Text, 5 | StyleSheet, 6 | Animated, 7 | ToastAndroid, 8 | WebView, 9 | ScrollView, 10 | TouchableOpacity, 11 | Modal, 12 | BackHandler, 13 | } from 'react-native'; 14 | 15 | import RNFS from 'react-native-fs'; 16 | import fastXmlParser from 'fast-xml-parser'; 17 | import Interactable from 'react-native-interactable'; 18 | 19 | import { getReadProgress, saveReadProgress } from '../../data/dataBase'; 20 | import { px2dp, SCREEN_HEIGHT, SCREEN_WIDTH } from '../../utils'; 21 | import { getDirFromBookId } from '../../data/bookFiles'; 22 | import TOC from './TOC'; 23 | 24 | const AnimatedToc = Animated.createAnimatedComponent(TOC); 25 | 26 | const SideMenuWidth = SCREEN_WIDTH * 4 / 5; 27 | const RemainingWidth = SCREEN_WIDTH - SideMenuWidth; 28 | 29 | const getInjectedScript = initPosition => ` 30 | function ready(fn) { 31 | if (document.readyState != 'loading'){ 32 | fn(); 33 | } else if (document.addEventListener) { 34 | document.addEventListener('DOMContentLoaded', fn); 35 | } else { 36 | document.attachEvent('onreadystatechange', function() { 37 | if (document.readyState != 'loading') 38 | fn(); 39 | }); 40 | } 41 | } 42 | 43 | ready(function(){ 44 | setTimeout(function(){ 45 | window.scrollTo(0, ${initPosition}); 46 | }, 0); 47 | 48 | window.onscroll = function (e) { 49 | window.postMessage(window.pageYOffset); 50 | }; 51 | }) 52 | `; 53 | 54 | export default class Empty extends React.PureComponent { 55 | static navigatorStyle = { 56 | tabBarHidden: true, 57 | // navBarHideOnScroll: true, 58 | statusBarColor: '#3F51B5', 59 | navBarTitleTextCentered: true, 60 | navBarButtonColor: '#fff', 61 | navBarBackgroundColor: '#3F51B5', 62 | navBarTextColor: '#fff', 63 | statusBarTextColorScheme: 'light', 64 | 65 | // iOS only 66 | statusBarHideWithNavBar: true, 67 | }; 68 | 69 | static navigatorButtons = { 70 | rightButtons: [], 71 | leftButtons: [ 72 | { 73 | title: 'TOC', // if you want a textual button 74 | buttonColor: '#fff', 75 | id: 'sideMenu', // id of the button which will pass to your press event handler. See the section bellow for Android specific button ids 76 | }, 77 | ], 78 | }; 79 | 80 | constructor(props) { 81 | super(props); 82 | 83 | this.state = { 84 | content: '', 85 | toc: [], 86 | }; 87 | 88 | this.bookDir = getDirFromBookId(this.props.bookId); 89 | this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); 90 | 91 | const progress = getReadProgress(this.props.bookId); 92 | this.initalPage = progress.src; 93 | this.initalPos = progress.position; 94 | 95 | this.readPos = progress.position; 96 | this.readPage = progress.src; 97 | this.tocIsShown = false; 98 | } 99 | 100 | componentDidMount() { 101 | RNFS.readFile(`${this.bookDir}/toc.json`).then(content => { 102 | const toc = JSON.parse(content); 103 | 104 | this.setState({ toc }); 105 | 106 | this._loadContent(this.initalPage); 107 | }); 108 | } 109 | 110 | onNavigatorEvent(event) { 111 | if (event.type == 'NavBarButtonPress') { 112 | if (event.id == 'sideMenu') { 113 | this.interactableView.setVelocity({ x: this.tocIsShown ? -2000 : 2000 }); 114 | } 115 | } 116 | 117 | if (event.id === 'willDisappear') { 118 | // 保存阅读进度 119 | saveReadProgress(this.props.bookId, this.readPage, this.readPos); 120 | } 121 | } 122 | 123 | _loadContent = src => { 124 | // 修改页面样式 125 | // todo: 修改字体大小,默认为 font-size: 1.125em; 126 | const fixedStyle = ' style="padding: 0 10px; font-size: 1em" '; 127 | const fixedMeta = ''; 128 | 129 | RNFS.readFile(`${this.bookDir}/content/${src}`).then(content => { 130 | this.setState({ 131 | content: content 132 | // .replace('', fixedMeta) 133 | .replace(' { 139 | this.interactableView.setVelocity({ x: 2000 }); 140 | }; 141 | 142 | _onNavPress = src => { 143 | this.interactableView.setVelocity({ x: -2000 }); 144 | this._loadContent(src); 145 | this.readPage = src; 146 | }; 147 | 148 | _onMessage = e => { 149 | this.readPos = parseInt(e.nativeEvent.data, 10); 150 | }; 151 | 152 | _onSnap = e => { 153 | this.tocIsShown = e.nativeEvent.index === 0; 154 | }; 155 | 156 | render() { 157 | if (this.state.toc.length === 0) { 158 | return null; 159 | } 160 | 161 | return ( 162 | 163 | 173 | 174 | 175 | (this.interactableView = view)} 177 | horizontalOnly={true} 178 | snapPoints={[{ x: 0 }, { x: -SideMenuWidth }]} 179 | boundaries={{ right: RemainingWidth / 2 }} 180 | initialPosition={{ x: -SideMenuWidth }} 181 | onSnap={this._onSnap} 182 | > 183 | 190 | 191 | 192 | 193 | 194 | ); 195 | } 196 | } 197 | 198 | const styles = StyleSheet.create({ 199 | wrapper: { 200 | flex: 1, 201 | backgroundColor: '#fff', 202 | }, 203 | 204 | touchArea: { 205 | height: 100, 206 | width: 100, 207 | // backgroundColor: '#00000010', 208 | backgroundColor: 'transparent', 209 | position: 'absolute', 210 | left: (SCREEN_WIDTH - 100) / 2, 211 | top: (SCREEN_HEIGHT - 100) / 2, 212 | }, 213 | 214 | topBar: { 215 | height: px2dp(200), 216 | width: SCREEN_WIDTH, 217 | backgroundColor: '#000000', 218 | position: 'absolute', 219 | left: 0, 220 | top: 0, 221 | }, 222 | 223 | sideMenuContainer: { 224 | position: 'absolute', 225 | top: 0, 226 | left: -RemainingWidth, 227 | right: 0, 228 | bottom: 0, 229 | flexDirection: 'row', 230 | zIndex: 1002, 231 | }, 232 | 233 | sideMenu: { 234 | left: 0, 235 | width: SCREEN_WIDTH, 236 | paddingLeft: RemainingWidth, 237 | backgroundColor: '#eee', 238 | paddingTop: 0, 239 | elevation: 5, 240 | }, 241 | 242 | webView: { 243 | flex: 1, 244 | }, 245 | }); 246 | -------------------------------------------------------------------------------- /src/screens/Search/ResultItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, View, Text, StyleSheet, TouchableHighlight } from 'react-native'; 3 | 4 | import moment from 'moment'; 5 | 6 | import { px2dp, SCREEN_WIDTH } from '../../utils'; 7 | 8 | export default class ResultItem extends React.PureComponent { 9 | render() { 10 | return ( 11 | 12 | 13 | {this.props.title} 14 | {this.props.desc} 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | row: { 23 | height: px2dp(250), 24 | flexDirection: 'column', 25 | justifyContent: 'center', 26 | alignItems: 'flex-start', 27 | backgroundColor: '#fff', 28 | paddingLeft: 10, 29 | paddingRight: 10, 30 | borderBottomWidth: StyleSheet.hairlineWidth, 31 | borderBottomColor: '#e9e9e9', 32 | width: SCREEN_WIDTH, 33 | }, 34 | 35 | title: { 36 | fontSize: 18, 37 | color: '#000', 38 | }, 39 | 40 | desc: { 41 | fontSize: 16, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/screens/Search/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet, Image, TextInput, TouchableHighlight } from 'react-native'; 3 | 4 | import Camera from 'react-native-camera'; 5 | 6 | import { search } from '../../data/search'; 7 | import { px2dp, SCREEN_WIDTH } from '../../utils'; 8 | 9 | import Feeds from '../../components/Feeds'; 10 | import ResultItem from './ResultItem'; 11 | 12 | const ITEM_HEIGHT = px2dp(250); 13 | 14 | export default class Search extends React.PureComponent { 15 | static navigatorStyle = { 16 | navBarHidden: false, 17 | navBarBackgroundColor: '#3F51B5', // This will be the TitleBars color when the react view is hidden and collapsed 18 | navBarTextColor: '#fff', 19 | statusBarColor: '#3F51B5', 20 | navBarTitleTextCentered: true, 21 | statusBarTextColorScheme: 'light', 22 | 23 | // iOS only 24 | statusBarHideWithNavBar: true, 25 | }; 26 | 27 | constructor(props) { 28 | super(props); 29 | 30 | this.state = { 31 | mode: 'init', 32 | }; 33 | 34 | this.q = ''; 35 | } 36 | 37 | _doSearch = e => { 38 | if (e.nativeEvent.text.trim() === '') { 39 | return; 40 | } 41 | 42 | this.q = e.nativeEvent.text.trim(); 43 | 44 | if (this.state.mode === 'show') { 45 | // 已经显示结果了,直接刷新下Feeds 46 | this.feeds && this.feeds.refresh(); 47 | } else { 48 | this.setState({ 49 | mode: 'show', 50 | }); 51 | } 52 | }; 53 | 54 | _gotoDetail = (id, title) => { 55 | // saveBookCover(item.cover.large); 56 | 57 | this.props.navigator.push({ 58 | screen: 'app.BookDetail', // unique ID registered with Navigation.registerScreen 59 | title: title, // navigation bar title of the pushed screen (optional) 60 | backButtonTitle: '', // override the back button title (optional) 61 | passProps: { 62 | id, 63 | book: null, 64 | fromSearch: true, 65 | }, 66 | animated: true, // does the push have transition animation or does it happen immediately (optional) 67 | }); 68 | }; 69 | 70 | _renderItem = ({ item }) => { 71 | return this._gotoDetail(item.id, item.title)} />; 72 | }; 73 | 74 | _getItemLayout = (data, index) => { 75 | return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }; 76 | }; 77 | 78 | _doScan = () => { 79 | this.props.navigator.push({ 80 | screen: 'app.Camera', // unique ID registered with Navigation.registerScreen 81 | title: 'QR', 82 | }); 83 | }; 84 | 85 | _renderSearchResult = () => { 86 | return ( 87 | (this.feeds = view)} 89 | navigator={this.props.navigator} 90 | renderItem={this._renderItem} 91 | fetchData={page => search(this.q, page)} 92 | keyExtractor={item => item.id} 93 | getItemLayout={this._getItemLayout} 94 | /> 95 | ); 96 | }; 97 | 98 | render() { 99 | return ( 100 | 101 | 102 | 112 | 113 | 114 | 115 | 116 | 117 | {this.state.mode === 'show' ? this._renderSearchResult() : null} 118 | 119 | ); 120 | } 121 | } 122 | 123 | const styles = StyleSheet.create({ 124 | wrapper: { 125 | flexDirection: 'column', 126 | justifyContent: 'flex-start', 127 | alignItems: 'center', 128 | flex: 1, 129 | width: SCREEN_WIDTH, 130 | backgroundColor: 'transparent', 131 | }, 132 | 133 | preview: { 134 | flex: 1, 135 | justifyContent: 'flex-end', 136 | alignItems: 'center', 137 | }, 138 | 139 | textInputWrapper: { 140 | backgroundColor: '#3F51B5', 141 | width: SCREEN_WIDTH, 142 | justifyContent: 'space-around', 143 | alignItems: 'center', 144 | flexDirection: 'row', 145 | height: px2dp(100), 146 | }, 147 | 148 | input: { 149 | height: px2dp(90), 150 | marginLeft: 5, 151 | width: SCREEN_WIDTH - 70, 152 | backgroundColor: '#C5CAE9', 153 | color: '#fff', 154 | // borderColor: 'gray', 155 | // borderWidth: 1, 156 | borderRadius: 4, 157 | }, 158 | 159 | scanIcon: { 160 | height: 40, 161 | width: 40, 162 | }, 163 | }); 164 | -------------------------------------------------------------------------------- /src/screens/index.js: -------------------------------------------------------------------------------- 1 | import { Navigation } from 'react-native-navigation'; 2 | 3 | import Home from './Home'; 4 | import BookDetail from './BookDetail'; 5 | import OnlineReader from './OnlineReader'; 6 | import CoverHeader from './BookDetail/CoverHeader'; 7 | import More from './More'; 8 | import Reader from './Reader'; 9 | import Download from './Download'; 10 | import Search from './Search'; 11 | import Camera from './Camera'; 12 | 13 | // register all screens of the app (including internal ones) 14 | export function registerScreens() { 15 | Navigation.registerComponent('app.Home', () => Home); 16 | Navigation.registerComponent('app.BookDetail', () => BookDetail); 17 | Navigation.registerComponent('app.BookDetail.Cover', () => CoverHeader); 18 | Navigation.registerComponent('app.OnlineReader', () => OnlineReader); 19 | Navigation.registerComponent('app.More', () => More); 20 | Navigation.registerComponent('app.Reader', () => Reader); 21 | Navigation.registerComponent('app.Download', () => Download); 22 | Navigation.registerComponent('app.Search', () => Search); 23 | Navigation.registerComponent('app.Camera', () => Camera); 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { PixelRatio, Dimensions } from 'react-native'; 4 | 5 | exports.px2dp = function px2dp(px: number): number { 6 | if (typeof px !== 'number') { 7 | console.error('输入必须为数字'); 8 | return px; 9 | } 10 | 11 | return px / PixelRatio.get(); 12 | }; 13 | 14 | /** 15 | * 格式化字符串 16 | * @template 模板字符串 17 | * const message = stringFormat('{0} and {1} is awesome!', 'react', 'rn); 18 | * >> message: 'react and rn is awesome!' 19 | */ 20 | exports.stringFormat = function stringFormat(template: string, ...values: any): string { 21 | const args = values; 22 | return template.replace(/{(\d+)}/g, (match, number) => { 23 | return typeof args[number] !== 'undefined' ? args[number] : match; 24 | }); 25 | }; 26 | 27 | /** 28 | * 屏幕尺寸 29 | */ 30 | const { width, height } = Dimensions.get('window'); 31 | exports.SCREEN_WIDTH = width; 32 | exports.SCREEN_HEIGHT = height; 33 | 34 | let currentBookCover = ''; 35 | /** 36 | * 暂存当前书的封面 37 | */ 38 | exports.saveBookCover = function(cover) { 39 | currentBookCover = cover; 40 | }; 41 | 42 | /** 43 | * 获取暂存的当前书的封面 44 | */ 45 | exports.getBookCover = function() { 46 | return currentBookCover; 47 | }; 48 | -------------------------------------------------------------------------------- /target/gitbookreader.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/le0zh/gitbook-reader-rn/8a211c12e85e4b7c27084ee8b11f86f30a6fbe6a/target/gitbookreader.apk --------------------------------------------------------------------------------